本文基于 PHPSerialize-labs 靶场 Level 1-3 整理。 参考文章:HelloCTF - PHP 反序列化基础
在编程中,对象是内存中的一块数据,程序关闭后就消失了。如果想把对象保存到文件里、存进数据库、或者通过网络传给另一台服务器,就需要先把对象变成一段文本字符串。
PHP 提供了两个内置函数:
phpserialize($obj); // 对象转字符串
unserialize($str); // 字符串还原成对象
举个简单例子:
phpclass User {
public $name = "Alice";
}
$obj = new User();
echo serialize($obj);
// 输出:O:4:"User":1:{s:4:"name";s:5:"Alice";}
拆解这段字符串:
| 片段 | 含义 |
|---|---|
O:4:"User" | O = Object(对象),4 = 类名长度,User = 类名 |
:1: | 这个对象有 1 个属性 |
{s:4:"name";s:5:"Alice";} | 属性名是 name(字符串,长度4),属性值是 Alice(字符串,长度5) |
反序列化漏洞的本质是:程序信任了用户传入的序列化字符串,在还原对象的过程中触发了危险的代码执行。
当 unserialize() 执行时,PHP 会根据字符串内容"凭空"创建出一个对象。如果这个对象的类里定义了某些魔术方法(比如 __destruct()、__wakeup()),这些方法会在特定时机自动执行。如果这些方法里包含了危险操作(如 eval()、system()、include()),而对象的属性值可以被我们控制,那么就能让服务器执行我们想要的恶意代码。
简单说:
反序列化漏洞 = 用户能控制对象属性 + 对象有自动触发的危险方法
不过 Level 1-3 还没有用到 unserialize(),它们是用 eval() 来模拟"任意代码执行"的环境,让你先学会面向对象的基础操作——这是后续理解反序列化漏洞的必经之路。
类是"图纸",对象是"按图纸造出来的房子"。
phpclass FLAG { // 定义一个类,名字叫 FLAG
public $flag = "hi"; // 类里有一个属性(变量)
}
$target = new FLAG(); // 用 new 关键字实例化(创建对象)
class:定义类的关键字new:实例化关键字,创建对象$target:对象的"名字",你可以把它理解为一个变量,指向内存中的那个对象属性就是类里的变量,用来存储数据。
phpclass FLAG {
public $public_flag = "Hello"; // 公共属性
protected $protected_flag = "World"; // 受保护属性
private $private_flag = "!"; // 私有属性
}
| 修饰符 | 本类内部 | 子类内部 | 类外部(直接访问) |
|---|---|---|---|
public | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ❌ |
private | ✅ | ❌ | ❌ |
💡 如果类外部强行访问
protected或private属性,PHP 会报错:Cannot access protected/private property...
方法就是类里的函数。函数和方法在写法上几乎一样,区别是:方法定义在类里面,需要通过对象来调用。
phpclass FLAG {
public $free_flag = "???";
// 这是一个方法(类里的函数)
function get_free_flag() {
echo $this->free_flag; // $this 指"当前这个对象"
}
}
$target = new FLAG();
$target->get_free_flag(); // 调用对象的方法,输出 ???
| 概念 | 定义位置 | 调用方式 | 例子 |
|---|---|---|---|
| 函数(Function) | 类外面 | 直接写名字调用 | echo strlen("abc"); |
| 方法(Method) | 类里面 | 通过 -> 调用 | $target->get_free_flag(); |
__construct()构造函数是一个特殊的方法,当使用 new 创建对象时,它会自动执行。
phpclass FLAG {
public $flag;
function __construct() { // 构造函数,名字固定
echo "对象创建了!";
$this->flag = "HelloCTF";
}
}
new FLAG(); // 一 new,就会自动调用 __construct(),输出"对象创建了!"
⚠️ 重要:反序列化创建的对象不会触发构造函数! 只有
new才会触发。这是后续 POP 链的核心区别之一。
extends子类可以继承父类的属性和方法(但 private 的继承不了)。
phpclass FLAG { // 父类
public $public_flag;
protected $protected_flag;
private $private_flag;
}
class SubFLAG extends FLAG { // 子类继承父类
// SubFLAG 自动拥有 public_flag 和 protected_flag
// 但 private_flag 没有继承过来
}
| 函数 | 作用 | 在靶场中的用途 |
|---|---|---|
eval($code) | 把字符串当作 PHP 代码执行 | 执行我们 POST 提交的 payload |
isset($var) | 判断变量是否已设置且不为 NULL | 判断有没有收到 code 参数 |
$_POST['key'] | 获取 POST 请求中的表单数据 | 接收我们提交的 code |
error_reporting(0) | 关闭 PHP 错误报告 | 隐藏报错信息,防止信息泄露 |
highlight_file($file) | 高亮显示 PHP 源代码 | 没有提交 code 时展示源码 |
echo $var | 输出内容到页面 | 打印 flag |
serialize($obj) | 把对象序列化成字符串 | Level 4 开始用到 |
unserialize($str) | 把字符串反序列化成对象 | 后续关卡核心 |
->-> 是 PHP 中访问对象属性和方法的专用符号。
php$target->free_flag; // 访问对象的属性
$target->get_free_flag(); // 调用对象的方法
$target->free_flag = "abc"; // 修改对象的属性值
php<?php
class FLAG{
public $flag_string = "HelloCTF{????}";
function __construct(){
echo $this->flag_string;
}
}
$code = $_POST['code'];
eval($code);
?>
| 代码 | 解释 |
|---|---|
class FLAG{ ... } | 定义 FLAG 类,里面有一个公共属性 $flag_string,一个构造函数 __construct() |
public $flag_string = "HelloCTF{????}" | 属性存着 flag(问号是占位符,实际部署时有真实 flag) |
function __construct(){ echo $this->flag_string; } | 构造函数,对象创建时自动执行,输出 flag |
$code = $_POST['code']; | 从 POST 请求中取 code 参数 |
eval($code); | 危险点:把我们传入的字符串当作 PHP 代码执行 |
题目要求"实例化 FLAG 类"。只要执行 new FLAG(),就会触发构造函数,自动输出 flag。
Payload:
POST / HTTP/1.1 Content-Type: application/x-www-form-urlencoded code=new+FLAG();
或者先赋值再实例化:
code=$o=new+FLAG();
application/x-www-form-urlencoded?因为 PHP 的超全局变量 $_POST 并不是任意 POST 数据都会自动填充,它只解析特定格式的请求体。
当 PHP 收到一个 POST 请求时,它会检查请求头中的 Content-Type:
application/x-www-form-urlencoded 或 multipart/form-data,PHP 就会读取请求体,将其按对应格式解析成数组,存入 $_POST。text/plain、application/json 等其他类型,PHP 不会解析请求体,$_POST 会是一个空数组。本题代码 $code = $_POST['code']; 直接从 $_POST 获取数据。如果不带 Content-Type: application/x-www-form-urlencoded,$_POST 就是空的,$code 为 NULL,eval("") 执行无代码,自然看不到 flag。所以这个头是必须的。
常见 Content-Type 类型及 PHP 处理方式
| Content-Type 值 | 说明 | PHP 中如何获取数据 |
|---|---|---|
application/x-www-form-urlencoded | 默认的表单提交格式,数据经 URL 编码,形如 key1=value1&key2=value2 | 自动解析到 $_POST |
multipart/form-data | 用于上传文件,请求体被分隔成多个部分 | 自动解析,文件信息在 $_FILES,普通字段仍在 $_POST |
application/json | 发送 JSON 格式数据,常见于 AJAX 请求 | $_POST 为空,需用 file_get_contents("php://input") 读取原始输入,再 json_decode() |
text/plain | 纯文本数据 | $_POST 为空,同样需读 php://input |
application/xml 或 text/xml | XML 格式数据 | $_POST 为空,需读 php://input 再解析 XML |
关键点:只有前两种 MIME 类型能让 PHP 自动填充
$_POST,其他类型需要手动读取原始请求体。
实际演示
假设请求体为 code=new FLAG();:
带 Content-Type: application/x-www-form-urlencoded
$_POST = ['code' => 'new FLAG();'] ✅ 正常执行。
不带 Content-Type,或设为 application/json
$_POST = [] ❌ 代码为空,eval 无效果。
php<?php
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');
}
?>
| 代码 | 解释 |
|---|---|
error_reporting(0); | 关闭报错,防止泄露信息 |
$flag_string = "HelloCTF{????}"; | 全局变量,存着真正的 flag,在类外面 |
class FLAG{ ... } | 定义 FLAG 类,里面 $free_flag 初始值是 "???"(假 flag) |
function get_free_flag(){ echo $this->free_flag; } | 方法:输出当前对象的 $free_flag 属性 |
$target = new FLAG(); | 服务器已经创建了一个对象 $target,此时它的 $free_flag 是 "???" |
if(isset($code)){ ... } | 判断有没有收到 code 参数 |
eval($code); | 执行我们提交的代码 |
$target->get_free_flag(); | 执行完我们的代码后,服务器调用 $target 的方法输出它的 $free_flag |
带参数的构造函数
php
class Dog {
public $name;
// 构造函数:创建对象时传入名字
function __construct($name) {
$this->name = $name;
echo "一只新狗出生了,名字叫:$name\n";
}
function bark() {
echo $this->name . ":汪!\n";
}
}
// 创建对象时传入参数,构造函数自动执行
$dog1 = new Dog("旺财"); // 输出:一只新狗出生了,名字叫:旺财
$dog2 = new Dog("来福"); // 输出:一只新狗出生了,名字叫:来福
$dog1->bark(); // 旺财:汪!
$dog2->bark(); // 来福:汪!
可以看到:
这里的箭头可以理解为进入这个类修改调用其中的东西
new Dog("旺财") 自动调用了 __construct("旺财")。
构造函数把传入的名字赋值给 $this->name。
每个对象都有自己的 name 值。
| Level 1 | Level 2 |
|---|---|
| flag 在类属性里,构造函数自动输出 | flag 在类外面的全局变量 $flag_string 里 |
new FLAG() 触发 __construct() 直接输出 | 类里没有构造函数,new FLAG() 只是默默创建对象,没有任何输出 |
| 新建一个对象就能拿到 flag | 服务器已经有一个 $target 对象,最后一定会调用它的 get_free_flag() |
所以 Level 2 的核心是:不是新建对象,而是修改已有的 $target 对象。
服务器最后会执行 $target->get_free_flag(),输出 $target->free_flag。如果能让 $target->free_flag 等于真正的 $flag_string,就能拿到 flag。
Payload:
code=$target->free_flag=$flag_string;
这行代码的意思是:把全局变量 $flag_string 的值,赋给 $target 对象的 $free_flag 属性。
执行流程:
eval($code) 执行了我们的赋值语句$target->free_flag 变成了 "HelloCTF{????}"$target->get_free_flag() 输出真正的 flag| 函数 | 作用 | 位置 |
|---|---|---|
error_reporting(0) | 关闭 PHP 错误报告 | 类外面 |
isset($code) | 判断 $code 是否存在且不为 NULL | 类外面 |
eval($code) | 执行字符串形式的 PHP 代码 | 类外面 |
highlight_file('source') | 高亮显示 source 文件的源码 | 类外面 |
| 方法 | 所属类 | 作用 |
|---|---|---|
get_free_flag() | FLAG | 输出当前对象的 $free_flag 属性值 |
| 符号 | 含义 | 例子 |
|---|---|---|
-> | 访问对象的属性或方法 | $target->free_flag |
= | 赋值 | $target->free_flag = $flag_string |
$ | 变量前缀 | $target, $flag_string |
php<?php
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>";
}
?>
| 代码 | 解释 |
|---|---|
public $public_flag = "HelloCTF{?" | 公共属性,flag 的第一部分 |
protected $protected_flag = "?" | 受保护属性,flag 的第二部分 |
private $private_flag = "?}" | 私有属性,flag 的第三部分 |
get_protected_flag() | 公共方法,返回 protected_flag 的值(类内部可以访问) |
get_private_flag() | 公共方法,返回 private_flag 的值(类内部可以访问) |
| 代码 | 解释 |
|---|---|
class SubFLAG extends FLAG | SubFLAG 继承 FLAG,成为子类 |
show_protected_flag() | 子类方法,返回 $this->protected_flag。可以访问,因为 protected 允许子类访问 |
show_private_flag() | 子类方法,返回 $this->private_flag。访问不到父类的 private 属性,因为 private 不继承 |
| 代码 | 解释 |
|---|---|
$target = new FLAG(); | 创建父类对象 |
$sub_target = new SubFLAG(); | 创建子类对象 |
else 分支里的三个 echo | 直接访问三个属性,演示权限效果:public 能输出,protected 和 private 报错 |
当你直接访问页面(不提交 code)时,会看到:
Public Flag: HelloCTF{se3_me_ ✅ 正常输出 Protected Flag: Error: Cannot access protected... ❌ 报错 Private Flag: Error: Cannot access private... ❌ 报错
这说明:
public 属性可以直接从类外部访问protected 和 private 属性不能直接从类外部访问flag 被拆成了三部分,分别存在三种权限的属性里。直接访问会报错,但我们可以通过类内部提供的公共方法来间接获取。
因为 eval($code) 允许我们执行任意代码,所以可以直接调用 $target 对象的公共方法:
Payload:
code=echo $target->public_flag . $target->get_protected_flag() . $target->get_private_flag();
. 是 PHP 里的字符串连接运算符,作用是把左右两边的字符串拼接成一个新的字符串。
php$name = "小明"; $age = 18; echo $name . "今年" . $age . "岁";小明今年18岁
或者分步输出:
code=echo $target->get_protected_flag(); echo $target->get_private_flag();

get_protected_flag() 是 public 方法,外部可以调用 ✅$this->protected_flag,是在类内部访问,不受限制 ✅get_private_flag() 同理 ✅$sub_target->show_protected_flag() 可以拿到 protected 部分,因为子类能访问继承来的 protected 属性 ✅$sub_target->show_private_flag() 拿不到父类的 private 属性,因为 private 不继承 ❌所以推荐用父类对象 $target 的两个 getter 方法来获取完整的 flag。
| 修饰符 | 本类内部 | 子类内部 | 类外部直接访问 | 通过公共方法间接访问 |
|---|---|---|---|---|
public | ✅ | ✅ | ✅ | ✅ |
protected | ✅ | ✅ | ❌(报错) | ✅(通过公共方法) |
private | ✅ | ❌(不继承) | ❌(报错) | ✅(通过公共方法) |
| 对比项 | Level 1 | Level 2 | Level 3 |
|---|---|---|---|
| 核心考点 | 类的实例化 | 对象属性赋值 | 访问控制修饰符 |
| flag 位置 | 类属性,构造函数自动输出 | 类外全局变量,需传入对象属性 | 拆成三部分,分散在三种权限属性中 |
| 利用方式 | new FLAG() 触发构造 | $target->free_flag = $flag_string | 调用公共 getter 方法间接读取 |
| 关键函数 | eval(), $_POST | eval(), isset(), $_POST | eval(), isset(), highlight_file() |
| 关键方法 | __construct() | get_free_flag() | get_protected_flag(), get_private_flag() |
| 新知识点 | new 实例化 | 对象属性赋值、全局变量 | public/protected/private 权限 |