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

  • 内容
  • 相关
et_highlighter51

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 参考资料

FoolMitAh/CVE-2018-14729

[1]  https://github.com/FoolMitAh/CVE-2018-14729

  • 打赏支付宝扫一扫
  • 打赏微信扫一扫
  • 打赏企鹅扫一扫

本文标签:

版权声明:若无特殊注明,本文皆为《Mushroom》原创,转载请保留文章出处。

本文链接:Discuz! 1.5-2.5 命令执行漏洞分析(CVE-2018-14729) - http://www.0xmg.com/post-1533.html

发表评论

电子邮件地址不会被公开。 必填项已用*标注