PHP程序为了保护和转储对象 object, 提供了序列化 serialize() 的方法,该方法将一个对象转换成字符串,同时仅保留对象中的成员变量,而不保留函数方法;
将PHP中对象序列化的函数为 serialize()
, 反序列化的函数为 unserialize()
;
Serialize 序列化
什么是序列化呢,我们直接通过一个例子来看一下:
<?php
class Test{
public $a="ThisA";
protected $b="ThisB";
private $c="ThisC";
public function test1(){
return 'this is test1';
}
}
$test = new Test();
var_dump(serialize($test));
?>
将Test类的对象 $test 序列化后就会产生如下的字节流,其主要目的是为了保存PHP中所有的类,方法,变量的类型以及他们所组成的结构:
serialize(mixed $value): string
序列化后的字节流详解
接着我们具体来分析一下这一串字节流代表什么内容:
- string(84) 表示序列化的字符串长度为84;
O
表示这是一个 object 对象,:4
表示对象类型(Test)的长度有4个字符;:"Test"
表示对象类型,:3
表示该对象中有3个成员变量;- 接着通过花括号来包裹成员变量的信息,变量涉及到 变量名 以及 变量的值, 它们都会被序列化,并用
;
来进行分割:s:1:"a"
是序列化变量名的结果s
表示是字符串,1
表示变量的名称(a)只有一个字符;:"a"
表示值为a
;- 其他同理可得;
接下来我们根据一篇古老文章来具体了解一下对象中不同类型的变量序列化后的格式, PHP 序列化(serialize)格式详解:
- a - array
- b - boolean
- d - double
- i - integer
- o - common object
- r - reference
- s - non-escaped binary string
- S - escaped binary string
- C - custom object
- O - class
- N - null
- R - pointer reference
- U - unicode string
NULL的序列化
N;
boolean
b:<digit>;
<digit> = [‘0’, ‘1’], 当boolean型数据为false时,序列化后的成员值为0;
integer
i:<number>;
<number>的取值范围为:-2147483648 到 2147483647;
允许有符号,如果超过范围,就会被序列化为 浮点数 而不是整型,并且反序列化后也无法得到原来的数值;
double
d:<number>;
<number>为浮点数,其范围为PHP中浮点数的范围一样大,并且可以表示为整型、浮点数型以及科学计数法;
如果值为正/负无穷大,那么会被会被表示为INF/-INF,如果超过能表示的最小精度或者非数(NAN)的情况,就会返回0;
string
s:<length>:"<value>";
S:<length>:"<value>";
- <length>是<value>的长度,是非负整数,允许带有正号(+);
- <value>表示字符串值,其范围与 ASCII 相对应,没有转义字符;
s
表示默认的使用方法;S
表示可以在字符串值中加入ASCII转义编码(从而进行绕过),编码的方式为:- 对于ASCII码小于128的字符(不包括 ‘\’),按照单个字节直接写入;
- 对于ASCII码在128~255的字符和 ‘\’ 字符,需要将其ASCII码值转化为16进制编码的字符串,以’\‘作为开头,后两个字节分别为这个字符的16进制编码;
- 这样的方法同样适用于所有的ASCII字符,例如 ‘r’ 就可以用 ’\72’ ,都可以在 ‘S’ 下被解析成功;
array
a:<n>:{<key_1><value_1><key_2><value_2>...<key_n><value_n>}
- <n>表示数组元素的个数;
- <key_1>就表示数组下标;
- <value_1>就表示对应的数组元素的值;
<key_1>下标的类型只能是整型或者字符串类型;
对象的序列化
O:<length>:"<class_name>":<n>:{<field_name>:<field_length>:<field_value>;...;<field_name_n>:<field_length_n>:<field_value_n>}
最后我们来到对象的序列化,就像我们之前见到的那样:
- <length>表示对象的类名<class_name>的字符串长度;
- <n>表示对象中的属性个数,这里不包括
static
&const
声明的静态属性,也就是说只有对象特有的属性才会被序列化; - <filed_name>表示每个属性的名字,用之前提到过的字符串类型进行表示,例如s:1:“a”;
- <field_length>表示属性名的长度;
- 以及对应的<filed_value>值,可以是任何类型的,例如, s:1:“a”;N, field_value是一个NULL;
访问控制修饰符
我们可以观察到后面的变量名和变量值也是一一对应的,但是变量名会根据 Access Modifiers
, 访问控制修饰符的不同而有不同的表示方法:
public(公有)
protected(受保护) // %00*%00属性名
private(私有的) // %00类名%00属性名
- protected属性被序列化的时候属性值会变成 =%00*%00
[属性名]=
- private属性被序列化的时候属性值会变成 =%00
[类名]%00 [属性名]=
虽然%00为空白符,但空字符也有长度,一个空字符长度为 1
就如同我们这里的结果:
- $b 为protected,因此其变量值用 =%00*%00
[属性名]= 的格式;
- %c 为private,其变量用 =%00
[类名]%00 [属性名]= 的格式;
s:4:"*b"; = s:4:"%00*%00b";
s:7:"Testc"; = s:7:"%00Test%00c";
引用 R:r
我们根据一个例子来看看两种引用的区别:
<?php
echo "<pre>";
class SampleClass {
var $value;
}
$a = new SampleClass();
$a->value = $a; # 让$a->value暂时指向$a所指向的地址(copy-on-write原则);
$b = new SampleClass();
$b->value =& $b; # 让$b->value永久指向$b所指向的地址;
echo serialize($a);
echo "\n";
echo serialize($b);
echo "\n";
# 重新赋值
$a->value = 1;
$b->value = 1;
var_dump($a);
var_dump($b);
echo "</pre>";
?>
结果是:
O:11:"SampleClass":1:{s:5:"value";r:1;}
O:11:"SampleClass":1:{s:5:"value";R:1;}
// after re-value
object(SampleClass)
[1]
public 'value' => int 1
int 1
我们可以从源码的对比中看出区别就在于 '&'
特别是在经过重新赋值之后:
- $a->value变成了int(1)
- 而$b整一个对象直接变成了int(1)
原因就是 ‘&’
进行指针引用后,使 $b->value
与 $b
这两个变量在 symbol table 中指向了同一个内存地址,其中任意一个的改变也会直接改变另一个;
引用标识之后的数字
两种引用在序列化后的格式为: r:<number>; R:<number>;
<number>表示的就是,当前这个引用,所引用的对象,在序列化字节流中,是第几个出现的(包括其中复合型成员的子成员);
我们通过一个例子来讲解一下:
<?php
class ClassA {
var $int;
var $str;
var $bool;
var $obj;
var $pr;
}
$a = new ClassA();
$a->int = 1;
$a->str = "Hello";
$a->bool = false;
$a->obj = $a;
$a->pr =& $a->str;
echo serialize($a);
?>
结果是:
O:6:"ClassA":5:{s:3:"int";i:1;s:3:"str";s:5:"Hello";s:4:"bool";b:0;s:3:"obj";r:1;s:2:"pr";R:3;}
我们先来看结果:
- $a->pr序列化后的结果为
'R:3;'
表示引用了第三个出现的成员
那么在这个序列化字节流中,第一个成员是ClassA对象,第二个就是对象中的子成员 $a->int,第三个就是 $a->str
也就是我们这里所进行引用的对象,也就会被标记为 'R:3;'
Unserialize 反序列化
上面我们了解了PHP将一个对象进行序列化的结果,下面我们再来看看将已经序列化的:
<?php
class Test {
public $a = 'This A';
protected $b = 'This B';
private $c = 'This C';
}
$test = new Test();
$sTest = serialize($test);
$usTest = unserialize($sTest);
print_r($usTest);
?>
结果如下:
我们看反序列化后的内容就可以更好的理解为什么要进行序列化,即通过序列化的过程将对象转存为字符串的形式,但是通过反序列化又可以重新转换为对象,就能更好对对象进行保存以及操作。
Magic Methods 魔术方法
很多教程都提到了魔术方法的概念,但没有进行定义和解释,官网的定义总结如下:
魔术方法是PHP定义在类内部的以 __
开头的方法,通过对 魔术方法 的实现可以重写PHP对于对象的默认操作,例如序列化等;
__sleep() and __serialize()
- __sleep()
[: array] ::
-
serialize()方法在执行时,会先检查类中是否有
__sleep()
方法,如果存在,就会先在对object进行序列化之前,执行重写的__sleep()
方法; -
其目的是在对象被序列化之前,对其做最后的调整;
-
同时可以返回一个数组,以此来指定想要序列化的变量;
<?php class Test { public $a = 'This A'; protected $b = 'This B'; private $c = 'This C'; public function test() { return 'This is test() function'; } public function __sleep() { echo "This is __sleep() function;" . "<br>"; $this->b = "This is changed B"; $this->c = "This is changed C"; return array('b', 'c'); } } $test = new Test(); $sTest = serialize($test); printf($sTest); $usTest = unserialize($sTest); ?>
结果如下,我们可以观察到__sleep()所输出的数组,是带上了访问控制修饰符的效果的:
This is __sleep() function;
O:4:"Test":2:{s:4:"*b";s:17:"This is changed B";s:7:"Testc";s:17:"This is changed C";}
- __serialize()
__serialize()
的目的是为了将对象中需要序列化的内容,整合成为键值对的形式返回,便于序列化的进行,但是此键值对数组不会带有访问控制修饰的信息;- serialize()在进行序列化之前,会检查类中是否定义了
__serialize()
方法,如果定义了,就会先执行__serialize()
; - 当同时定义了
__serialize()
以及__sleep()
, PHP会执行__serialize()
而忽略__sleep()
;
<?php class Test { public $a = 'This A'; protected $b = 'This B'; private $c = 'This C'; public function __serialize(): array { $this->b = 'This is changed changed B'; $this->c = 'This is changed changed C'; return [ 'b' => $this->b, 'c' => $this->c ]; } public function __sleep() { echo "This is __sleep() function;" . "<br>"; $this->b = "This is changed B"; $this->c = "This is changed C"; return array('b', 'c'); } } $test = new Test(); $sTest = serialize($test); printf($sTest); ?>
结果如下,可以看到,变量中并没有携带访问控制修饰符的信息,这种形式都通常需要与 __unserialize()
配合使用,我们后面会提到;
This is __serialize() function;
O:4:"Test":2:{s:1:"b";s:25:"This is changed changed B";s:1:"c";s:25:"This is changed changed C";}
__wakeup() and __unserialize()
- __wakeup() :void
-
unserialize()
方法在对字符串进行反序列化之前会检查__wakeup()
方法是否存在,如果存在,就先执行; -
__wakeup()
设计的目的是为了在反序列化之前,预先执行一些准备工作,例如连接数据库;<?php class Connection { protected $link; private $dsn, $username, $password; public function __construct($dsn, $username, $password) { $this->dsn = $dsn; $this->username = $username; $this->password = $password; $this->connect(); } private function connect() { $this->link = new PDO($this->dsn, $this->username, $this->password); } public function __sleep() { return array('dsn', 'username', 'password'); } public function __wakeup() { $this->connect(); } }?>
-
- __unserialize(array $data) : void
-
unserialize()
方法在对字符串进行反序列化之前会检查__unserialize()
方法是否存在,如果存在,就先执行; -
__unserialize()
方法原本设计的目的是通过读取由 序列化的字符串 转化得来的 数组 还原(赋值)对象; -
当同时定义了
__unserialize()
以及__wakeup()
, PHP会执行__unserialize()
而忽略__wakeup()
;<?php class Connection { protected $link; private $dsn, $username, $password; public function __construct($dsn, $username, $password) { $this->dsn = $dsn; $this->username = $username; $this->password = $password; $this->connect(); } private function connect() { $this->link = new PDO($this->dsn, $this->username, $this->password); } public function __serialize(): array { return [ 'dsn' => $this->dsn, 'user' => $this->username, 'pass' => $this->password, ]; } public function __unserialize(array $data): void { $this->dsn = $data ['dsn']; $this->username = $data ['user']; $this->password = $data ['pass']; $this->connect(); } }?>
-
__toString()
__toString()
方法比较好理解,和Java定义类时重写的 toString() 方法类似,就是当对象被当作 String 时就会执行 __toString
来返回特定的String;
比如下面的例子:
<?php
// Declare a simple class
class TestClass
{
public $foo;
public function __construct($foo)
{
$this->foo = $foo;
}
public function __toString()
{
return $this->foo;
}
}
$class = new TestClass('Hello');
echo $class;
?>
结果为:
Hello
__invoke()
__invoke()
是对象在被当作方法来调用时,就会被调用。
<?php
class CallableClass{
public function __invoke($x)
{
echo "</br>" . "Value: " . $x . "</br>";
}
}
$obj = new CallableClass();
$obj(5);
echo is_callable($obj);
上面的代码就将 CallableClass 类的对象 $obj 作为方法,并传递参数。
结果为:
Value: 5
1
__call() and __callStatic()
- public __call(string $name, array $arguments): mixed
- 会在对象调用无法访问到(不存在或者没有访问权限)的方法时被触发调用;
- public static __callStatic(string $name, array $arguments): mixed
- 会在对象对象调用无法访问到(不存在或者没有访问权限)的 静态 方法时被调用;
<?php
class Tmp{
public function __call($name, $arguments)
{
echo "Unaccessible method's name: " . $name . "</br>";
echo "Arguments are: </br>";
print_r($arguments);
}
public static function __callStatic($name, $args)
{
echo "Unaccessible static method's name: " . $name . "</br>";
echo "Arguments are: </br>";
print_r($args);
}
}
$tmp = new Tmp();
$tmp->X(1, 2, 3);
echo "<br>";
$tmp->Y('a', 'b', 'c');
结果如下:
Unaccessible method's name: X
Arguments are:
Array (
[0] => 1
[1] => 2
[2] => 3 )
Unaccessible method's name: Y
Arguments are:
Array (
[0] => a
[1] => b
[2] => c )
__construct() and __destruct()
- __construct(mixed …$values = “”): void
-
__construct()
方法就是PHP中类定义时的构造器方法; -
当一个新对象被构造的时候就会调用该方法;
-
子类的
__construct()
方法不会主动调用父类的构造器,需要主动使用 parent::__construct() 去调用; -
但如果子类没有重写构造器,则会默认继承父类的构造器;
<?php class BaseClass { function __construct() { printf("In BaseClass constructor" . "</br>"); } } class SubClass extends BaseClass { function __construct() { parent::__construct(); print("In SubClass constructor" . "</br>"); } } class OtherSubClass extends BaseClass { } print("BaseClass:</br>"); $obj = new BaseClass(); print("SubClass:</br>"); $obj = new SubClass(); print("OtherSubClass:</br>"); $obj = new OtherSubClass();
结果是:
BaseClass: In BaseClass constructor SubClass: In BaseClass constructor In SubClass constructor OtherSubClass: In BaseClass constructor
-
- __destruct(): void
-
__destruct()
方法会在一个对象不再被任何 symbol table 中的任何变量名所引用时,即 reference count 为0时被调用,来销毁该对象,释放内存空间(具体可以参考个人有关PHP垃圾收集的那篇文章); -
在对象被unset(),以及程序结束或者
exit()
被调用后,都会触发对象内置的__destruct()
方法; -
这里的重点是触发的时机,建议大家debug来看看具体什么时候会触发:
<?php class Tmp { public function __destruct() { echo "__destruct called</br>"; } } echo "|<br>"; $tmp = new Tmp(); echo "|<br>"; $new_tmp = unserialize(serialize($tmp)); echo "|<br>";
-
从结果我们可以看出,直到最后程序结束之后,这两个对象才会触发 __destruct()
销毁释放内存:
|
|
|
__destruct() called
__destruct() called
__set() and __get()
- public __set(string $name, mixed $value): void
- 会在对象想要 写入 无法访问到(不存在或者没有访问权限)的 成员变量 时被触发调用;
- public __get(string $name): mixed
- 会在对象想要 读取 无法访问到(不存在或者没有访问权限)的 成员变量 时被触发调用;
举例说明:
<?php
class Tmp {
private $x = "This is A";
public function __set($name, $value)
{
printf("Set unaccessible variable " . $name . " to " . $value . "</br>");
}
public function __get($name)
{
printf("Get unaccessible variable: " . $name . "</br>");
}
}
$tmp = new Tmp();
$tmp->x = "XXX";
printf($tmp->y);
结果:
Set unaccessible variable x to XXX
Get unaccessible variable: y
__isset() and __unset()
- public __isset(string $name): bool
__isset()
会在程序对于无法访问到(不存在或者没有访问权限)的成员变量调用isset()
或者empty()
时被触发调用;
- public __unset(string $name): void
__unset()
会在程序对于无法访问到(不存在或者没有访问权限)的成员变量调用unset()
时被触发调用;
举例说明:
<?php
class Tmp {
private $x = "This is A";
public function __isset($name)
{
echo $name . " triggers the __isset()</br>";
}
public function __unset($name)
{
echo $name . " triggers the __unset()</br>";
}
}
$tmp = new Tmp();
empty($tmp->a);
unset($tmp->b);
结果:
a triggers the __isset()
b triggers the __unset()
其他魔术方法
值得注意的是,所有的魔术方法,除了 __construct()
, __destruct()
, and __clone()
都需要被申明为public方法,否则会有 E_WARNING;
- public static __set_state(array $properties): object
- 这个静态的魔术方法会在类被
var_export()
方法导出时被调用.
- 这个静态的魔术方法会在类被
- public __clone(): void
- 当
clone()
方法调用结束,对象的克隆也结束之后,如果新克隆的对象中有定义__clone()
的话,就会被触发调用,执行对应的方法;
- 当
- public debugInfo(): array
- 当对象被
var_dump()
方法要求获取其中的内容时被调用,可以自定义想要返回的内容; - 如果没有定义,那么默认返回所有public, protected and private的属性;
- 当对象被
POP 面向属性编程
Property Oriented Programming,POP 面向属性编程,这个概念根据我的调查,应该不是类似于面向对象编程(e.g., JAVA),面向过程编程(C)这种传统意义上的编程,而是安全领域独有的一种概念。
这也导致了我一开始很难理解这个概念,因为看不同的文章用中英文写出来的理解总有一些差别;
因此我们这里也先不着急给POP Chain下定义,先来看一个很简单的例子,也许之后大家就能够理解我对于POP的总结与定义:
<?php
class Example
{
private $obj;
function __construct()
{
// some PHP code...
}
function __wakeup()
{
if (isset($this->obj)) return $this->obj->evaluate();
}
}
class CodeSnippet
{
private $code;
function evaluate()
{
if (isset($this->code)) {
eval($this->code);
}
}
}
// some PHP code...
if (isset($_POST
['data'])) {
$user_data = unserialize($_POST
['data']);
}
上面的代码定义了两个类,Example以及CodeSnippet;
其中Example中首先定义了一个私有的 属性 $obj
;
- 同时实现了
__wakeup()
魔术方法,回忆一下,其会在反序列化(unserialize())的时候被调用,然后执行$obj
这个 属性 变量中的方法evaluate()
;
CodeSnippet中也有一个私有 属性: $code
:
- 实现了一个方法
evaluate()
其可以执行$code
中的PHP代码; - 然后该PHP脚本会对POST的变量
data
进行反序列化;
隐隐约约大家应该也能感受到些什么,反序列化会触发 __wakeup()
, 接着触发 evaluate()
?
我们接着来看我们到底可以进行怎样的利用:
<?php
class CodeSnippet
{
private $code = "phpinfo();";
}
class Example
{
private $obj;
function __construct()
{
$this->obj = new CodeSnippet();
}
}
print urlencode(serialize(new Example()));
我们可以构造以上的PHP脚本,目的是为了输出一个序列化后的字符串:
- 首先定义了一个 codeSnippet 类,并且其中有一个私有属性
$code
其中包含了想要执行的php代码; - 接着定义了一个 Example 类,其中有一个私有属性
$obj
, 同时实现了魔术方法__construct()
, 其中构造了一个新的 CodeSnippet 对象,并赋值给$obj
- 最后新建一个Example类并进行序列化后进行url编码后输出;
O%3A7%3A%22Example%22%3A1%3A%7Bs%3A12%3A%22%00Example%00obj%22%3BO%3A11%3A%22CodeSnippet%22%3A1%3A%7Bs%3A17%3A%22%00CodeSnippet%00code%22%3Bs%3A10%3A%22phpinfo%28%29%3B%22%3B%7D%7D
之后将序列化的字符串作为POST变量提交到服务端脚本,就可以实现RCE
这里值得注意的几件事是:
- PHP中变量的调用记得用
isset()
先做判断否则会出现如下warning:
- 同时要注意使用浏览器工具是否如你所想,大家可以看到我用了很多hackbar类似的插件,他们总会出现一些问题,比如对于url的过度解析,或者是加入一下不必要的参数,因此出现奇怪的问题的时候,可以用Burp抓包来看一看,然后最稳健的方法还是Burp来进行HTTP请求,最直观。
至此,我们可以来总结一下这个例子并给出POP的定义:
- 我们在能够获取服务端脚本源码的情况下,通过对于反序列化函数
unserialize()
会调用__wakeup()
魔术方法的特性; - 在本地构造 Example 以及 CodeSnippet 类;
- 为其中的
属性
$code 进行赋值; - 接着通过
__construct()
方法来新建 CodeSnippet 对象($this->obj); - 接着序列化后作为一个对象(有属性的值的)发送给服务端脚本;
- 最后服务端会在反序列化的时候调用
__wakepup()
, 并利用我们已经定义好了属性值进行执行,导致RCE的发生;
在POP面向属性编程中,我们的终极目的是调用 敏感函数, 来造成漏洞的利用(e.g., RCE),其中:
- 敏感函数可以是包含在魔术方法中,在满足触发条件之后,被一起调用;
- 也可以是魔术方法中包含另一个类的实例对象调用其本身的属性方法,这个属性方法中包含了 敏感函数:
- 魔术方法被触发后,调用另一个类的实例对象中的方法,使 敏感函数 被一起调用;
POP chain,其实就是套娃,一个对象的方法调用包含另一个对象的方法调用,一个(魔术)方法触发另一个(魔术)方法,形成一条调用链;
同时这些对象都是类中的 属性, 这些属性都是攻击者可以在序列化后的字符串中所控制的;
因此,POP面向属性编程作为一种编程思想,是为了实现一个功能,也就是调用敏感函数,那么实现的方法就是控制对象中的属性,来不断地调用属性中的方法。
常见的敏感函数:
- 命令执行:exec()、passthru()、popen()、system()
- 文件操作:file_put_contents()、file_get_contents()、unlink()
在上面的例子中:
unserialize() -> __wakeup() -> evaluate() -> eval()
就是一条POP链,我们就是要找到这样一条函数调用链,最终执行输入的恶意代码;
以上就是全部的基础知识的信息,之后我们就可以通过具体的题目来进行进一步的巩固和拓展(应该会新开一篇文章来记录);
常用技巧
绕过魔术方法
__wakeup()
在反序列化字符串时,如果一个对象申明的属性个数大于实际个数时,就会自动跳过 __wakeup() 的执行
(此方法只适用于PHP5 < 5.6.25, PHP7 < 7.0.10) 例如
O:4:“Name”:2:{s:14:“�Name�username”;s:5:“admin”;s:14:“�Name�password”;i:100;}
👇
O:4:“Name”:3:{s:14:“�Name�username”;s:5:“admin”;s:14:“�Name�password”;i:100;}
这个情况下就会绕过 __wakeup() 执行
配合PHP伪协议
使用 glob://
伪协议来查找特定的文件路径(同时可以配合测信道攻击使用)
序列化成员绕过
字符串绕过
通过将字符串标识符 s
修改成 S
便可以解释十六进制字符,进而将被过滤的字符串使用ASCII替换,从而进行绕过
引用绕过
例如不允许序列化字节流出现 'R:2'
:
修改变量的申明的顺序,即可修改R:2后面的数字
修改为不同的引用方式,可以在R/r中切换
属性赋值覆盖绕过
可以通过浅copy来为受限的属性赋值
例如以下代码,就将 $a->user
作为引用传递给 $a->name
从而保持两个值的一致,即使 $a->name
被修改或者过滤,但是通过 $a->user
依然可以对其进行修改
$a->name = &$a->user;
Reference
[1] ‘PHP: Magic Methods - Manual’. https://www.php.net/manual/en/language.oop5.magic.php#object.serialize.
[2] ‘PHP: Constructors and Destructors - Manual’. https://www.php.net/manual/en/language.oop5.decon.php#object.construct.
[3] ‘PHP: Magic Methods - Manual’. https://www.php.net/manual/en/language.oop5.magic.php#object.set-state.
[4] ‘PHP: Magic Methods - Manual’. https://www.php.net/manual/en/language.oop5.magic.php#object.debuginfo.
[5] ‘PHP: Object Cloning - Manual’. https://www.php.net/manual/en/language.oop5.cloning.php#object.clone.
[6] ‘__serialize和__unserialize魔术方法详解_yink12138的博客-CSDN博客’. https://blog.csdn.net/yink12138/article/details/122318003.
[7] ‘PHP: serialize - Manual’. https://www.php.net/manual/en/function.serialize.php.
[8] ‘PHP: Magic Methods - Manual’. https://www.php.net/manual/en/language.oop5.magic.php#object.sleep.
[9] ‘PHP序列化反序列化漏洞总结(一篇懂)_WHOAMIAnony的博客-CSDN博客_反序列化漏洞php’. https://blog.csdn.net/qq_45521281/article/details/105891381.
[10] ‘PHP反序列化漏洞简介及相关技巧小结 - FreeBuf网络安全行业门户’. https://www.freebuf.com/articles/web/209975.html.
[11] shavchen, ‘PHP反序列化漏洞说明’, Weixin Official Accounts Platform. http://mp.weixin.qq.com/s?__biz=MzU1NjgzOTAyMg==&mid=2247485657&idx=1&sn=eb36b032da129a99860c736ad7d6ac11&chksm=fc3fb1d8cb4838ce017e2b095ac2747113b7b00a9c2fdca2ae4938e0ccb9a0a80ce1783eda61#rd.
[12] ‘oop - Php Destruct Called Twice - Stack Overflow’. https://stackoverflow.com/questions/5201342/php-destruct-called-twice.
[13] ‘php反序列化与POP链 | V0W’s Blog’. https://v0w.top/2020/03/05/unsearise-POP/#0x01-%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%9F%BA%E6%9C%AC%E7%9F%A5%E8%AF%86.
[14] Li F., ‘PHP序列化中的R与r’, Frank, Apr. 10, 2021. https://blog.frankli.site/2021/04/11/Security/php-src/PHP-Serialize-tips/index.html.
[15] ‘PHP Object Injection Exploitation Notes’. https://snoopysecurity.github.io/web-application-security/2021/01/08/02_php_object_injection_exploitation-notes.html.
[16] V. Li, ‘Diving into unserialize(): POP Chains’, Medium, Sep. 29, 2019. https://vickieli.medium.com/diving-into-unserialize-pop-chains-35bc1141b69a.
[17] ‘php反序列化利用——POP链构造实例’, 简书. http://maleskine-production:30000/p/16c56bebc63d.
[18] ‘Answer to “‘Notice: Undefined variable’, ‘Notice: Undefined index’, ‘Warning: Undefined array key’, and ‘Notice: Undefined offset’ using PHP”’, Stack Overflow, Nov. 23, 2010. https://stackoverflow.com/a/4261200/17534765.
[19] ‘Laravel POP链简析’, Math & Sec ,HACHp1的个人博客. https://hachp1.github.io/posts/Web%E5%AE%89%E5%85%A8/20190906-laravel_pop1.html.