不安全反序列化利用篇(三):从对象字段到危险方法调用

本文中的实验均基于 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 分支,而是直接决定危险操作作用于哪里。只要攻击者能控制这个字段,就可能让应用删除、读取或写入非预期文件。

可以把这一类攻击链抽象为:

复制代码
客户端可控的序列化对象
        ↓
服务端反序列化对象
        ↓
对象字段被应用功能读取
        ↓
字段值进入危险方法
        ↓
应用执行非预期操作

所以,分析反序列化漏洞时,不能只找 isAdminroleaccess_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 的前置基础:攻击者真正利用的不是凭空出现的新代码,而是目标应用中已经存在、并且能被反序列化数据影响的代码路径。

相关推荐
xiaoxiangsiyan2 小时前
企业日常运维高频应用服务全解
运维·网络·云原生·容器·dns
奥莱维2 小时前
【无标题】
java·前端·javascript
那年窗外下的雪.2 小时前
AIDC 学习日志 Day 3:OSPF Underlay 实验、故障排查与深入解析
服务器·网络·php
星恒讯工业路由器2 小时前
IP地址冲突:成因、影响与工业网络中的应对策略
网络·信息与通信·ip地址冲突·工业网络·网络故障排查·ip-mac绑定
深圳恒讯2 小时前
CN2 GIA和CN2 GT的区别
运维·服务器·网络
风流 少年3 小时前
Spring AI 2.0:Hello World
java·人工智能·spring
qq_589666053 小时前
Java继承
java·开发语言
八角.。3 小时前
面向对象-多态
java·开发语言
橙-极纪元3 小时前
把docker的镜像文件从一台服务器中,迁移至另一台服务器
服务器·docker·容器