2026-07-11
php反序列化
00
请注意,本文编写于 126 天前,最后修改于 88 天前,其中某些信息可能已经过时。

目录

一、什么是 PHP 反序列化漏洞?
1.1 先理解"序列化"和"反序列化"
1.2 序列化字符串长什么样?
1.3 漏洞是怎么产生的?
二、PHP 面向对象基础(Level 1-3 前置知识)
2.1 类(Class)和对象(Object)
2.2 属性(Property)
访问修饰符(权限控制)
2.3 方法(Method)
方法 vs 函数
2.4 构造函数 __construct()
2.5 继承 extends
2.6 常用的 PHP 内置函数(Level 1-3 中出现过的)
2.7 对象操作符 ->
三、Level 1:类的实例化
3.1 源码
3.2 逐行解释
3.3 解题思路
3.4 为什么 Content-Type 必须是 application/x-www-form-urlencoded?
四、Level 2:对象值的传递
4.1 源码
4.2 逐行解释
这里举个例子
4.3 为什么 Level 1 的方法不管用了?
4.4 解题思路
4.5 Level 2 涉及的所有函数和方法总结
函数(Function)
方法(Method)
关键操作符
五、Level 3:访问控制修饰符
5.1 源码
5.2 逐行解释
FLAG 类(父类)
SubFLAG 类(子类)
主逻辑
5.3 权限验证
5.4 解题思路
为什么这样有效?
关于子类的方法
5.5 访问控制权限总结表
六、三关对比总结

本文基于 PHPSerialize-labs 靶场 Level 1-3 整理。 参考文章:HelloCTF - PHP 反序列化基础


一、什么是 PHP 反序列化漏洞?

1.1 先理解"序列化"和"反序列化"

在编程中,对象是内存中的一块数据,程序关闭后就消失了。如果想把对象保存到文件里、存进数据库、或者通过网络传给另一台服务器,就需要先把对象变成一段文本字符串。

  • 序列化(Serialize):把 PHP 对象 → 转成字符串
  • 反序列化(Unserialize):把字符串 → 还原成 PHP 对象

PHP 提供了两个内置函数:

php
serialize($obj); // 对象转字符串 unserialize($str); // 字符串还原成对象

1.2 序列化字符串长什么样?

举个简单例子:

php
class 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)

1.3 漏洞是怎么产生的?

反序列化漏洞的本质是:程序信任了用户传入的序列化字符串,在还原对象的过程中触发了危险的代码执行。

当 unserialize() 执行时,PHP 会根据字符串内容"凭空"创建出一个对象。如果这个对象的类里定义了某些魔术方法(比如 __destruct()、__wakeup()),这些方法会在特定时机自动执行。如果这些方法里包含了危险操作(如 eval()、system()、include()),而对象的属性值可以被我们控制,那么就能让服务器执行我们想要的恶意代码。

简单说:

反序列化漏洞 = 用户能控制对象属性 + 对象有自动触发的危险方法

不过 Level 1-3 还没有用到 unserialize(),它们是用 eval() 来模拟"任意代码执行"的环境,让你先学会面向对象的基础操作——这是后续理解反序列化漏洞的必经之路。


二、PHP 面向对象基础(Level 1-3 前置知识)

2.1 类(Class)和对象(Object)

类是"图纸",对象是"按图纸造出来的房子"。

php
class FLAG { // 定义一个类,名字叫 FLAG public $flag = "hi"; // 类里有一个属性(变量) } $target = new FLAG(); // 用 new 关键字实例化(创建对象)
  • class:定义类的关键字
  • new:实例化关键字,创建对象
  • $target:对象的"名字",你可以把它理解为一个变量,指向内存中的那个对象

2.2 属性(Property)

属性就是类里的变量,用来存储数据。

php
class FLAG { public $public_flag = "Hello"; // 公共属性 protected $protected_flag = "World"; // 受保护属性 private $private_flag = "!"; // 私有属性 }

访问修饰符(权限控制)

修饰符本类内部子类内部类外部(直接访问)
public✅✅✅
protected✅✅❌
private✅❌❌
  • public:公共的,谁都能访问,像公共厕所。
  • protected:受保护的,只有"家里人"(本类+子类)能用,像家族秘方。
  • private:私有的,只有自己能用,连儿子都继承不了,像日记本。

💡 如果类外部强行访问 protected 或 private 属性,PHP 会报错:Cannot access protected/private property...

2.3 方法(Method)

方法就是类里的函数。函数和方法在写法上几乎一样,区别是:方法定义在类里面,需要通过对象来调用。

php
class FLAG { public $free_flag = "???"; // 这是一个方法(类里的函数) function get_free_flag() { echo $this->free_flag; // $this 指"当前这个对象" } } $target = new FLAG(); $target->get_free_flag(); // 调用对象的方法,输出 ???

方法 vs 函数

概念定义位置调用方式例子
函数(Function)类外面直接写名字调用echo strlen("abc");
方法(Method)类里面通过 -> 调用$target->get_free_flag();

2.4 构造函数 __construct()

构造函数是一个特殊的方法,当使用 new 创建对象时,它会自动执行。

php
class FLAG { public $flag; function __construct() { // 构造函数,名字固定 echo "对象创建了!"; $this->flag = "HelloCTF"; } } new FLAG(); // 一 new,就会自动调用 __construct(),输出"对象创建了!"

⚠️ 重要:反序列化创建的对象不会触发构造函数! 只有 new 才会触发。这是后续 POP 链的核心区别之一。

2.5 继承 extends

子类可以继承父类的属性和方法(但 private 的继承不了)。

php
class FLAG { // 父类 public $public_flag; protected $protected_flag; private $private_flag; } class SubFLAG extends FLAG { // 子类继承父类 // SubFLAG 自动拥有 public_flag 和 protected_flag // 但 private_flag 没有继承过来 }

2.6 常用的 PHP 内置函数(Level 1-3 中出现过的)

函数作用在靶场中的用途
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)把字符串反序列化成对象后续关卡核心

2.7 对象操作符 ->

-> 是 PHP 中访问对象属性和方法的专用符号。

php
$target->free_flag; // 访问对象的属性 $target->get_free_flag(); // 调用对象的方法 $target->free_flag = "abc"; // 修改对象的属性值

三、Level 1:类的实例化

3.1 源码

php
<?php class FLAG{ public $flag_string = "HelloCTF{????}"; function __construct(){ echo $this->flag_string; } } $code = $_POST['code']; eval($code); ?>

3.2 逐行解释

代码解释
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 代码执行

3.3 解题思路

题目要求"实例化 FLAG 类"。只要执行 new FLAG(),就会触发构造函数,自动输出 flag。

图片.png Payload:

POST / HTTP/1.1 Content-Type: application/x-www-form-urlencoded code=new+FLAG();

或者先赋值再实例化:

code=$o=new+FLAG();

3.4 为什么 Content-Type 必须是 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/xmlXML 格式数据$_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 无效果。


四、Level 2:对象值的传递

4.1 源码

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'); } ?>

4.2 逐行解释

代码解释
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 值。

4.3 为什么 Level 1 的方法不管用了?

Level 1Level 2
flag 在类属性里,构造函数自动输出flag 在类外面的全局变量 $flag_string 里
new FLAG() 触发 __construct() 直接输出类里没有构造函数,new FLAG() 只是默默创建对象,没有任何输出
新建一个对象就能拿到 flag服务器已经有一个 $target 对象,最后一定会调用它的 get_free_flag()

所以 Level 2 的核心是:不是新建对象,而是修改已有的 $target 对象。

4.4 解题思路

服务器最后会执行 $target->get_free_flag(),输出 $target->free_flag。如果能让 $target->free_flag 等于真正的 $flag_string,就能拿到 flag。

图片.png Payload:

code=$target->free_flag=$flag_string;

这行代码的意思是:把全局变量 $flag_string 的值,赋给 $target 对象的 $free_flag 属性。

执行流程:

  1. eval($code) 执行了我们的赋值语句
  2. $target->free_flag 变成了 "HelloCTF{????}"
  3. $target->get_free_flag() 输出真正的 flag

4.5 Level 2 涉及的所有函数和方法总结

函数(Function)

函数作用位置
error_reporting(0)关闭 PHP 错误报告类外面
isset($code)判断 $code 是否存在且不为 NULL类外面
eval($code)执行字符串形式的 PHP 代码类外面
highlight_file('source')高亮显示 source 文件的源码类外面

方法(Method)

方法所属类作用
get_free_flag()FLAG输出当前对象的 $free_flag 属性值

关键操作符

符号含义例子
->访问对象的属性或方法$target->free_flag
=赋值$target->free_flag = $flag_string
$变量前缀$target, $flag_string

五、Level 3:访问控制修饰符

5.1 源码

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

5.2 逐行解释

FLAG 类(父类)

代码解释
public $public_flag = "HelloCTF{?"公共属性,flag 的第一部分
protected $protected_flag = "?"受保护属性,flag 的第二部分
private $private_flag = "?}"私有属性,flag 的第三部分
get_protected_flag()公共方法,返回 protected_flag 的值(类内部可以访问)
get_private_flag()公共方法,返回 private_flag 的值(类内部可以访问)

SubFLAG 类(子类)

代码解释
class SubFLAG extends FLAGSubFLAG 继承 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 报错

5.3 权限验证

当你直接访问页面(不提交 code)时,会看到:

Public Flag: HelloCTF{se3_me_ ✅ 正常输出 Protected Flag: Error: Cannot access protected... ❌ 报错 Private Flag: Error: Cannot access private... ❌ 报错

这说明:

  • public 属性可以直接从类外部访问
  • protected 和 private 属性不能直接从类外部访问

5.4 解题思路

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();

图片.png

为什么这样有效?

  • 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。

5.5 访问控制权限总结表

修饰符本类内部子类内部类外部直接访问通过公共方法间接访问
public✅✅✅✅
protected✅✅❌(报错)✅(通过公共方法)
private✅❌(不继承)❌(报错)✅(通过公共方法)

六、三关对比总结

对比项Level 1Level 2Level 3
核心考点类的实例化对象属性赋值访问控制修饰符
flag 位置类属性,构造函数自动输出类外全局变量,需传入对象属性拆成三部分,分散在三种权限属性中
利用方式new FLAG() 触发构造$target->free_flag = $flag_string调用公共 getter 方法间接读取
关键函数eval(), $_POSTeval(), isset(), $_POSTeval(), isset(), highlight_file()
关键方法__construct()get_free_flag()get_protected_flag(), get_private_flag()
新知识点new 实例化对象属性赋值、全局变量public/protected/private 权限