0x01 WooYun-2014-51687
输入1',报错了。 后面跟#等注释符号,显示有waf。 尝试使用万能密码,1' or '1'='1,还是报错了,突然觉得这不会是整形的吧。。。用and测试了下,果然是整形注入。 使用联合查询1 union select 1,2的时候有新的报错。 由于有报错信息,依次删除字符,来判断,到底是哪个关键字引起的WAF。最后测试出,,没问题,单独使用select也没问题,看来问题出在union上了。 采用大小写,编码,内联注释等绕过,发现均不可绕过,于是改用盲注,但是每次到使用select的时候同样也会有WAF。。。纠结了一会就放弃了,直接来看源码吧。
0x02 源码分析
WooYun
-2014-51687 Source
<?php
if(isset($_GET['Submit'])){
$id = $_GET['id'];
$id = CheckSql($id,'select');
$getid = "SELECT first_name, last_name FROM users WHERE user_id = $id";
$result = mysql_query($getid) or die('<pre>' . mysql_error() . '</pre>' );
$num = mysql_numrows($result);
$i = 0;
while ($i < $num) {
$first = mysql_result($result,$i,"first_name");
$last = mysql_result($result,$i,"last_name");
echo '<pre>';
echo 'ID: ' . $id . '<br>First name: ' . $first . '<br>Surname: ' . $last;
echo '</pre>';
$i++;
}
}
function CheckSql($db_string,$querytype='select')
{
$clean = '';
$error='';
$old_pos = 0;
$pos = -1;
$userIP = GetIP();
$getUrl = GetCurUrl();
if($querytype=='select')
{
$notallow1 = "[^0-9a-z@\._-]{1,}(union|sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}";
if(eregi($notallow1,$db_string))
{
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;
}
$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)));
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";
}
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";
}
elseif (preg_match('~\([^)]*?select~s', $clean) != 0)
{
$fail = true;
$error="sub select detect";
}
if (!empty($fail))
{
exit("<font size='5' color='red'>Safe Alert: Request Error step 2!</font>");
}
else
{
return $db_string;
}
}
function GetCurUrl()
{
if(!empty($_SERVER["REQUEST_URI"]))
{
$scriptName = $_SERVER["REQUEST_URI"];
$nowurl = $scriptName;
}
else
{
$scriptName = $_SERVER["PHP_SELF"];
if(empty($_SERVER["QUERY_STRING"])) {
$nowurl = $scriptName;
}
else {
$nowurl = $scriptName."?".$_SERVER["QUERY_STRING"];
}
}
return $nowurl;
}
function getIP() {
return $_SERVER ["HTTP_X_FORWARDED_FOR"];
}
?>
这次的代码有点长。。。一点一点来看。 一开始对select语句做了检查 接着来看第一个检查语句 如果上列关键字不是以数字,字母,@,.,_,-开头和结尾都会被WAF检测出来,也就是说我只要满足其中一个条件就可以绕过此限制了,难怪之前使用/*!union*/报错的是step 1,而使用/*!12345union*/报错的是step 2。这里多说一下,/*!12345union*/里面的12345其实是mysql数据库的版本号,指的是大于此版本号的数据库可以正常使用。 这一大串其实就是说将两个'包含起来的字符串过滤掉,这个看了好久加上测试才看个大概,最后那个$s$没看懂,array()里面的好像是将空白字符换成空格,看点蒙,大佬看懂了指点一下阿,这到底要干啥。。。 如果有union的时候并且有以union开头或者不是[a-z]union开头或者结尾的时候就会报错,/*!12345union*/不满足条件。 如果有注释的话,就会报错,注意,这里是最难弄的地方,等会看payload再来说这个怎么绕过。 如果select前面有(则报错,这里就是为什么我之前盲注的时候select会报错的原因。
3.漏洞利用
好了,分析完了,开始讲漏洞利用了。 来简单分析下,为什么这样写就可以绕过去。 首先/*!12345union*/可以绕过去union的检查,关键的来了,使用两个`将单引号转义,在前面和后面各来一个,以此将中间的代码跳出上面所有的检查,所以这里就直接绕过了。接着我们去数据库里面看看这样的语句是如何执行成功的。 这里有#将后面都注释掉了,然后1 and @'就变成了0,因为@是用来定义用户变量的,这里的变量没有定义所以是Null,最后运算的结果就是0了。 不能不说,能搞出这样绕过的真的厉害,学习了。