漏洞介绍

Drupal是一个使用PHP编写的免费开源的Web内容管理框架。

在Drupal 7.x和8.x版本中存在远程代码执行漏洞。该漏洞是由于输入过滤不充分,通过对URL中的’#’字符进行两次URL编码可以绕过sanitize()函数过滤,从而导致远程代码执行。

这个漏洞是CVE-2018-7600的绕过利用,两个漏洞原理是一样的,攻击者可以通过不同方式利用该漏洞远程执行代码,只是入口方式不一样。所以,一旦参数可控并且没有经过正确的过滤,就很有可能出问题。

漏洞范围

7 .0<=Drupal <7.59

8.0.0<=Drupal <8.4.8

8.5.0<=Drupal <8.5.3

漏洞靶场

使用vulhub的靶场:

vulhub-master/drupal/CVE-2018-7602

这里的靶场环境为Drupal 7.57

启动靶场环境:

1
docker-compose up -d

服务启动后,访问http://your-ip:8081即可查看到Drupal的安装页面。安装过程中可以选择默认设置,使用SQLite数据库进行安装。安装时需要记住创建的账户信息,因为需要用它来利用漏洞。

漏洞原理

触发漏洞需要发两个post包,一个存入form_build_id一个取出后执行。

问题出在删除文章的时候,因此需要文章删除权限,我们先走一遍正常删除文章的逻辑

img
请求中每个node即代表一篇文章。
可以看到是会重定向到文章页面的,根据上个漏洞的分析我们猜测,一定还是走到了drupal_redirect_form(),我们已经知道如果走到drupal_redirect_form()分支,是不会往数据库缓存form_build_id的,我们的目的还是让程序不满足一定条件从而不进行表单提交后重定向,所以还是跟着CVE-2018-7600的套路来走

从代码层面看一下

之前的流程还是一样,直接跳到drupal_build_form()方法第386行

[drupal_build_form()] includes/form.inc#L314-L397

1
drupal_process_form($form_id, $form, $form_state);

[drupal_process_form()] includes/form.inc#L386

跟入drupal_process_form()

img
还是一样,$form_state['submitted']被设置为true

$form_state[‘submitted’] includes/form.inc#L1988

回到902行

1
2
if ($form_state['submitted'] && !form_get_errors() && !$form_state['rebuild'])`
条件被满足,进入这个分支便会执行`drupal_redirect_form()

img

includes/form.inc#L902

而在这一步之前需要经过的判断是_form_element_triggered_scripted_submission()
所以回到一开始的问题,构造一个_triggering_element_value使得键值对相等,从而不进行rebuild

我们传入_triggering_element_name=form_id

img
img

includes/form.inc#L2180-L2187

可以看到条件被满足,$form_state['submitted']没有被设置为true,还是保持默认值false

img

includes/form.inc#L1987

img

includes/form.inc#L902

进入drupal_rebuild_form()

img

includes/form.inc#L974

表单被缓存

img

includes/form.inc#L502-L503

img

然后我们发送第二个post包来取出我们构造好的form,向**file/ajax/actions/cancel/%23options/path**发起请求

img

参数传递进去

img

modules/file/file.module#L238-L250

最终还是跟入到
$output = drupal_render($form);

includes/common.inc#L5997-L6107

根据前几次的经验,我们还是选择'#post_render'参数,

img

includes/common.inc#L6077-L6083

假如我们能控制这个参数,在drupal_render()方法里就会把这个参数作为$function函数名,而传给它的参数则是[%23markup]

所以问题回到了一开始,我们需要传递什么样的恶意参数,可以让系统直接接收而不经过过滤,还是之前的套路,搜索module下删除文章的相关操作

img

modules/node/node.pages.inc#L338-L346

可以看到node_form_delete_submit()方法从get方法直接接收参数destination,与最初分析正常删除文章的参数正是同一个,那么我们就可以利用destination传进恶意参数

构造如下
destination=a?q[%2523post_render][]=passthru%26q[%23type]=markup%26q[%23markup]=dir

a参数是次要的,主要是q参数,因为在includes/common.incdrupal_parse_url()方法

includes/common.inc#L575-L621

1
2
3
4
if (isset($options['query']['q'])) {
$options['path'] = $options['query']['q'];
unset($options['query']['q']);
}

includes/common.inc#L615-L618

从q取出值赋给$options['path'],也就是a被覆盖了,这个时候的$options['path']就是我们传入的数组

注意q的元素需要转义百分号,对#进行二次编码,以绕过CVE-2018-7600的补丁,不然在取值时会被认为q[是一个值

img

includes/common.inc#L615-L618

参数缓存进整个form后通过第二个请求取出,同样经过

1
2
3
foreach ($form_parents as $parent) {
$form = $form[$parent];
}

modules/file/file.module#L271-L273

遍历叶子节点取出参数

img

modules/file/file.module#L271-L278

进入drupal_render()执行

img

includes/common.inc#L6080

漏洞复现

使用来自pimps/CVE-2018-7600的PoC,执行以下命令来利用漏洞:

1
2
3
# 将"id"替换为你想执行的命令
# 第一个"drupal"是用户名,第二个"drupal"是密码
python3 drupa7-CVE-2018-7602.py -c "id" drupal drupal http://127.0.0.1:8081/

如下图所示,命令成功执行:

image-20250507190917700

参考

https://github.com/vulhub/vulhub/blob/master/drupal/CVE-2018-7602/README.zh-cn.md

【漏洞分析】Drupal内核远程代码执行漏洞CVE-2018-7602 – 绿盟科技技术博客 (nsfocus.net)