不安全反序列化利用篇(五):预构建 Gadget Chain——从反序列化检测到框架利用 反序列化漏洞的根因是应用将客户端可控的数据交给危险的反序列化机制处理。Gadget Chain 则是攻击者利用目标应用及其依赖中已经存在的代码将可控数据逐步传递到危险操作的一种手段。分析这类漏洞时需要先区分两个问题服务器是否真的执行了反序列化以及目标环境中是否存在能够到达危险 Sink 的 Gadget ChainURLDNS 和 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 └── KeyURL 对象 └── 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()是普通方法还是 GadgetURL.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 标准库中的HashMap和URL没有经过 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 ChainPHPGGC即 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; ... }这符合 PHPserialize()的格式O:类名长度:类名:属性数量:{...}PHP 常见类型标记包括i:123; integer b:1; boolean s:5:hello; string a:2:{...} array O:4:User:2:{...} objectJava 原生序列化则是二进制对象流原始数据通常以以下魔数开头AC ED 00 05Base64 编码后经常以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() 官方说明四、Symfony 签名 Cookie 实验从框架指纹到 Gadget Chain本次 PortSwigger Lab 使用序列化对象保存 Session同时使用 HMAC-SHA1 对 Cookie 内容进行签名。实验展示了完整利用链中三个关键问题如何识别序列化格式和目标框架如何突破签名 Cookie 的完整性保护如何选择与目标版本兼容的预构建 Gadget Chain。识别 Cookie 的封装结构原始 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.phpHTML 注释不会显示在正常渲染页面中但仍然属于服务器响应内容因此可以在以下位置发现Burp 的 Response Raw浏览器“查看页面源代码”对响应内容搜索phpinfo、cgi-bin或Debug。官方解题说明同样明确指出该路径来自开发者注释而不是来自 Symfony 错误响应。PortSwigger 官方 Lab这一步可以提炼为一般化的识别方法检查正常响应中的 HTML 注释 → 检查 JavaScript 和静态资源 → 检查错误响应中的内部路径 → 检查公开的调试和诊断端点不能将/cgi-bin/phpinfo.php当成所有 PHP 应用的固定路径它只是这个 Lab 特意留下的线索。phpinfo 为什么会泄露密钥phpinfo()是 PHP 的诊断函数可以输出当前 PHP 环境、配置、模块、请求信息和环境变量。本 Lab 将 Cookie 签名密钥保存在服务器进程的环境变量SECRET_KEY调试页面调用phpinfo()后环境变量被直接输出到 HTTP 响应中td classeSECRET_KEY/td td classv.../td所以攻击者不是根据 HMAC 结果“破解”了密钥而是开发者注释泄露调试页面路径 → 请求 phpinfo 页面 → phpinfo 输出服务器环境变量 → HTTP 响应直接泄露 SECRET_KEYHMAC 算法本身没有被绕过。失效的是密钥保密性。只要密钥仍然保密攻击者无法根据一个 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 配置适当的ObjectInputFilterPHP 谨慎使用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。