【漏洞分析】CVE-2018-7600 Drupal 7.x 版本代码执行

CVE-2018-7600影响范围包括了Drupal 6.x,7.x,8.x版本,前几天8.x版本的PoC出来之后大家都赶紧分析了一波,然后热度似乎慢慢退去了。两天前Drupalgeddon2项目更新了7.x版本的exp,实际环境也出现了利用,下面就简单来看一下

0x01 概述

CVE-2018-7600影响范围包括了Drupal 6.x,7.x,8.x版本,前几天8.x版本的PoC出来之后大家都赶紧分析了一波,然后热度似乎慢慢退去了。两天前Drupalgeddon2项目更新了7.x版本的exp,实际环境也出现了利用,下面就简单来看一下

看到项目上这样写

Drupal < 7.58 ~ user/password URL, attacking triggering_element_name form & #post_render parameter, using PHP’s passthru function

提示了问题出在user/password路径下,通过#post_render传递恶意参数,问题出现在triggering_element_name表单处理下

0x02 漏洞分析

我们从三个问题入手,为什么PoC发了两个包,第二次请求为什么要带上一个form_build_id,以及为什么选择user/password这个入口

先分析第一个post,照例还是先看一下Drupal 7的表单处理流程,跟8版本不太一样,但是入口还是相似的。
根据文档描述,当我们提交一个表单(例如找回密码)时,系统会通过form_builder()方法创建一个form

一系列预处理后,会由drupal_build_form
()
方法创建一个表单,在第386行调用drupal_process_form()方法,
跟进drupal_process_form()方法,这时候默认的$form_state['submitted']为false


不满足if条件,$form_state['submitted']被设置为true


于是进入这个分支,最终被drupal_redirect_form重定向

我们的目的是要让系统缓存一个form_build_id,以便后面拿出来用。要想form被缓存,就得想办法让if ($form_state['submitted'] && !form_get_errors() && !$form_state['rebuild'])不成立,也就是说要使$form_state['submitted']为false
从而进入下面的drupal_rebuild_form

那么如何让$form_state['submitted']为false呢?

includes/form.inc第886行
$form = form_builder($form_id, $form, $form_state);
跟进form_builder方法,第1987行

$form_state['triggering_element']['#executes_submit_callback']存在值的时候就为true,那么我们就想办法让这个值为空
往上看第1972行

如果没有设置$form_state['triggering_element'],那么$form_state['triggering_element']就设置为第一个button的值,所以正常传递表单的时候$form_state['triggering_element']['#executes_submit_callback']就总会有值

现在问题来了,如何构造一个form能够确保$form_state['triggering_element']['#executes_submit_callback']为空或者说不存在这个数组呢?

我们注意到第1864行

_form_builder_handle_input_element()方法对表单先进行了处理,跟进去看一下

第2144行

这里$form_state['triggering_element']被设置为$element,前提是满足_form_element_triggered_scripted_submission()方法,继续跟入
第2180行

这个方法的意思是说如果_triggering_element_value$element的键值都相等的话,返回true
$form_state['triggering_element']赋值为$element,其中不含['#executes_submit_callback'],一开始的条件就成立了

根据PoC,我们传入_triggering_element_name=name


看到进入这个分支,进入form_set_cache()方法



数据库中插入缓存form_build_id


成功写入缓存

接下去来看一下这个缓存有什么用

分析PoC的第二个包,请求参数是这样q=file/ajax/name/%23value/form_build_id
form_build_id即我们上一个写入数据库的缓存表单

首先请求会进入includes/menu.incmenu_get_item()方法,

$path即我们传进去的q参数,经过一系列处理传给menu_get_ancestors()方法,该方法把path重新组合成一堆router,也就是Drupal处理路由到具体url的传参方式,最终被db_query_range()带入数据库查询
我们关注查询结果$router_itempage_callback值,因为这个值最终会作为参数被带入call_user_func_array()


到这里就跟8版本的情况有点类似了

跟入回调函数file_ajax_upload()


还是一样,把$form_parents完整取出赋值给$form,加上一些前缀后缀后最终进入drupal_render()方法

最终得到执行

到目前为止我们分析清楚了为什么PoC要发两次包,以及第二次请求为什么要带上一个form_build_id,现在来想一想为什么要请求user/password这个路径呢?
在user这个module下的user_pass()方法

看到这里是不是感觉跟8版本很相似,#default_value从get的name参数里取值,而name可以作为数组传入,它的属性在下面正好可以被利用,一个巧妙的利用链就串起来了。

0x03 总结

Drupal 7.x的利用比8.x要复杂一些,但触发点和一开始的风险因素还是类似的,一是接收参数过滤不当,而是可控参数进入危险方法。官方补丁把入口处的#全给过滤了,简单粗暴又有效,估计再利用框架本身的特性想传递进一些数组或元素就很难了。

0x04 参考

  • https://github.com/dreadlocked/Drupalgeddon2
  • https://research.checkpoint.com/uncovering-drupalgeddon-2/

发表评论