漏洞介绍
CVE-2018-7600是一个严重的安全漏洞,影响到Drupal内容管理系统的多个版本。这个漏洞允许未经身份验证的远程攻击者执行任意代码,从而完全控制受影响的网站。Drupal是一个用PHP编写的开源内容管理框架(CMF),广泛应用于各种网站,包括政府、电子零售、企业组织和金融机构等。
漏洞范围
Drupal 版本
< 7.58,
8.x < 8.3.9,
8.4.x <8.4.6
8.5.x < 8.5.1
靶场配置
使用vulhub的靶场
drupal版本为8.5.0
vulhub-master/drupal/CVE-2018-7600
1 | 启动靶场:docker-compose up -d |
漏洞原理
这个漏洞的根本原因出在drupal对表单的渲染上:

可见,在drupal中,我们不需要直接写html表单,而是先创建一个数组,表单呈现引擎通过位于\drupal\core\lib\Drupal\Core\Form\FormBuilder.php文件中的buildForm方法构造出一个名为$form表单,然后成对应的html表单进行呈现。
通过下图buildform的定义,可以看出它是用来构造一个表单的

最终的$form是如下图这个样子:

这个漏洞,恰恰就出在了这里。
但是对于一个drupal框架的应用程序来说,后台表单数组都是开发者写好的,攻击者是无法改变表单数组元素的key值的。
很多应用都提供了如下的一个便利的方法:
比如要注册一个用户,用户名、密码、邮箱、电话,这些东西都填好了。当点击提交的时候,网站告诉你,用户名已存在。
这时候,你会发现,密码、邮箱、电话这些元素不需要你再次填写了,页面已经将保存下来了。
drupal系统同样有这样的功能。
攻击者可以利用这个功能,往表单中输入恶意数据,具体如何实现的呢?下面我们做个试验:
我们先提交个正常的表单
先在buildform函数返回处下断后

core/lib/Drupal/Core/Form/FormBuilder.php#L357
填写表单并提交

页面跳转到注册成功页面,

我们在buildform函数返回处下的断点根本没有断下来。
接着我们再按着上面的表单一模一样的注册一个看看:

但这次呢,在断点处成功断下了:

在这处断点,我们把name的值改为”kingsguard_test_1”试试

这次的返回页面如下:

整个流程是:
- 用户填写表单->表单没有问题->返回注册成功页面
- 用户填写表单->表单内容有问题(例如用户名已被注册)->调用buildform方法,把用户传入的内容一同构造为表单数组->渲染表单数组为html页面返回
这就是刚刚在buildform断点处把name值由kingsguard改为kingsguard_test_1,返回的页面里username值也变成kingsguard_test_1的原因。
到这里,攻击链已经很明确了,攻击者传入的值,可以通过buildform(方法构造表单数组,并且这个表单数组接下来还会被drupal表单呈现引擎解析为html页面。
当我们在这个注册表单页面里,如果想上传一张图片


这时候发送的请求如下

当上传成功后,往往有一个缩略图显示在那,如下图菊花处:

这个缩略图,是通过drupal\core\modules\file\src\Element\ManagedFile.php文件中的uploadAjaxCallback方法来解析。
注意,还记的上文buildform方法吗?buildform生成$form数组后,将生成的$form数组传递给uploadAjaxCallback方法来解析,目的是在返回页面上显示那个缩率的菊花。
既然流程已经捋顺了,我们通过构造poc来动态调试下,发送如下图post包:

首先会进入buildform函数来构造表单数组,接下来这个表单数组($form)会进入uploadAjaxCallback方法。
看下这个uploadAjaxCallback方法:

core/modules/file/src/Element/ManagedFile.php#L179,L182
传入uploadAjaxCallback方法中的$form变量,就是buildform方法生成的表单数组:

$form数组传入uploadAjaxCallback方法中后,可以看到有这么一行(下图红框处):

$form_parents变量竟然可以从get中传入,意味着这个变量可控,其实就是我们poc中的element_parents=account/mail/%23value。
通过poc,此处的$form_parents变量如下图

$form_parents变量和$form通过NestedArray::getValue方法后,结果值赋给$form
新的form变量如下:

接下来看这里的renderRoot方法:

core/modules/file/src/Element/ManagedFile.php#L193
此处传入的$form变量为:

继续看renderRoot方法:
1 | public function renderRoot(&$elements) { |
core/lib/Drupal/Core/Render/Renderer.php#L129-L144
里面调用了render方法
继续看render方法:
1 | public function render(&$elements, $is_root_call = FALSE) { |
core/lib/Drupal/Core/Render/Renderer.php#L182-L202
里面调用了doRender方法
继续看doRender方法:
在这个方法的505行

core/lib/Drupal/Core/Render/Renderer.php#L505
调用call_user_func方法
此处的参数如下:

可见,这里的
$callable=”exec”
$elements[‘#children’]=”kingsguard_text”(这里我们传入的恶意代码)
这一部分利用过程为
在触发uploadAjaxCallback方法后,Drupal遇到#post_render键时,它会将其值(这里设为exec)作为回调函数来执行。这意味着Drupal会尝试执行exec函数。exec函数是PHP中的一个函数,它允许执行外部程序,并可能返回最后一行的输出。攻击者可以通过注入额外的表单值(例如#markup)来提供这些命令参数。例如,攻击者可以设置#markup为id或ls等命令。所以exec成功执行恶意代码。
漏洞利用链
- 用户输入恶意数据: 攻击者在表单中输入恶意数据,如在注册用户名时使用
mail[#post_render][]这样的特殊键。 - 表单验证失败: 当表单验证失败(例如,用户名已存在),Drupal会调用
buildForm方法重新构建表单数组,并保留用户之前输入的数据。 - 恶意数据构造表单数组: 攻击者输入的恶意数据被用来构造表单数组,其中包括特殊键和攻击者控制的值。
- Ajax上传触发漏洞: 利用Drupal的Ajax上传功能(图片上传),攻击者可以触发
uploadAjaxCallback方法,该方法会处理包含恶意数据的表单数组并渲染为HTML页面。 - 特殊键被滥用: 在
uploadAjaxCallback方法中,攻击者控制的表单数组被处理,特殊键如#post_render或#lazy_builder会调用相关联的回调函数(如exec、passthru)。如果攻击者能够控制这些回调函数的参数(被注入的恶意数据),他们就可以执行任意代码。 - 远程代码执行: 通过滥用回调函数(如
exec、passthru),攻击者可以在服务器上执行任意命令,从而完全控制受影响的网站。
漏洞复现
环境启动后,访问http://your-ip:8080/将会看到drupal的安装页面,一路默认配置下一步安装。因为没有mysql环境,所以安装的时候可以选择sqlite数据库。
1 | POST /user/register?element_parents=account/mail/%23value&ajax_form=1&_wrapper_format=drupal_ajax HTTP/1.1 |
更改请求包

响应包为

执行了id命令
https://github.com/a2u/CVE-2018-7600/tree/master
使用该脚本

文件写入成功

参考
https://blog.nsfocus.net/cve-2018-7600-analysis/
https://xz.aliyun.com/t/2271?time__1311=n4%2Bxni0%3DoDqxgDIgxBwraDODBlzMqKG87UYeD