作者:lucifaer
作者博客:https://www.lucifaer.com/
雞肋的漏洞,不過官方的解決方案也是有點意思…
0x00 漏洞簡述
漏洞信息
8月27號有人在GitHub上公布了有關Discuz 1.5-2.5版本中后臺數據庫備份功能存在的命令執行漏洞的細節。
漏洞影響版本
Discuz! 1.5-2.5
0x01 漏洞復現
官方論壇下載相應版本就好。
0x02 漏洞分析
需要注意的是這個漏洞其實是需要登錄后臺的,并且能有數據庫備份權限,所以比較雞肋。
我這邊是用Discuz! 2.5完成漏洞復現的,并用此進行漏洞分析的。
漏洞點在:source/admincp/admincp_db.php第296行:
@shell_exec($mysqlbin.'mysqldump --force --quick '.($db->version() > '4.1' ? '--skip-opt --create-options' : '-all').' --add-drop-table'.($_GET['extendins'] == 1 ? ' --extended-insert' : '').''.($db->version() > '4.1' && $_GET['sqlcompat'] == 'MYSQL40' ? ' --compatible=mysql40' : '').' --host="'.$dbhost.($dbport ? (is_numeric($dbport) ? ' --port='.$dbport : ' --socket="'.$dbport.'"') : '').'" --user="'.$dbuser.'" --password="'.$dbpw.'" "'.$dbname.'" '.$tablesstr.' > '.$dumpfile);
在shell_exec()函數中可控點在$tablesstr,向上看到第281行:
$tablesstr = '';
foreach($tables as $table) {
$tablesstr .= '"'.$table.'" ';
}
跟一下$table的獲取流程,在上面的第143行:
if($_GET['type'] == 'discuz' || $_GET['type'] == 'discuz_uc')
{
$tables = arraykeys2(fetchtablelist($tablepre), 'Name');
}
elseif($_GET['type'] == 'custom')
{
$tables = array();
if(empty($_GET['setup']))
{
$tables = C::t('common_setting')->fetch('custombackup', true);
}
else
{
C::t('common_setting')->update('custombackup', empty($_GET['customtables'])? '' : $_GET['customtables']);
$tables = & $_GET['customtables'];
}
if( !is_array($tables) || empty($tables))
{
cpmsg('database_export_custom_invalid', '', 'error');
}
}
可以看到:
C::t('common_setting')->update('custombackup', empty($_GET['customtables'])? '' : $_GET['customtables']);
$tables = & $_GET['customtables'];
首先會從$_GET的數組中獲取customtables字段的內容,判斷內容是否為空,不為空則將從外部獲取到的customtables字段內容寫入common_setting表的skey=custombackup的svalue字段,寫入過程中會將這個字段做序列化存儲:

之后再將該值賦給$tables。
至此可以看到漏洞產生的原因是由于shell_exec()中的$tablesstr可控,導致代碼注入。
0x03 漏洞利用
漏洞的調用棧如下:
admin.php->source/class/discuz/discuz_admincp.php->source/admincp/admincp_db.php
跟著漏洞的調用棧看一下如何利用。
首先在admin.php中:
if(empty($action) || $frames != null) {
$admincp->show_admincp_main();
} elseif($action == 'logout') {
$admincp->do_admin_logout();
dheader("Location: ./index.php");
} elseif(in_array($action, $admincp_actions_normal) || ($admincp->isfounder && in_array($action, $admincp_actions_founder))) {
if($admincp->allow($action, $operation, $do) || $action == 'index') {
require $admincp->admincpfile($action);
} else {
cpheader();
cpmsg('action_noaccess', '', 'error');
}
} else {
cpheader();
cpmsg('action_noaccess', '', 'error');
}
關鍵點在構造參數滿足require $admincp->admincpfile($action);且$action為db。也就說需要構造參數滿足:
$admincp->isfounder && in_array($action, $admincp_actions_founder) # 為真
$admincp->allow($action, $operation, $do) # 為真
$admincp->isfounder是確認當前用戶的,返回為True,這里只需要構造$action為db。
跟進require $admincp->admincpfile($action);:
function admincpfile($action) {
return './source/admincp/admincp_'.$action.'.php';
}
這里就包含了source/admincp/admincp_db.php。跟進看一下:

這邊需要滿足$operation == 'export',同時存在exportsubmit字段。
之后,

需要構造file字段,同時$_GET['type'] == 'custom'且$_GET['setup']和$_GET['customtables']非空。向下跟,還需要滿足最后一個條件$_GET['method'] != 'multivol',這樣才能調用else中的操作,完成代碼注入。
有了上面的這些基礎分析,我們抓個符合上方條件的包來看一下。經過測試,


這樣可以抓到符合我們條件的請求包。

接下來只需要將customtables的內容更改一下就可以造成命令執行了:


效果為:

0x04 參數獲取問題
通過上面的分析可以看到最終可控參數的獲取都是利用$_GET來獲取的,但是我們在構造時發送的是post數據,那么為什么會照常獲取到呢?
在admin.php第18行包含了source/class/class_core.php:跟進看一下:
...
C::creatapp();
class core
{
...
public static function creatapp() {
if(!is_object(self::$_app)) {
self::$_app = discuz_application::instance();
}
return self::$_app;
}
...
}
跟進到source/class/discuz/discuz_application.php中:
public function __construct() {
$this->_init_env();
$this->_init_config();
$this->_init_input();
$this->_init_output();
}
接著跟進到_init_input()中:
...
if($_SERVER['REQUEST_METHOD'] == 'POST' && !empty($_POST)) {
$_GET = array_merge($_GET, $_POST);
}
...
可以看到如果構造了post請求,Discuz的核心類會將$_GET和$_POST這兩個list拼接到一起,賦給$_GET數組。
0x05 修復方法
可以利用addslashes()對可控點進行限制,同時利用escapeshellarg()函數來限制$tablesstr執行命令。
0x06 Discuz 3.4的做法
Discuz 3.4非常有趣的一點不是把這個漏洞修了,而是直接在source/admincp/admincp_db.php第307行寫了一個錯誤…:
list(, $mysql_base) = DB::fetch($query, DB::$drivertype == 'mysqli' ? MYSQLI_NUM : MYSQL_NUM);
調用了一個未聲明的靜態變量,所以該功能直接是掛掉的,沒有辦法使用,可謂是簡單粗暴…
0x07 參考資料
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/763/
暫無評論