本文中的实验均基于 PortSwigger Web Security Academy 官方教学靶场,仅针对指定 Lab 页面进行授权测试,目的是完成指定 Lab,不涉及真实网站或第三方系统。
前两章主要讨论反序列化对象如何影响认证和授权判断:修改对象属性可以改变权限状态,修改数据类型可以影响认证比较。第三章开始,利用思路进一步扩展:反序列化对象中的数据不一定只会被拿来判断,它还可能被应用功能当作参数,传入文件删除、文件读取、文件写入、模板处理等危险方法。
这类问题的核心不是攻击者直接调用了危险函数,而是攻击者控制了反序列化对象中的某个字段,并让应用在正常执行路径中使用这个字段。换句话说,攻击者并不需要"发明"新的代码路径,只需要让已有功能处理恶意数据。
本章通过两个 PortSwigger Lab 作为例子,分析两种递进关系:
利用应用已有功能
↓
手动触发危险方法
利用 magic method
↓
自动触发危险方法
两个实验的目标都是删除 Carlos home directory 下的 morale.txt 文件。这个目标只是结果验证,真正要理解的是:反序列化对象中的可控数据,如何一步步流入危险方法。
一、从安全判断到危险方法调用
前面的反序列化利用主要围绕"判断逻辑"展开。例如:
if ($user->isAdmin) {
// allow admin access
}
或者:
if ($login['access_token'] == $storedToken) {
// authenticated
}
这类问题的本质是:服务端把反序列化对象中的字段当作认证或授权依据,导致攻击者可以通过修改字段影响判断结果。
第三章关注的是另一类场景:服务端不是拿字段做判断,而是把字段作为危险方法的输入。例如:
unlink($user->avatar_link);
在这种情况下,字段值不再只是影响 if 分支,而是直接决定危险操作作用于哪里。只要攻击者能控制这个字段,就可能让应用删除、读取或写入非预期文件。
可以把这一类攻击链抽象为:
客户端可控的序列化对象
↓
服务端反序列化对象
↓
对象字段被应用功能读取
↓
字段值进入危险方法
↓
应用执行非预期操作
所以,分析反序列化漏洞时,不能只找 isAdmin、role、access_token 这类认证授权字段,也要关注文件路径、模板路径、URL、命令参数、回调函数、类名等可能进入危险方法的数据。
二、利用应用功能:让正常功能处理恶意字段
第一个实验 Using application functionality to exploit insecure deserialization 展示的是手动触发型利用。
实验中的应用使用基于序列化对象的 Session 机制。用户对象中存在一个头像路径字段,例如:
avatar_link = users/wiener/avatar
正常情况下,这个字段表示当前用户头像文件的位置。删除账号时,应用可能会顺便删除这个头像文件。后端逻辑可以抽象为:
$user = unserialize($_COOKIE['session']);
deleteUser($user->username);
unlink($user->avatar_link);
如果 avatar_link 来自客户端可控的序列化对象,攻击者就可以把它从自己的头像路径改成目标文件路径:
/home/carlos/morale.txt
随后攻击者触发"删除账号"这个正常功能。服务端以为自己在删除当前用户的头像文件,实际上删除的是攻击者指定的目标文件。
这个实验的利用链可以概括为:
登录普通用户
↓
解码 session cookie
↓
定位文件路径字段 avatar_link
↓
将路径改为 /home/carlos/morale.txt
↓
保持序列化格式合法
↓
触发删除账号功能
↓
服务端调用文件删除方法
↓
目标文件被删除
这里的重点不是"删除账号"按钮本身,而是这个按钮背后的业务逻辑读取了反序列化对象中的路径字段,并将其传入了危险方法。攻击者借用的是应用已有功能,而不是直接访问底层文件系统。
三、路径字段为什么敏感
文件路径字段在反序列化对象中非常敏感,因为它一旦可控,后续影响取决于它进入了什么方法。
例如:
unlink() → 文件删除
file_get_contents() → 文件读取
file_put_contents() → 文件写入
include() / require() → 文件包含
在第一个实验中,路径字段进入的是 unlink(),因此造成的是任意文件删除。攻击者没有直接调用 unlink(),而是让应用在删除账号时自动执行:
unlink('/home/carlos/morale.txt');
这说明反序列化漏洞的影响,不只取决于对象里有哪些字段,还取决于这些字段后续被哪些功能使用。同一个字段,在不同上下文里可能产生完全不同的安全影响:
头像路径用于页面展示 → 可能只是图片加载异常
头像路径用于文件读取 → 可能造成任意文件读取
头像路径用于文件删除 → 可能造成任意文件删除
头像路径用于模板处理 → 可能进入更复杂的执行链
因此,分析这类漏洞时,不能停在"字段可以修改",还要继续追踪"字段被谁使用、如何使用、是否进入危险方法"。
四、Magic method:从手动触发到自动触发
第二个实验 Arbitrary object injection in PHP 引入了 magic method。它的意义在于:攻击者不一定需要手动点击某个业务功能,只要服务端反序列化恶意对象,某些方法就可能自动执行。
Magic method 是面向对象语言中的特殊方法,不需要开发者显式调用。当特定事件发生时,语言运行时会自动调用它。
PHP 中常见的 magic method 包括:
__construct()
__wakeup()
__destruct()
__toString()
其中,__wakeup() 会在 unserialize() 恢复对象时自动触发;__destruct() 会在对象销毁时自动触发。它们本身不是漏洞,危险点在于这些方法内部是否处理了攻击者可控的数据,并将这些数据传入危险操作。
第二个实验中,通过源码备份文件可以看到 CustomTemplate 类。关键逻辑可以抽象为:
class CustomTemplate {
private $lock_file_path;
function __destruct() {
if (file_exists($this->lock_file_path)) {
unlink($this->lock_file_path);
}
}
}
这里的危险点是:
unlink($this->lock_file_path);
只要攻击者能让服务端反序列化出一个 CustomTemplate 对象,并控制它的 lock_file_path 属性,那么对象销毁时,__destruct() 就会自动调用 unlink() 删除指定文件。
攻击链可以概括为:
获取源码
↓
发现 CustomTemplate 类
↓
发现 __destruct() magic method
↓
发现 lock_file_path 会进入 unlink()
↓
构造 CustomTemplate 序列化对象
↓
将 lock_file_path 指向 /home/carlos/morale.txt
↓
替换 session cookie
↓
服务端 unserialize()
↓
对象销毁时自动触发 __destruct()
↓
目标文件被删除
这个实验的关键变化是:危险方法不再由删除账号功能触发,而是由对象生命周期自动触发。
五、任意对象注入的意义
前面的实验通常是在修改服务端原本给出的对象,例如 User 对象。而 arbitrary object injection 的关键是:如果服务端没有限制反序列化对象的类型,攻击者不一定只能提交 User,还可以提交应用中任意可用类的对象。
服务端原本可能期待:
O:4:"User":...
攻击者却提交:
O:14:"CustomTemplate":...
只要 CustomTemplate 类在应用代码中存在,PHP 就可能实例化这个对象。即使后续业务逻辑发现"这不是合法用户"并抛出 Invalid user,恶意对象也可能已经被创建,magic method 也可能已经在对象生命周期中触发。
这里要分清两层逻辑:
反序列化层:
对象是否成功被创建?
业务校验层:
应用是否接受它作为合法用户?
Arbitrary object injection 利用的是第一层。只要对象成功创建,并且 magic method 能够执行,后续业务逻辑报错不一定阻止利用成立。
这也是为什么实验中 Invalid user 不一定代表失败。它说明应用业务逻辑拒绝了非预期对象,但危险代码可能已经在此之前或请求结束时执行。
六、这两个实验的递进关系
这两个实验展示的是同一类问题的两个阶段。
第一个实验是手动触发:
控制对象字段
↓
手动调用应用功能
↓
应用功能使用字段执行危险操作
第二个实验是自动触发:
注入非预期对象
↓
反序列化或对象销毁自动调用 magic method
↓
magic method 使用属性执行危险操作
它们的共同点是:危险操作不是攻击者直接调用的,而是应用代码自己调用的。攻击者真正控制的是数据,以及数据进入代码路径的方式。
区别在于触发点不同:
Using application functionality:
需要攻击者访问某个正常功能,例如删除账号。
Arbitrary object injection:
只要服务端反序列化恶意对象,magic method 就可能自动触发。
这一步是反序列化利用从基础到高级的关键转折:攻击者不再只修改已有对象,也不再只等待某个业务功能使用字段,而是开始主动选择应用中可用的类,并利用类中的自动执行方法。
七、Gadget、Magic method 和 Sink 的关系
这一章已经开始接近 Gadget Chain 的基础概念,所以需要把几个术语放清楚。
Gadget 是应用或依赖库中已经存在、可以被攻击者在非预期上下文中利用的一段代码。攻击者没有上传新代码,而是复用目标系统已有代码。
Magic method 经常可以作为反序列化攻击中的入口 Gadget。因为它会在反序列化、对象销毁、字符串转换等事件中自动执行,不需要攻击者显式调用。
Sink 是最终执行敏感操作的位置,例如:
unlink()
file_get_contents()
file_put_contents()
include()
exec()
system()
第二个实验中的链路很短:
不可信序列化数据
↓
CustomTemplate 对象被反序列化
↓
__destruct() 自动触发
↓
lock_file_path 进入 unlink()
↓
删除目标文件
在这个链路中:
CustomTemplate::__destruct() 是 Gadget
unlink() 是 Sink
lock_file_path 是攻击者控制的数据
如果一个 Gadget 就能把可控数据直接送进 Sink,利用链会很短。后续更复杂的反序列化攻击中,往往需要多个 Gadget 串联,让数据经过多个类和方法,最终到达 Sink,这就是 Gadget Chain。
八、失败时如何定位问题
这两个实验很容易因为序列化格式、编码或对象属性可见性出错。排查时应按攻击链分层定位。
如果出现:
unserialize() failed
说明 PHP 在反序列化阶段失败。常见原因是序列化格式错误、字符串长度不匹配、Base64 或 URL 编码损坏,或者 private 属性中的 null byte 没有正确构造。
如果出现:
Invalid user
说明对象已经成功反序列化,但业务逻辑发现它不是预期的 User 对象。这个错误不一定代表攻击失败,因为 magic method 可能已经在对象创建后或请求结束时执行。
如果对象能反序列化,但目标文件没有被删除,可能是属性没有真正写入目标字段。例如 PHP private 属性在序列化中有特殊格式,不能简单写成普通属性名。private 属性名在序列化数据中通常包含类名和 null byte,如果构造不正确,类内部访问的 $this->lock_file_path 就不会指向攻击者设置的路径。
因此,排错时可以按下面顺序看:
序列化格式是否能被解析?
↓
对象类型是否是目标类?
↓
属性是否写入 magic method 实际读取的位置?
↓
magic method 是否会自动触发?
↓
可控属性是否进入危险 Sink?
不同报错代表攻击链断在不同位置。不要把所有失败都归结为 payload 错,也不要把业务层报错直接等同于反序列化失败。
九、防御思路
防御这类反序列化问题,核心原则仍然是:不要对不可信数据执行通用反序列化,更不能让反序列化对象直接进入危险功能。
更合理的做法包括:
- 不把完整对象序列化后交给客户端保存;
- 客户端只保存不可预测的 Session ID,真实状态保存在服务端;
- 如果必须使用客户端状态,应使用服务端密钥进行签名或 MAC 校验;
- 反序列化前限制允许的类,避免 arbitrary object injection;
- 对反序列化后的字段进行严格类型、格式和值域校验;
- 不让客户端可控字段直接进入文件路径、命令、模板、URL 等危险参数;
- 对文件操作使用固定目录、路径白名单和路径规范化校验;
- 审计
__wakeup()、__destruct()、__toString()等 magic method; - 避免在 magic method 中处理不可信数据或调用危险方法。
签名可以防止客户端直接篡改对象,但不能替代类白名单、危险方法审计和服务端权限设计。一旦签名校验遗漏、密钥泄露,或者存在其他反序列化入口,magic method 和 Gadget Chain 仍可能成为攻击路径。
十、小结
第三章的重点是:反序列化对象中的字段不只会影响判断逻辑,也可能成为应用功能或 magic method 的危险参数。
Using application functionality to exploit insecure deserialization 说明:如果应用的正常功能会读取反序列化对象中的路径字段,并将其传入文件删除方法,攻击者就可以通过修改该字段,借应用功能删除非预期文件。
Arbitrary object injection in PHP 进一步说明:如果服务端没有限制反序列化对象的类型,攻击者可以注入应用中任意可用类的对象。一旦该类包含会自动触发的 magic method,并且 magic method 将可控属性传入危险 Sink,攻击者就能在不手动调用业务功能的情况下触发危险操作。
这一章可以用一条主线概括:
客户端可控序列化数据
↓
服务端反序列化对象
↓
对象字段或属性进入应用代码
↓
应用功能或 magic method 被触发
↓
可控数据流向危险 Sink
↓
产生文件删除等安全影响
前两章关注的是"反序列化对象如何改变认证和授权判断"。第三章关注的是"反序列化对象如何驱动已有代码执行危险操作"。这一步是理解 Gadget Chain 的前置基础:攻击者真正利用的不是凭空出现的新代码,而是目标应用中已经存在、并且能被反序列化数据影响的代码路径。