DRUPAL 8.x远程代码执行漏洞(CVE-2018-7600)
事件背景
框架漏洞收集
CVE-2018-7600有两个POC分别是7和8的,本文仅研究8版本的POC,与其它的文章不同的事,本文我将数据流向调试并记录下来了
漏洞说明
1. 漏洞原理:Drupal对表单请求内容未做严格过滤,因此,这使得攻击者可能将恶意注入表单内容,此漏洞允许未经身份验证的攻击者在默认或常见的Drupal安装上执行远程代码执行。
2. 组件描述:Drupal是使用PHP语言编写的开源内容管理框架(CMF),它由由内容管理系统和PHP开发框架共同构成,在GPL2.0及更新协议下发布。连续多年荣获全球最佳CMS大奖,是基于PHP语言最著名的WEB应用程序。
3. 影响版本: 7.x-8.x,本篇分析漏洞仅对版本8有效,版本7的是另外的利用点,但是CVE编号相同
漏洞复现
环境搭建:GitHub - drupal/drupal at 8.3.3
这里可能需要利用docker环境的命令配置一下,我自己的环境为:windows10、PHP7.1、电脑带有docker可使用composer命令生成vendor目录、需要提供yaml环境(https://www.cnblogs.com/yycode/p/16039854.html)、在php.ini文件中添加”yaml.decode_php=On“
然后访问/core/install.php进入安装界面,简单填数据库的信息安装即可
直接用payload
url : ?element_parents=account/mail/%23value&ajax_form=1
POST : form_id=user_register_form&mail[0][#lazy_builder][0]=passthru&mail[0][#lazy_builder][1][0]=whoami
命令执行成功
漏洞分析
漏洞的原因来源于框架的一个特点,即drupal采用引擎对数组渲染生成HTML表达
这里出现了两个问题,而这两个问题则就造成了漏洞的利用,一个是框架对数组数据的传入没有任何过滤,二是生成的表单最后会传入到drupal\core\modules\file\src\Element\ManagedFile.php的uploadAjaxCallback方法中解析,而在解析的过程中有几个框架自带的属性可以造成漏洞的利用
- #access_callback
由Drupal使用来确定当前用户是否有权访问元素。 - #pre_render
在渲染之前操作渲染数组。 - #post_render
接收渲染过程的结果并在其周围添加包装。 - #lazy_builder
用于在渲染过程的最后添加元素。
#access_callback 标签虽然callback回调函数可控,但需要回调处理的字符串不可控,导致无法利用
由于对数组无任何过滤输入,即可传入这些自带的属性,造成回调函数的利用,不过有个问题就是数据怎么传入,如何触发漏洞,从头开始分析,构造的HTML表单的key值一般都是前期写死的,而我们经过分析必须得向表单传入可控键才能触发漏洞,这个问题的解决方法就是一个很正常的功能造成的
打个断点监听一下,运行以后如下
说明在buildform打的断点没有停下来,不过如果再以上述步骤一模一样操作一遍会发现
可以看到在断点处停下来了,第二次请求就停下来了,说明用户存在的话断点会走到最后,将表单返回
这就是一个一般网站常用的便捷功能,当注册的用户或邮箱存在时会保留上次输入的信息,而保留的这部分数据可以导致了数据的输入,这些数据最后会流入到drupal\core\modules\file\src\Element\ManagedFile.php::uploadAjaxCallback中解析,而现在还是没有解决,如何才能修改表单的键,根据其它师傅的分析才明白可以利用注册中图片的一个传输获得payload的大概身影,尝试抓取直接上传图片的请求,可以得到如下
由于格式是multipart/form-data,不太方便,再查看缓存的POST
利用burp抓到的url以及缓存的POST,提取几个关键的参数伪造为application/x-www-form-urlencoded类型的请求即可,经过分析大概提取了如下几个关键参数
url中element_parents参数的内容是定位到表单的一个位置最后提取其中的数据,比如上传图片时为user_picture/widget/0,则会将$form表单下的user_picture下的widget下的0提取出来无论为数组还是元素,代码实现如下
经过测试GET参数还有ajax_form为必须,不是很清楚具体作用,根据参数名和回显数据猜测应该是保存的表单
POST提取出了三处,分别为form_id、mail、name,其中form_id是必须定位到注册接口的,mail和name都可以传入数据,但是经过测试发现name有过滤,不接受数组的传输,而mail无任何过滤,因此采用mail传值
接下来从drupal\core\modules\file\src\Element\ManagedFile.php::uploadAjaxCallback开始审计
explode()函数会把element_parents内容以斜杆划分为几个值的数组,接下来步入getValue
大概作用为递归查询$form中是否有$form_parents,有的话最后把值返回,没有则返回空,因此最后$form为我们构造恶意的键对应的值,往下继续看
根据上述可以大概了解$form现在有三个键,一个是我们传入的,一个是#suffix,一个是#prefix,最后$form被传入$renderer->renderRoot(),跟进查看
继续跟进render()查看
就和套娃一样,继续跟进doRender()
由于没有#access和#access_callback因此如上条件判断皆可跳过,往下走
这里又添加了一个键值,下面的条件判断依旧可以跳过
这里即是最开始说的几个框架自带属性功能#lazy_builder,这也是我构造payload一个方法,因此如果利用的是#lazy_builder攻击,则进入条件判断查看,根据如上大概需要构造的payload有如下几个要求
#lazy_builder对应的值为数组
#lazy_builder对应的数组有且仅仅包含两个键
#lazy_builder下键为1对应的值为数组,测试如下
继续看下面
array_diff()函数用于比较两个(或更多个)数组的值,并返回差集,差集仅仅限于前减后
根据上例,我们必须保证$elements的键值小于等于$supported_keys且不能比在里面的数据多些什么,但是根据前面的数据我们很显然无法达到要求,其中#suffix、#prefix、还有后来添加的#cache,因此此处无法绕过,会导致报错,那么真的就没有办法了吗?确实,在这一层没有办法了,那我们就不能进入#lazy_builder这层条件了,这说明了#lazy_builder不可用吗?其实并不是,跳过这个条件继续看下去
其中经过数据处理$children为一个0数组 $elements['#children']为空$theme_is_implemented为false
已知$children为0数组,则提出来的值为0,因此就相当于将$elements中0键对应的值重新循环了一遍,而这次循环不再带有#suffix、#prefix等其它的键,那我们是不是可以将原本恶意的数据封装在0数组里,然后就可以绕过之前#lazy_builder条件里的一个判断了,那么解决了之前的那个困惑,后面直接看到关键条件
根据分析两个参数都可控的条件下利用回调函数call_user_func_array()最后实现命令注入,payload的构造为
URL : ?element_parents=account/mail/%23value&ajax_form=1
POST : form_id=user_register_form&mail[0][#lazy_builder][0]=passthru&mail[0][#lazy_builder][1][0]=whoami
当然,这也仅仅是利用到一个#lazy_builder属性造成的漏洞,另外有一个#pre_render
但是$elements数据不能完全控制,导致这个利用点很鸡肋,只能用需要一个参数且对参数要求不严格的函数,如print_r、var_dump等
除此之外还有一个看着像是可以利用的,为#post_render
回调函数会把#post_render的值作为函数名,后面两个则作为参数
最后$elements['#children']取决于$elements['#markup'],因此控制$elements['#markup']的值即可
payload为
虽然未成功,但是经过测试发现call_user_func()函数一个问题,也就是两个参数时可能会出现的问题,测试一下便明白
不知道为什么,在本地测试时发现以变量传入就是会报错,而我尝试在在线平台测试时发现
可以看到在线平台则可以,这说明这个函数是有缺陷的,我大胆猜测可能是php版本的原因,于是在本地测了一下和在线一样版本7.4发现
报了500还是不行,猜测可能是配置原因或者是报错的一个设置,但是找不出来,不过#lazy_builder可以实现即可。