Lucifaer's Blog.

Discuz! 1.5-2.5 命令执行漏洞分析(CVE-2018-14729)

Word count: 1,144 / Reading time: 5 min
2018/08/29 Share

鸡肋的漏洞,不过官方的解决方案也是有点意思…

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行:

1
@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行:

1
2
3
4
$tablesstr = '';
foreach($tables as $table) {
$tablesstr .= '"'.$table.'" ';
}

跟一下$table的获取流程,在上面的第143行:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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');
}
}

可以看到:

1
2
C::t('common_setting')->update('custombackup', empty($_GET['customtables'])? '' : $_GET['customtables']);
$tables = & $_GET['customtables'];

首先会从$_GET的数组中获取customtables字段的内容,判断内容是否为空,不为空则将从外部获取到的customtables字段内容写入common_setting表的skey=custombackupsvalue字段,写入过程中会将这个字段做序列化存储:

之后再将该值赋给$tables

至此可以看到漏洞产生的原因是由于shell_exec()中的$tablesstr可控,导致代码注入。

0x03 漏洞利用

漏洞的调用栈如下:

1
admin.php->source/class/discuz/discuz_admincp.php->source/admincp/admincp_db.php

跟着漏洞的调用栈看一下如何利用。

首先在admin.php中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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);$actiondb。也就说需要构造参数满足:

1
2
3
$admincp->isfounder && in_array($action, $admincp_actions_founder) # 为真

$admincp->allow($action, $operation, $do) # 为真

$admincp->isfounder是确认当前用户的,返回为True,这里只需要构造$actiondb

跟进require $admincp->admincpfile($action);

1
2
3
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:跟进看一下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
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中:

1
2
3
4
5
6
public function __construct() {
$this->_init_env();
$this->_init_config();
$this->_init_input();
$this->_init_output();
}

接着跟进到_init_input()中:

1
2
3
4
5
...
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行写了一个错误…:

1
list(, $mysql_base) = DB::fetch($query, DB::$drivertype == 'mysqli' ? MYSQLI_NUM : MYSQL_NUM);

调用了一个未声明的静态变量,所以该功能直接是挂掉的,没有办法使用,可谓是简单粗暴…

0x07 参考资料

FoolMitAh/CVE-2018-14729

CATALOG
  1. 1. 0x00 漏洞简述
    1. 1.1. 漏洞信息
    2. 1.2. 漏洞影响版本
  2. 2. 0x01 漏洞复现
  3. 3. 0x02 漏洞分析
  4. 4. 0x03 漏洞利用
  5. 5. 0x04 参数获取问题
  6. 6. 0x05 修复方法
  7. 7. 0x06 Discuz 3.4的做法
  8. 8. 0x07 参考资料