使用的是探姬制作的反序列化靶场 PHPSerialize-labs
直接下载ZIP后用中间件指向目录即可,小皮也可以
Level 1: 类的实例化
打开题目,我们看到定义了一个类 FLAG,在该类中还使用了 __construct()
方法,在创建对象时会自动调用
使用
new xxx();
来实例化一个对象

得到flag

Level 2: 对象中值的传递
观察本题源代码:
<?php
/*
--- HelloCTF - 反序列化靶场 关卡 2 : 类值的传递 ---
HINT:尝试将flag传递出来~
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-07-01 20:30
# @Repo: github.com/ProbiusOfficial/PHPSerialize-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com
*/
error_reporting(0);
$flag_string = "HelloCTF{????}";
class FLAG{
public $free_flag = "???";
function get_free_flag(){
echo $this->free_flag;
}
}
$target = new FLAG();
$code = $_POST['code'];
if(isset($code)){
eval($code);
$target->get_free_flag();
}
else{
highlight_file('source');
}
Now Flag is ???
我们可以发现,flag值应该就在flag_string变量中,那么我们可以有多种方法获取到其值
方法一:直接输出
我们发现flag_string变量是一个全局变量,他是定义在类之外的。那么直接输出其值
code=echo $flag_string; exit;

方法二:利用变量传递
我们观察代码,发现下方会调用get_free_flag()函数,该函数会输出free_flag的值,那么我们修改 $target->free_flag
使其等于 $flag_string
->
用于访问 对象的属性(变量)或方法(函数)。它的作用类似于其他语言中的.
(如 JavaScript、Python)
code=$target->free_flag = $flag_string;
然后就可以直接输出flag了

方法三:暴力输出所有变量
既然flag存储在变量中,那么我们直接输出所有变量再观察flag即可。
code=var_dump(get_defined_vars());

然后就看到flag
level 3:对象中值的权限
方法一:暴力获取所有变量
源代码:
<?php
/*
--- HelloCTF - 反序列化靶场 关卡 3 : 对象中值的权限 ---
HINT:尝试将flag传递出来~
# -*- coding: utf-8 -*-
# @Author: 探姬
# @Date: 2024-07-01 20:30
# @Repo: github.com/ProbiusOfficial/PHPSerialize-labs
# @email: admin@hello-ctf.com
# @link: hello-ctf.com
*/
class FLAG{
public $public_flag = "HelloCTF{?";
protected $protected_flag = "?";
private $private_flag = "?}";
function get_protected_flag(){
return $this->protected_flag;
}
function get_private_flag(){
return $this->private_flag;
}
}
class SubFLAG extends FLAG{
function show_protected_flag(){
return $this->protected_flag;
}
function show_private_flag(){
return $this->private_flag;
}
}
$target = new FLAG();
$sub_target = new SubFLAG();
$code = $_POST['code'];
if(isset($code)){
eval($code);
} else {
highlight_file(__FILE__);
echo "Trying to get FLAG...<br>";
echo "Public Flag: ".$target->public_flag."<br>";
echo "Protected Flag:".$target->protected_flag ."<br>";
echo "Private Flag:".$target->private_flag ."<br>";
}
?>
Trying to get FLAG...
Public Flag: HelloCTF{se3_me_
Protected Flag: Error: Cannot access protected property FLAG:: in ?
Private Flag: Error: Cannot access private property FLAG:: in ?
...Wait,where is the flag?
还是同上一题一样,所有的值存储在变量中,同时观察代码容易发现,flag是被分为了三部分,第一部分为HelloCTF{ 开头,第二部分为不带大括号的中间内容,第三部分是以反大括号结尾的。
code=var_dump(get_defined_vars());

方法二:逐个输出变量
本题中,protected_flag
和private_flag
两个变量不是全局变量,是在FLAG类中的私有变量,也就是说在该类中才可调用,我们观察到在代码中有创建对象的操作,即 $target=new FLAG();
,那么我们直接输出这个对象中的值即可
code=echo $target->public_flag,$target->get_protected_flag(),$target->get_private_flag();exit;

获取类中的变量这个和python也是一样的,如我也创建一个FLAG类
class FLAG:
def __init__(self):
self.public_flag = "flag{"
self._protected_flag = "哈哈哈"
self.__private_flag = "终于要放假了}"
target = FLAG()
try:
print(public_flag)
print(protected_flag)
print(private_flag)
except:
print("你TM都不知道自己在干啥")
print(target.public_flag+target._protected_flag+target._FLAG__private_flag)
在类中同样切分三部分,不同的只是,在python中,类之内变量默认为public, _ 表示protected, __代表private,我们实例化对象之后,才可以调用它的 protected 和 private 变量。

可以看到,上面直接调变量名的都无法访问,下方加上实例化的变量名,同时private变量还要加上.类名
才能正常访问
level 4:序列化初体验
首先查看源码
class FLAG3{
private $flag3_object_array = array("?","?");
}
class FLAG{
private $flag1_string = "?";
private $flag2_number = '?';
private $flag3_object;
function __construct() {
$this->flag3_object = new FLAG3();
}
}
$flag_is_here = new FLAG();
$code = $_POST['code'];
if(isset($code)){
eval($code);
} else {
highlight_file(__FILE__);
}
观察代码,发现流程是:
- 定义了两个类:
FLAG3
类:包含一个私有属性$flag3_object_array
,是一个数组,初始值为两个问号FLAG
类:包含三个私有属性:
$flag1_string
:字符串,初始为问号$flag2_number
:数字(但用字符串表示),初始为问号$flag3_object
:一个FLAG3类的对象- 程序创建了一个
FLAG
类的实例$flag_is_here
- 然后检查是否有POST参数
code
:
- 如果有,就执行
eval($code)
- 如果没有,就高亮显示当前文件内容
方法一:暴力输出变量
依旧是直接暴力输出变量的值,使用:
code=var_dump($flag_is_here);
输出实例化之后的对象中所有的值

可以看见flag1_string
变量的值应该就是flag
方法二:使用ReflectionClass
ReflectionClass
是 PHP 的反射类,用于在运行时动态获取和操作类的信息(如属性、方法、接口等),无需实例化即可分析类结构。核心功能:
- 获取类名、命名空间、父类、接口等元信息。
- 检查类特性(是否抽象、接口、Trait)。
- 访问私有/受保护属性和方法(通过
setAccessible(true)
)。- 动态实例化对象(即使构造函数私有)。
典型用途:依赖注入、单元测试、代码分析工具。
示例:
$class = new ReflectionClass('User');
$props = $class->getProperties(); // 获取所有属性
$methods = $class->getMethods(); // 获取所有方法
最终Payload:
code=$ref=new ReflectionClass($flag_is_here);$prop=$ref->getProperty("flag1_string");$prop->setAccessible(true);echo $prop->getValue($flag_is_here);

主要是这几步:
1、创建反射类对象
$ref = new ReflectionClass($flag_is_here);
- 获取
$flag_is_here
的类信息。
2、获取目标属性
$prop = $ref->getProperty("flag1_string");
- 找到类中的
flag1_string
属性(可能是private
或protected
)。
绕过访问限制
$prop->setAccessible(true);
- 允许访问私有/受保护的属性。
获取并输出属性值
echo $prop->getValue($flag_is_here);
- 读取
$flag_is_here
对象的flag1_string
值并打印。
方法三:序列化
PHP在序列化的时候,会把protected和private属性的变量也同时序列化,只是为了区分不同作用域的属性,PHP 会在私有属性名前添加:
-
private
属性 →\x00类名\x00属性名
(如FLAGflag1_string
) -
protected
属性 →\x00*\x00属性名
-
public
属性 → 直接存储属性名

level-5:序列化的普通值规则
直接看源代码
<?php
$your_object = unserialize($_POST['o']);
$your_array = unserialize($_POST['a']);
$your_string = unserialize($_POST['s']);
$your_number = unserialize($_POST['i']);
$your_boolean = unserialize($_POST['b']);
$your_NULL = unserialize($_POST['n']);
if(
$your_boolean &&
$your_NULL == null &&
$your_string == "IWANT" &&
$your_number == 1 &&
$your_object->a_value == "FLAG" &&
$your_array['a'] == "Plz" && $your_array['b'] == "Give_M3"
){
echo $flag;
}
else{
echo "You really know how to serialize?";
}
只要这几个变量的反序列化之后的值等于if中这些变量的值,那么就可以获得flag了。那么也就是说,直接把对比结果序列化,然后再对应给相应的变量即可。