CVE-2018-17030挖掘

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"]可能为quoteauthorsource

再跟入/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_pathadmin/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,也就是我们也可以传入数组,且传入数组时有:

如果patternreplacement 都是数组,每个pattern使用replacement中对应的元素进行替换。如果replacement中的元素比pattern中的少,多出来的pattern使用空字符串进行替换。

所以只要我们让$edit_id也为数组即可。最后的POC为:


0x03 后记

比赛时并没有选手使用这个洞进行攻击,而是挖到了其他的RCE漏洞,很强。而在比赛结束后,我也顺手把这个洞申请到了CVE-ID并给这个项目提了一个issue

可以看到该CMS的开发者认为developer权限的用户拥有执行任意代码的权限,而如果想以低权限用户利用这个漏洞就需要配合一些社工手段,过于困难,所以他并不承认这是一个漏洞。

在于他的交流中,我不由得有了两个疑问:

  1. 是否CMS的高权限用户理应拥有任意代码执行的权限呢?
  2. 一个漏洞难以利用就不算是漏洞了吗?

开发人员和安全研究人员对于这两个问题,可能有不同的答案吧。

发表评论