不安全反序列化利用篇(四):预构建 Gadget Chain——从反序列化检测到框架利用

反序列化漏洞的根因,是应用将客户端可控的数据交给危险的反序列化机制处理。Gadget Chain 则是攻击者利用目标应用及其依赖中已经存在的代码,将可控数据逐步传递到危险操作的一种手段。

分析这类漏洞时,需要先区分两个问题:

复制代码
服务器是否真的执行了反序列化?

以及:

复制代码
目标环境中是否存在能够到达危险 Sink 的 Gadget Chain?

URLDNS 和 JRMPClient 主要用于检测 Java 反序列化是否发生;PHPGGC 则用于根据 PHP 框架及其依赖生成预构建的 Gadget Chain。它们解决的问题不同,也不能跨语言混用。

一、URLDNS:通过 DNS 查询检测 Java 反序列化

URLDNS 的目标不是执行命令,而是制造一个能够从外部观察的 DNS 查询,以确认服务器是否处理了 Java 序列化对象。

理解 URLDNS,首先需要理解 HashMap

HashMap 为什么需要 hashCode()

HashMap 是 Java 中保存 Key 与 Value 对应关系的数据结构。例如:

复制代码
alice   → Alice 的用户资料
bob     → Bob 的用户资料
carlos  → Carlos 的用户资料

为了避免每次查询都遍历全部数据,HashMap 会先调用 Key 的:

复制代码
key.hashCode()

得到一个整数,再根据这个整数确定数据应当保存在哪个桶中。

复制代码
Key
→ hashCode()
→ 得到哈希值
→ 计算桶位置
→ 保存或查找数据

这里的哈希值不是为了加密,而是为了提高数据查找效率。

当 HashMap 被反序列化时,服务器需要重新建立其内部桶结构。为了把每个 Key 放回正确的位置,HashMap 会再次计算 Key 的哈希值:

复制代码
服务器恢复 HashMap
→ 读取 Key 和 Value
→ 调用 Key.hashCode()
→ 重新建立桶结构

URLDNS 正是利用了这个自动调用行为。

URL 对象为什么会产生 DNS 查询

URLDNS 将一个 java.net.URL 对象放入 HashMap 的 Key 中:

复制代码
HashMap
└── Key:URL 对象
    └── host:攻击者提供的唯一域名

服务器反序列化时:

复制代码
HashMap 恢复桶结构
→ 调用 URL Key 的 hashCode()
→ URL 处理其中的主机名
→ Java 尝试解析主机名
→ 服务器发出 DNS 查询

如果 URL 中使用 Burp Collaborator 提供的唯一域名,那么服务器产生的 DNS 查询就会被 Collaborator 记录。

完整因果链是:

复制代码
ObjectInputStream.readObject()
→ HashMap.readObject()
→ 重建 HashMap
→ URL.hashCode()
→ 主机名解析
→ DNS 查询

ysoserial 还会处理 URL 对象内部的哈希缓存,确保目标服务器需要重新计算哈希值。否则,DNS 查询可能在 Payload 生成阶段就发生在本地,而不是发生在目标服务器上。

URL.hashCode() 是普通方法还是 Gadget

URL.hashCode() 首先是 Java 标准库中真实存在的普通方法。它不是 ysoserial 添加到目标服务器上的代码,也不是专门为攻击设计的函数。

Gadget 并不是 Java 中的一种特殊语法类型,而是一种安全分析中的角色:

目标环境中原本存在、能够被非预期触发,并产生攻击者所需效果的代码组件。

正常情况下,URL.hashCode() 只是计算 URL 对象的哈希值。但在 URLDNS 中,它同时满足:

  • 目标服务器上存在该方法;
  • HashMap 反序列化时会自动调用它;
  • 攻击者能够控制 URL 中的主机名;
  • 它能够继续触发主机名解析;
  • DNS 查询能够被攻击者从外部观察。

因此,URL.hashCode() 是 Java 标准库中的合法方法,但在 URLDNS 这条特定调用链中承担了 Gadget 的作用。

更准确地划分:

复制代码
HashMap.readObject()
→ 启动对象恢复和哈希计算

URL.hashCode()
→ 将执行流引向主机名处理

DNS 解析
→ 产生最终可观察的网络副作用

URLDNS 的证据边界

如果 Collaborator 收到 DNS 查询,可以证明:

  • 目标处理了提交的 Java 序列化数据;
  • HashMap、URL 等 URLDNS 所需的 JDK 类可以使用;
  • 反序列化过程触发了 URL 主机名解析;
  • 服务器具备可观察的 DNS 出站能力。

但它不能直接证明:

  • Apache Commons Collections 存在;
  • 某条 Commons Collections Gadget Chain 可以使用;
  • 服务器没有配置对象过滤;
  • 调用链能够到达命令执行函数;
  • Java 进程拥有执行目标命令所需的系统权限。

原因在于 URLDNS 使用的是 Java 标准库中的 HashMapURL,没有经过 Apache Commons Collections。

因此:

复制代码
URLDNS 成功
≠ Commons Collections 存在
≠ RCE Gadget Chain 可用
≠ 任意命令执行成立

同样,URLDNS 没有回连也不能证明目标安全。失败还可能来自 DNS 出站限制、对象过滤、Payload 损坏、DNS 缓存,或者数据根本没有到达反序列化入口。

二、JRMPClient:通过 TCP 连接和时间差检测

JRMPClient 同样主要用于检测 Java 反序列化,但它观察的不是 DNS 查询,而是 TCP 连接行为。

JRMP 是 Java Remote Method Protocol,主要用于 Java RMI 通信。JRMPClient Payload 中包含一个远程地址:

复制代码
IP:端口

服务器反序列化该对象时,Java 的 RMI/JRMP 处理逻辑会尝试连接这个地址:

复制代码
服务器执行反序列化
→ 恢复 JRMP 远程引用
→ Java 处理远程端点
→ 尝试建立 TCP 连接

如果指定的是受控监听地址,并且监听端记录到连接,就能确认服务器处理了 JRMPClient 对象。

为什么可以通过响应时间检测

在无法直接接收连接的情况下,还可以比较不同网络状态下的响应时间。

连接一个会快速拒绝连接的地址时:

复制代码
服务器发送 TCP SYN
→ 对端立即返回 RST
→ 连接快速失败
→ HTTP 请求较快返回

连接一个被防火墙静默丢弃的地址时:

复制代码
服务器发送 TCP SYN
→ 没有收到任何响应
→ 操作系统等待并重试
→ 连接最终超时
→ HTTP 请求明显变慢

因此,可以只改变 Payload 中的 IP,进行重复对照:

复制代码
快速失败地址
→ 响应较快

静默丢弃地址
→ 响应明显变慢

如果这种差异能够稳定复现,就支持"服务器在反序列化过程中尝试建立 TCP 连接"的判断。

不过,时间差仍然属于间接证据,需要排除:

  • 服务器负载变化;
  • 代理或网络抖动;
  • 防火墙主动拒绝而不是静默丢弃;
  • 应用自身设置的连接超时;
  • 多次请求之间的缓存或连接复用。

JRMPClient 能够证明的是:

复制代码
反序列化过程触发了 TCP 连接尝试

它同样不能单独证明任意命令执行。

URLDNS 和 JRMPClient 都包含在 ysoserial 中,不是两个需要单独下载的工具。PortSwigger 对检测型 Gadget Chain 的说明

三、PHPGGC:利用 PHP 框架中的预构建 Gadget Chain

PHPGGC,即 PHP Generic Gadget Chains,是 PHP 生态中的预构建 Gadget Chain 工具。它收录了 Symfony、Laravel、Monolog、Doctrine、CodeIgniter 等框架或组件中的已知对象调用链。

PHPGGC 不会向目标服务器植入新的代码。它只是按照目标框架中已有的类关系,在本地生成一个特殊的 PHP 序列化对象:

复制代码
目标框架中已经存在相关类
→ PHPGGC 按照已知关系组织对象属性
→ 生成序列化对象
→ 目标执行 unserialize()
→ magic method 自动启动调用链
→ 攻击者数据到达危险 Sink

因此,使用 PHPGGC 前必须回答:

复制代码
目标使用什么语言?
目标使用什么序列化机制?
目标使用什么框架和版本?
服务器加载了哪些相关类?
哪条 Gadget Chain 与该版本兼容?
数据是否会进入 unserialize()?
是否存在签名、加密或对象过滤?

仅仅发现 PHPGGC 中存在某条链,不能证明目标存在反序列化漏洞。真正的漏洞仍然是对客户端可控数据执行危险反序列化,Gadget Chain 只是利用手段。

如何区分 PHP 与 Java 原生序列化

不能只凭实验说明判断数据属于 PHP 还是 Java,应当直接检查解码后的序列化格式。

本次 Cookie 的 Base64 token 解码后类似:

复制代码
O:4:"User":2:{
    s:8:"username";
    s:6:"wiener";
    ...
}

这符合 PHP serialize() 的格式:

复制代码
O:<类名长度>:"<类名>":<属性数量>:{...}

PHP 常见类型标记包括:

复制代码
i:123;              integer
b:1;                boolean
s:5:"hello";        string
a:2:{...}           array
O:4:"User":2:{...}  object

Java 原生序列化则是二进制对象流,原始数据通常以以下魔数开头:

复制代码
AC ED 00 05

Base64 编码后经常以:

复制代码
rO0AB...

开头。

Java 对象流中同样会出现可读的类名和属性名,所以从 Burp 中查看时可能感觉与 PHP 对象相似。但 Java 类名周围存在大量二进制协议标记,而 PHP 则具有明显的类型、长度、分号和大括号语法。

因此,本次实验中的判断依据是:

复制代码
Base64 解码
→ 出现 O:4:"User":2:{...}
→ 符合 PHP serialize() 语法
→ 判断为 PHP 原生序列化对象

随后错误响应中出现 Symfony 4.3.6,是对 PHP 框架和版本的进一步识别,而不是判断 PHP 序列化格式的唯一依据。

需要注意,Java 应用不一定使用 Java 原生序列化,PHP 应用也不一定使用 serialize()。语言、框架和序列化格式需要分别判断。Java 原生序列化协议PHP serialize() 官方说明

本次 PortSwigger Lab 使用序列化对象保存 Session,同时使用 HMAC-SHA1 对 Cookie 内容进行签名。实验展示了完整利用链中三个关键问题:

  • 如何识别序列化格式和目标框架;
  • 如何突破签名 Cookie 的完整性保护;
  • 如何选择与目标版本兼容的预构建 Gadget Chain。

原始 Cookie 经过 URL 解码后,结构类似:

复制代码
{
  "token": "Base64编码的数据",
  "sig_hmac_sha1": "HMAC-SHA1签名"
}

继续对 token 进行 Base64 解码,可以看到 PHP 序列化对象。

整个封装关系是:

复制代码
URL 编码的 Cookie
→ JSON
→ token + sig_hmac_sha1
→ token 进行 Base64 解码
→ PHP 序列化对象

这说明客户端持有的是一个经过签名保护的 PHP 序列化对象。

签名保护解决了什么问题

HMAC-SHA1 不负责隐藏 token,而是验证它是否被不知道密钥的人修改。

复制代码
原 token + 原签名
→ 验证通过

修改后的 token + 原签名
→ 验证失败

服务器大致按照以下顺序处理:

复制代码
URL 解码 Cookie
→ 解析 JSON
→ 取出 token 和 sig_hmac_sha1
→ 使用 SECRET_KEY 重新计算 HMAC-SHA1
→ 比较签名
→ 签名正确后 Base64 解码 token
→ 执行 unserialize()

因此,即使 PHPGGC 能生成恶意对象,如果没有签名密钥,也无法让恶意 Cookie 通过完整性验证。

通过错误响应识别框架

实验中只修改签名的一个字符,同时保持 JSON 和 Base64 token 不变,服务器返回:

复制代码
Internal Server Error: Symfony Version: 4.3.6

这是一个只改变单一变量的对照实验:

复制代码
正常签名
→ 请求正常处理

错误签名
→ 验证失败
→ 错误响应泄露 Symfony 4.3.6

框架版本的作用,是帮助选择兼容的 PHPGGC Gadget Chain。它不是漏洞本身,也不能单独证明命令执行成立。

/cgi-bin/phpinfo.php 是如何发现的

该路径不是根据 Symfony 版本计算出来的,也不是因为所有 PHP 网站都会存在这个地址。

本 Lab 的正常页面 HTML 中遗留了开发者注释,注释中暴露了调试页面路径:

复制代码
/cgi-bin/phpinfo.php

HTML 注释不会显示在正常渲染页面中,但仍然属于服务器响应内容,因此可以在以下位置发现:

  • Burp 的 Response Raw;
  • 浏览器"查看页面源代码";
  • 对响应内容搜索 phpinfocgi-binDebug

官方解题说明同样明确指出,该路径来自开发者注释,而不是来自 Symfony 错误响应。PortSwigger 官方 Lab

这一步可以提炼为一般化的识别方法:

复制代码
检查正常响应中的 HTML 注释
→ 检查 JavaScript 和静态资源
→ 检查错误响应中的内部路径
→ 检查公开的调试和诊断端点

不能将 /cgi-bin/phpinfo.php 当成所有 PHP 应用的固定路径,它只是这个 Lab 特意留下的线索。

phpinfo 为什么会泄露密钥

phpinfo() 是 PHP 的诊断函数,可以输出当前 PHP 环境、配置、模块、请求信息和环境变量。

本 Lab 将 Cookie 签名密钥保存在服务器进程的环境变量:

复制代码
SECRET_KEY

调试页面调用 phpinfo() 后,环境变量被直接输出到 HTTP 响应中:

复制代码
<td class="e">SECRET_KEY</td>
<td class="v">...</td>

所以攻击者不是根据 HMAC 结果"破解"了密钥,而是:

复制代码
开发者注释泄露调试页面路径
→ 请求 phpinfo 页面
→ phpinfo 输出服务器环境变量
→ HTTP 响应直接泄露 SECRET_KEY

HMAC 算法本身没有被绕过。失效的是密钥保密性。

只要密钥仍然保密,攻击者无法根据一个 HMAC-SHA1 结果直接反推出密钥。但一旦服务器直接泄露密钥,攻击者就可以为任意新 token 生成合法签名。PHP phpinfo() 官方说明

匹配 Symfony Gadget Chain

错误响应确认目标使用 Symfony 4.3.6。PHPGGC 中的 Symfony/RCE4 覆盖对应版本范围,因此可以生成与目标类结构兼容的对象。

利用过程可以概括为:

复制代码
识别 PHP 序列化格式
→ 确认 Symfony 4.3.6
→ 选择兼容的 Symfony/RCE4
→ PHPGGC 生成恶意序列化对象
→ 按 Cookie 要求进行 Base64 编码
→ 使用泄露的 SECRET_KEY 计算新 HMAC
→ 重新组成签名 Cookie
→ 服务器验签通过
→ 服务器执行 unserialize()
→ __destruct 启动 Gadget Chain
→ 攻击者数据到达命令执行 Sink

具体 Payload 并不是本实验最值得记忆的部分。更重要的是理解:PHPGGC 只负责生成中间的序列化对象,应用特有的编码、签名和 Cookie 封装仍然需要单独分析。

为什么命令执行后仍然返回 500

提交恶意 Cookie 后,服务器返回:

复制代码
Call to a member function saveDeferred() on null

但 Lab 仍然成功。

原因是 Symfony/RCE4 的危险函数调用发生在报错之前:

复制代码
TagAwareAdapter::__destruct()
→ commit()
→ invalidateTags()
→ ProxyAdapter->saveDeferred()
→ ProxyAdapter->doSave()
→ 调用攻击者指定的函数
→ 命令执行
→ 程序继续处理缓存对象
→ pool 属性为 null
→ 调用 saveDeferred() 时报错

PHPGGC 不需要构造一个功能完整的 Symfony 缓存对象,只需要让对象具备足够的字段,使程序先到达危险调用位置。后续对象状态不完整,仍然可能产生异常,但已经完成的文件删除等副作用不会被撤销。

因此:

复制代码
HTTP 500
≠ Gadget Chain 一定失败

判断是否利用成功,应结合:

  • 错误发生在调用链的哪个位置;
  • 危险 Sink 是否位于错误之前;
  • 预期副作用是否出现;
  • Lab 状态是否变为 Solved

本次实验中,Lab 显示 Solved 是最终结果证据,500 响应只是 Gadget Chain 执行后的尾部异常。

从实验提炼出的通用分析框架

URLDNS、JRMPClient 和 PHPGGC 可以放入一套分层分析模型:

复制代码
识别输入格式
→ 判断数据是否由客户端控制
→ 确认服务器是否真的反序列化
→ 判断语言和序列化机制
→ 识别框架、依赖与版本
→ 检查签名、加密和对象过滤
→ 匹配可用 Gadget Chain
→ 确定入口 Gadget
→ 跟踪攻击者数据如何传递
→ 确定最终 Sink
→ 使用最低影响方式验证结果

其中:

复制代码
URLDNS
→ 通过 DNS 副作用确认 Java 反序列化

JRMPClient
→ 通过 TCP 连接或时间差确认 Java 反序列化

PHPGGC
→ 根据 PHP 框架和版本生成预构建 Gadget Chain

检测链回答的是"服务器是否处理了对象",利用链回答的是"现有代码能否把攻击者数据带到危险操作"。两者不能混为一谈。

防御思路

最根本的防御,是避免对客户端可控数据执行原生对象反序列化。Session 状态应优先保存在服务器端,客户端只保存不可预测的 Session ID。

如果业务确实需要处理序列化数据,还应采取以下控制:

  • 只接受必要的基础数据类型;
  • 使用严格的类型白名单;
  • Java 配置适当的 ObjectInputFilter
  • PHP 谨慎使用 unserialize() 并限制 allowed_classes
  • 在反序列化前验证数据完整性;
  • 安全保存签名密钥并定期轮换;
  • 禁止生产环境暴露 phpinfo() 和调试页面;
  • 不在 HTML 注释、JavaScript 和错误响应中泄露内部路径;
  • 关闭详细框架错误信息;
  • 移除不必要的依赖并及时升级框架;
  • 限制应用进程的文件、网络和命令执行权限;
  • 限制不必要的 DNS 与 TCP 出站连接。

签名 Cookie 只能在密钥保密的前提下保护数据完整性。它不能消除反序列化本身的危险。一旦密钥通过调试页面、源代码、日志或配置泄露,攻击者就可以为恶意对象生成合法签名。

本章总结

本章需要区分四个核心概念:

复制代码
反序列化不可信数据
= 漏洞根因

Gadget
= 目标环境中可被复用的现有代码

Gadget Chain
= 多个 Gadget 组成的可达调用路径

Sink
= 最终执行敏感操作的位置

URLDNS 利用 HashMap 在恢复桶结构时调用 URL.hashCode(),通过 DNS 查询确认 Java 反序列化;JRMPClient 利用 RMI/JRMP 远程引用处理,通过 TCP 连接或响应时间差提供另一种检测方式;PHPGGC 则根据 PHP 框架中的已知类和 magic method 生成预构建对象链,将反序列化推进到文件操作或命令执行。

在 Symfony Lab 中,最终利用成立不是因为单一漏洞,而是多个条件同时出现:

复制代码
客户端控制序列化 token
+ 服务器执行 PHP 反序列化
+ 错误响应泄露 Symfony 版本
+ 开发者注释泄露调试路径
+ phpinfo 泄露 HMAC 密钥
+ 目标加载兼容的 Symfony Gadget
+ PHPGGC 生成匹配对象
+ 恶意 token 被重新合法签名
= Gadget Chain 到达命令执行 Sink

这也是分析反序列化漏洞时最值得迁移的思路:不要只问"有没有现成 Payload",而要逐层确认输入格式、反序列化入口、目标依赖、自动触发机制、完整性保护、数据传递路径和最终 Sink。

相关推荐
CodeBlog-star1 小时前
LLM能力与边界:多模态、幻觉、上下文窗口及开源模型对比
人工智能·python·开源·llm
COOLMO研究AI1 小时前
Python 如何实现 AI API 的动态路由与多通道负载均衡:多账号与多供应商的高可用调度
人工智能·python·负载均衡
LONGZETECH1 小时前
无人机组装调试实训高损耗痛点分析与虚拟仿真教学落地方案
c语言·开发语言·架构·无人机
OptimizationMaster1 小时前
Python自动重启中国移动光猫(ZXHN G7611V2)
python·自动化工具·重启中国移动光猫
测开小菜鸟1 小时前
智能体安全与伦理测试:守护AI的“底线”,让智能体安全、负责任地工作
网络·人工智能·安全
luj_17681 小时前
大航海时代:沉浸式财商实战沙盒
c语言·开发语言·网络·经验分享·算法
赵广陆2 小时前
企业实战:Markdown图片检索
android·java·开发语言
C++、Java和Python的菜鸟2 小时前
第3章 从0开始用若依
java·开发语言
刀锋00012 小时前
从0到1手搓生产级 AI Agent:LangGraph 1.2 + LangChain 1.3 保姆级实战(全部代码已跑通)
人工智能·python·langchain·ai agent·langgraph
夜雪一千2 小时前
Python 如何实现 SHA 加密?SHA1 / SHA256 / SHA512 实战教程
开发语言·python