0x01 前言
这次挖洞其实并不是冲着拿CVE去的,而是为了给HITB-XCTF GSEC CTF Singapore 2018
出题。因为是国际赛,想着题目应该要有点难度,所以就打算出个小型CMS的0day挖掘题。之所以选上BigTree CMS
,也只是因为这是学(吴)长(神)挖过的CMS。不过因为是后台漏洞,比赛过程中还是出现了各种各样的非预期状况(各种搅屎),可以说是非常失败了Orz……
0x02 挖掘
首先是全局搜索了一些危险函数,然后发现call_user_func()
这个函数在/core/admin/auto-modules/forms/proccess.php
这个文件中出现频率很高,所以看这个文件。
首先是第6、7行有:
if ($bigtree["form"]["hooks"]["pre"]) {
$bigtree["preprocessed"] = call_user_func($bigtree["form"]["hooks"]["pre"],$_POST);
...
}
这个$bigtree["form"]["hooks"]
是developer
权限的用户可以更改的:
可以看到有四个可以修改的hook
,分别是$bigtree["form"]["hooks"]["edit"]
、$bigtree["form"]["hooks"]["pre"]
、$bigtree["form"]["hooks"]["post"]
、$bigtree["form"]["hooks"]["publish"]
。
回到代码,这里乍看之下bigtree["form"]["hooks"]["pre"]
,$_POST
都是我们可控的,那么我们可以不可以任意代码执行呢?我的答案是不能,因为第二个参数$_POST
是一个数组,而我想不到一个可以执行数组中的代码的函数。如果有师傅有办法,望不吝赐教。
那么继续往下看,在同文件的第182行有:
if ($bigtree["form"]["hooks"]["post"]) {
call_user_func($bigtree["form"]["hooks"]["post"],$edit_id,$item,$did_publish);
}
这里的call_user_func()
传入了四个参数,查看手册可知:
mixed call_user_func ( callable
$callback
[, mixed$parameter
[, mixed$...
]] )第一个参数
callback
是被调用的回调函数,其余参数是回调函数的参数。
也就是说,后面三个参数都会传入第一个参数。那么有没有哪个可以执行代码的函数能传入三个参数呢?没错,就是preg_replace()
!我们只要使用e
修饰符就可以执行命令。
我们看看后面的三个参数是否可控。同文件第78、81、94行分别定义了这三个参数:
$edit_id = $_POST["id"] ? $_POST["id"] : false;
...
$item = $bigtree["entry"];
...
$did_publish = false;
$edit_id
我们显然可控,$did_publish
是bool值,但在使用preg_replace()
执行代码时它的位置其实不是很重要,那么重头戏就在$item
了。同文件42行开始有:
$bigtree["entry"] = array();
$bigtree["post_data"] = $_POST;
...
foreach ($bigtree["form"]["fields"] as $resource) {
$field = array(
...
"key" => $resource["column"],
...
"ignore" => false,
"input" => $bigtree["post_data"][$resource["column"]],
...
);
$output = BigTreeAdmin::processField($field);
if (!is_null($output)) {
$bigtree["entry"][$field["key"]] = $output;
}
}
注意到其中有用到我们POST传入的数据。
在/core/admin/auto-modules/form.php
第9行有:
$bigtree["form"] = $form = BigTreeAutoModule::getForm($bigtree["module_action"]["form"]);
跟入/core/inc/bigtree/auto-modules.php
第611行,有:
static function getForm($id,$decode_ipl = true) {
...
$form = sqlfetch(sqlquery("SELECT * FROM bigtree_module_forms WHERE id = '".sqlescape($id)."'"));
$form["fields"] = json_decode($form["fields"],true);
...
// For backwards compatibility
if (is_array($form["fields"])) {
$related_fields = array();
foreach ($form["fields"] as $field) {
$related_fields[$field["column"]] = $field;
}
$form["fields"] = $related_fields;
}
return $form;
}
可以看到$bigtree["form"]["fields"]
是从数据库中取出的,直接进数据库查看其结构:
这样我们就获得了$resource["column"]
可能为quote
、author
、source
。
再跟入/core/inc/bigtree/admin.php
第6473行:
static function processField($field) {
global $admin,$bigtree,$cms;
// Save current context
$bigtree["saved_extension_context"] = $bigtree["extension_context"];
// Check if the field type is stored in an extension
if (strpos($field["type"],"*") !== false) {
list($extension,$field_type) = explode("*",$field["type"]);
$bigtree["extension_context"] = $extension;
$field_type_path = SERVER_ROOT."extensions/$extension/field-types/$field_type/process.php";
} else {
$field_type_path = BigTree::path("admin/form-field-types/process/".$field["type"].".php");
}
// If we have a customized handler for this data type, run it.
if (file_exists($field_type_path)) {
include $field_type_path;
// If it's explicitly ignored return null
if ($field["ignore"]) {
return null;
} else {
$output = $field["output"];
}
// Fall back to default handling
} else {
if (is_array($field["input"])) {
$output = $field["input"];
} else {
$output = BigTree::safeEncode($field["input"]);
}
}
...
return $output;
}
从数据库中我们可以知道,当$resource["column"]
为quote
时,$field["type"]
为textarea
,所以$field_type_path
为admin/form-field-types/process/textarea.php
,但这个文件不存在,所以就会有:
$output = $field["input"];
这个$field["input"]
是从我们POST传入的数据中取得的,我们可控,所以最后就相当于:
$bigtree["entry"] = array();
$bigtree["entry"]["quote"] = $_POST["quote"];
$item = $bigtree["entry"];
所以$item
为一个数组,且我们部分可控。我们再查看preg_replace
的手册:
mixed preg_replace ( mixed
$pattern
, mixed$replacement
, mixed$subject
[, int$limit
= -1 [, int&$count
]] )搜索
subject
中匹配pattern
的部分, 以replacement
进行替换。
可以看到传入的参数类型都是mixed
,也就是我们也可以传入数组,且传入数组时有:
如果
pattern
和replacement
都是数组,每个pattern
使用replacement
中对应的元素进行替换。如果replacement
中的元素比pattern
中的少,多出来的pattern
使用空字符串进行替换。
所以只要我们让$edit_id
也为数组即可。最后的POC为:
0x03 后记
比赛时并没有选手使用这个洞进行攻击,而是挖到了其他的RCE漏洞,很强。而在比赛结束后,我也顺手把这个洞申请到了CVE-ID并给这个项目提了一个issue。
可以看到该CMS的开发者认为developer
权限的用户拥有执行任意代码的权限,而如果想以低权限用户利用这个漏洞就需要配合一些社工手段,过于困难,所以他并不承认这是一个漏洞。
在于他的交流中,我不由得有了两个疑问:
- 是否CMS的高权限用户理应拥有任意代码执行的权限呢?
- 一个漏洞难以利用就不算是漏洞了吗?
开发人员和安全研究人员对于这两个问题,可能有不同的答案吧。