JGD:面向 Java 反序列化 Gadget Chain 的自主挖掘 Agent
原创 侠盗鲁平 2026-09-28 06:57 日本

面向 Java 反序列化 Gadget Chain 的自主挖掘 Agent
GitHub: https://github.com/lupingQAQ/JGD
WeChat: IndexSec
从"发现了可疑类"到"拿到可投递的 payload"——解决最后一公里。
0x00 背景
Java 反序列化漏洞依旧是 Java 安全领域长期高危的漏洞类型之一。与其它漏洞类型不同,Java 反序列化利用的核心不在于漏洞入口点,而在于环境中的依赖——Gadget Chain。
Gadget Chain 由三个部分组成:
Source(入口):反序列化时自动调用的方法,如
readObject、hashCode、toString、equalsGadget(跳板):链中间的转发类,通过字段引用将调用传递给下一跳
Sink(执行点):最终触发危险行为的方法,如
Runtime.getRuntime().exec(...)、TemplatesImpl.newTransformer()(内部经defineClass加载攻击者字节码)
术语约定:source(入口触发方法,如 readObject/hashCode;承载 source 方法的链首类称触发类,如 BAVE/HashMap)、gadget(中间跳板类)、sink(执行点方法)。文中"sink 侧"指到达执行点所需的一组类(如 ROME + TemplatesImpl)。
当前工具生态存在明显断层:
工具 | 定位 | 局限 |
|---|---|---|
ysoserial | 预置 Payload 生成 | 定位为 Payload 生成器,仅覆盖已知链 |
GadgetInspector (Netflix, DEF CON 26) | 静态污点分析自动挖掘 | 2018 年后停更,不支持 JDK 9+ 模块化,误报率高 |
JDD (BlackHat Asia 2025) | 片段化搜索 + 数据流辅助 | 学术原型,部署复杂,仅支持 Soot 分析框架 |
ysogate / javaserializetools | Payload 生成增强 | 本质仍是已知链的组合变换 |
核心矛盾:静态分析工具(GadgetInspector)能找到"可能的链"但无法验证;动态利用工具(ysoserial)只覆盖已知链但无法发现新的。中间缺少一个自动化环节——把静态候选推进到动态验证、再产出可投递 PoC 的端到端验证链路。
JGD(JavaGadgetDigger)针对的正是这一断层。这里的 Agent 指 LLM 驱动的自动化分析流水线:静态分析、动态验证、结论审计全部内化,输入 JAR 目录即产出链与 Payload,中间无需人工决策。
0x01 整体架构
JGD 采用四层分析管线(LLM 驱动,发现与验证由两个独立模型担任),单命令运行:
输入: 任意 JAR 目录
│
├─ ① 已知链判定(签名匹配 + 版本门控 + 动态复验)
├─ ② 中间 gadget 发现(字节码反汇编 + 操作数栈符号执行)
├─ ③ 链配对(多触发类 × 字段填充 × 批处理动态验证)
└─ ④ PoC 生成(sink 侧候选循环 + RCE 端到端触发验证)
│
输出: 已知链四态报告 + 未公开链清单 + 序列化 Payload (.ser)「四态」为已知链判定的四个递进状态:PRESENT(类在场)→ VERSION(版本适用)→ ASSEMBLE(可组装)→ FIRE(可触发)。
每一层的产出都经过双模型交叉验证(发现者 ≠ 验证者),终局判定经独立验收——这是 JGD 与纯静态工具的根本区别:中间结论不被默认信任,必须通过交叉验证才能进入报告。
0x02 静态分析
2.1 字节码反汇编
JGD 内置纯 Python 的 JVM class 文件解析器(bcdisasm.py),不依赖 JDK、不启动 JVM、不受目标 class 版本限制。支持解析常量池、字段描述符(含 static/final/transient 访问标志)、以及逐条字节码指令的精确解码(含变长分支指令的长度计算)。
这解决了一个 GadgetInspector 的固有问题:它依赖 ASM 框架,而 ASM 对高版本 class 文件(Java 17+, major version 61+)的兼容性更新滞后。
2.2 操作数栈符号执行
中间 gadget 检测的核心问题是:如何判断 a.hashCode() 中的 a 是当前对象的字段还是方法参数?
JVM 是栈机器,aload_0; getfield name; invokevirtual hashCode 与 aload_1; invokevirtual hashCode 在字节码层面只差一条指令,但语义完全不同——前者是字段转发(对象来自自身字段,反序列化后攻击者可控,构成中间 gadget),后者是参数转发(对象来自调用方传参,不落入序列化状态,不构成 gadget)。
JGD 实现了一个轻量级的操作数栈符号模拟器,追踪每条指令对栈的影响:
aload_0 → [THIS]
getfield name → [FIELD:name] ← 栈顶标记为字段
invokevirtual equals → 接收者是 FIELD:name ✓ 字段转发通过这种方式,JGD 区分出两种转发模式:
接收者位转发(receiver-side forwarding):转发字段作为下一跳方法调用的接收者(receiver),即经典的字段转发模式:
// NamePrincipal.equals()
returnthis.name.contentEquals(that.getName());
// 字节码: aload_0; getfield name → 栈顶为 FIELD:name → contentEquals 的接收者参数位转发(argument-side forwarding):转发字段作为参数流入下一跳方法,并在该方法内部被分派调用。公开资料中尚未见现有工具覆盖此类模式:
// antlr4 Pair.hashCode()
return MurmurHash.update(MurmurHash.update(0, a), b);
// 字节码: aload_0; getfield a → 作为 invokestatic MurmurHash.update 的参数流入
// MurmurHash.update 内部对参数调用 hashCode() → 触发分派参数位转发在 antlr4 Pair、Artemis ObjLongPair 等类中大量存在——这些类正是 JGD 发现的多条未公开链的中间 gadget(见 0x06)。
2.3 调用图与 CHA 分派边
JGD 按目标依赖集合(target classpath,即目标 JAR 集合)实时构建方法级调用图,并补全 CHA(Class Hierarchy Analysis)分派边:
接口方法节点 (Transformer, transform)
│ CHA 边
▼
实现类节点 (InvokerTransformer, transform)没有 CHA 边的调用图在接口分派处断裂——例如 x.transform(input) 只会连到 (Transformer, transform) 接口节点,而真正的 Sink 路径在 (InvokerTransformer, transform) 实现节点上。反序列化链的本质恰恰就是多态分派,这使得 CHA 边成为不可或缺的补充(GadgetInspector 的原始实现同样存在此问题,su18 的改造中也有提及)。
2.4 分析目标隔离
JGD 对所有中间产物(调用图、RAG 向量库、状态文件、审计结果、PoC 目录)实施分析目标隔离——切换目标 JAR 目录时,旧目标的全部状态自动归档,新目标从零开始。这避免了跨目标状态污染,也是 JGD"输入任意 JAR 目录即可产出结果"的工程前提。
0x03 动态验证
3.1 五层递进探针体系
探针 | 功能 | 关键技术 |
|---|---|---|
DynV | 类可实例化/可序列化验证 | 构造器 → ReflectionFactory → Unsafe 三级降级 |
PV | 字段转发验证(单跳) | 反射注入 canary 对象(哨兵对象)+ Proxy 注入接口字段 |
CV | 完整链 PoC(单链打通,多跳) | BadAttributeValueExpException(BAVE)触发类全回环(JDK ≤ 11) |
CC | 批处理全链配对(一对多批量) | 单个 JVM 验证一个中间 gadget × 全部下游 gadget,五种触发类切换 |
PoC | 可投递 Payload | TemplatesImpl sink 侧 + 本地触发验证 |
3.2 五种触发类(source 类)
JGD 实现五种反序列化触发类(source 类,链的入口),覆盖全部经典触发方法(toString / hashCode / equals / compare / compareTo)与不同 JDK 版本:
触发类 | 触发方法 | JDK 约束 | 说明 |
|---|---|---|---|
BAVE | readObject → val.toString() | ≤ 11 | JDK 17 起 |
HashMap | readObject → rehash → hashCode() | 无限制 | 不依赖任何特定类,任何 JDK 均可触发 |
HashMap_EQ | 双实例同哈希 → | 无限制 | 构造两个等哈希的中间 gadget 实例,readObject 重哈希时触发 |
PriorityQueue | readObject → heapify → compare()/compareTo() | 无限制 | 桥类作为 Comparator(比较器)或 Comparable(元素)入队,堆重建时分派——覆盖 CC2/CB1 型 Comparator 触发链族 |
TreeMap | readObject → 重建 put → compare()/compareTo() | 无限制 | 桥类作为比较器或 Comparable key,反序列化重放 put 时触发比较分派 |
触发类覆盖的意义:不同中间 gadget 的触发方法不同——MutableObj 只在 toString 上转发(仅 BAVE 触发类可达),ObjLongPair 只在 hashCode 上转发(仅 HashMap 触发类可达),而 CC2/CB1 型链族的桥类在 compare/compareTo 上转发(仅 PriorityQueue/TreeMap 载体可达)。如果只测试一种触发类,另一半中间 gadget 会被系统性漏检。至此五类经典触发路径全部纳入批处理验证管线。
3.3 字段填充(field population)
动态探针的主要假阴性来源是空状态 NPE——中间 gadget 对象的字段全是 null,toString 或 hashCode 一调就空指针异常,探针直接失败。
JGD 的 survival() 函数为每个非转发字段注入类型匹配的非空占位值(type-matched placeholder):
字段类型 | 注入值 | 目的 |
|---|---|---|
Object | 下游 gadget 对象 / canary 对象 | 触发转发 |
String | Canary 字符串 | 数据流追踪(异常消息回显) |
接口/抽象类 | Proxy.newProxyInstance(...) | 动态代理分派 |
数组 | Array.newInstance(componentType, 0) | 避免遍历 NPE |
具体类 | ReflectionFactory 裸实例 | 避免字段访问 NPE |
java.net.URL | http://canary.invalid/x | JNDI/HTTP 类 sink 的数据流追踪 |
实测效果:字段填充前 carrier_fail 占 90%(6926/7721 对),填充后基本清零——大量原本被判定为"不可利用"的中间 gadget 实际可用。
3.4 三类检测信号(evidence channels)
JGD 的动态观测不依赖 JVM 插桩(无 javaagent/ClassFileTransformer),而是通过三类正交的检测信号获取证据。记两跳为:第一跳 hop1(触发类 → 中间 gadget)、第二跳 hop2(中间 gadget → 下游 gadget)。
信号 1:canary 可达性(hop2)
canary 对象被注入到下游 gadget 的字段中。在序列化对象图中,canary 的唯一可达路径是 触发类 → 中间 gadget → 下游 gadget → 字段。因此 canary 的方法被调用即证明两跳全通——即使没有异常抛出(链走通时恰好无异常无栈帧,这是早期版本的一个回归教训)。
信号 2:异常栈帧解析(hop1)
反序列化抛异常时,解析输出中的 FRAME 行。中间 gadget 类名出现在栈帧中即 hop1 分派发生的直接证据,且可识别具体的分派方法名。
信号 3:canary 调用者栈
canary 的方法体内记录 new Throwable().getStackTrace() 并写入文件,直接给出完整的调用链:
HashMap.put → HashMap.hash
→ ObjLongPair.hashCode(ObjLongPair.java:55)
→ Objects.hash → Arrays.hashCode
→ EqualsBean.hashCode → EqualsBean.beanHashCode
→ ObjectBean.toString → ToStringBean.toString → Method.invoke
→ TemplatesImpl.defineClass → payload static block这是 JGD 最强的证据形式——不需要任何推测,直接展示从入口到执行的每一帧。
0x04 加速
优化 | 原理 | 效果 |
|---|---|---|
批处理复用 | 一个 JVM 跑完一个中间 gadget 的全部 42 个下游 gadget(而非每对启动一个) | JVM 启动 42× 减少 |
最小 classpath | 只挂载本批实际用到的 jar(通配符展开 828 jar 需 ~30s/次) | JVM 启动 30s → <2s |
dispatch 集合缓存 | 381 个中间 gadget 只有 ~20 种唯一的 dispatch 方法集,图扫描按集缓存 | 每轮扫描 1-2h → 秒级命中 |
4 线程并行 | 跨(中间 gadget × 触发类)并行执行批处理 | 吞吐 4× 提升 |
综合效果:同为约 9 分钟窗口(单机 4 线程),处理对数 126 → **1500+**(12×)
PoC 生成阶段采用链间 3 线程并行 + -Xmx256m 内存治理:54.4s → 20.3s(2.7×),全量运行 OOM = 0。
0x05 交叉验证
JGD 的审计层采用发现者 ≠ 验证者原则——发现侧与验证侧各使用一个 LLM,验证侧可以否决或降级发现侧的结论:
发现侧(选候选)→ 动态探针 → 验证侧交叉验证(可否决/降级)
↓ 被否决?
发现侧辩护 → 分歧标记 DISPUTED
↓ 终局判定
验证侧验收(拒收 → 自动延长轮次继续挖掘)项目迭代中,验证侧的每次拒收都驱动了实质性修复:
验证侧拒收理由 | 驱动的修复 |
|---|---|
hop2 canary 触发但无栈帧证据 | canary 增加调用者栈自打印(R29) |
覆盖统计自相矛盾 | 逐 gadget 账本 pool_size/pairs_tried/pairs_total(R35) |
"声明不等于证据" | 性能回归数据 + Maven sha1 校验 + OOM 观测(R52) |
探针有系统性盲区 | 反射 API 种子 + equals 触发类 + 类型化 canary + 深度 4(R47) |
这套机制确保 JGD 的结论不依赖任何单一模型的自我判断——交叉验证未通过的结果不会进入最终报告。
0x06 发现的新链
以下链均在 2026 最新版 Apache 系目标语料上完成动态验证与 RCE 端到端演示。按新颖性分三档:全新入口类、未公开中间跳板、已知模式的新变体(仓库内以 T1/T2/T3 标记)。
全新入口类(T1)
ObjLongPair → HashMap 链
HashMap.readObject() → rehash → hash(key)
→ ObjLongPair.hashCode() // Artemis, class file v61(Java 17)
→ Objects.hash(first, second)
→ first.hashCode() // Object 字段, isInstance 恒真
→ EqualsBean.hashCode() → beanHashCode()
→ ObjectBean.toString() → ToStringBean.toString()
→ Templates.getOutputProperties()
→ TemplatesImpl.newTransformer()
→ defineClass() → payload static block → RCE新颖性分析:
入口类
org.apache.activemq.artemis.api.core.ObjLongPair,据我们所知未见于 ysoserial、GadgetInspector、JDD 及公开 CVE writeup与已知 ROME 入口范式(直接 ROME key / BAVE / HotSwappableTargetSource / XString)正交
经验证侧交叉确认
entry_novel=true, tier=T1不依赖 BAVE(JDK 17 可用),
first为Object类型——攻击者可注入任意可序列化对象
利用条件:任意反序列化漏洞 + Artemis ≥ 2.44 + ROME + JDK ≥ 17
未公开中间跳板(T2)
MutableObj 链(hutool-core)
BAVE.readObject() → val.toString()
→ MutableObj.toString() // hutool, 未公开中间跳板
→ value.toString() // Object 字段
→ ObjectBean.toString() → ToStringBean → getters
→ TemplatesImpl.defineClass() → RCEhutool-core 在国内 Java 项目中出现率极高,MutableObj 是其中最基础的泛型可变值容器,公开资料未见其被列入任何 gadget 黑名单。
antlr4 Pair 链
BAVE.readObject() → val.toString()
→ Pair.toString() // antlr4-runtime, 未公开中间跳板
→ String.format → a.toString() // 两个 Object 字段(参数位转发)
→ ObjectBean.toString() → ToStringBean → getters
→ TemplatesImpl.defineClass() → RCEantlr4-runtime 覆盖面极广:Hibernate(HQL 解析)、Spark SQL、graphql-java、AWS SDK 解析器等。Pair 是其中最高频的工具类之一。
Artemis 配置类链族(4 条)
FederationConfiguration、FederationAddressPolicyConfiguration、FederationQueuePolicyConfiguration、BroadcastGroupConfiguration——这四条是同一模式在 Artemis 配置类族上的批量验证,全部经 HashMap 触发类触发 hashCode → Object 字段转发 → ROME sink 侧 → RCE 端到端验证。
已知模式的新变体(T3)
EWAHCompressedBitmap 链(JavaEWAH / Lucene 生态)
hashCode → rlw.getRunningBit() / buffer.getWord(i)——分派深度浅(目标方法是原始类型 getter),单链价值有限。但 JavaEWAH 是 Lucene/Solr/Elasticsearch 的传递依赖,其字段可被攻击者控制这一事实本身具有防御参考价值。
覆盖统计
指标 | 数值 |
|---|---|
配对覆盖 | 4587/4587 = 100%(逐 gadget 账本可复算) |
RCE 端到端 PoC | 5 条(构建阶段与反序列化阶段分别触发) |
机制完整链 | 425 条 DEPTH2(含 equals 触发类新解锁 212 条) |
数据流候选 | 84 条 VALUE_FLOW |
证据 | 141 条以上(经交叉验证) |
0x07 使用方式
获取与配置
git clone https://github.com/lupingQAQ/JGD.git
cp config/models.example.json config/models.json
# 填入你的双模型 API 配置(任意 OpenAI/Anthropic 兼容端点)双模型交叉验证需要 API Key,请先完成配置再运行完整管线。
CLI 模式(批量审计)
python3 audit_target.py --target /path/to/jars --name "your-product"TUI 模式(交互式可视化)
python3 tui.py --lang zh支持链树浏览、逐链详情(含完整分派栈)、中英文切换。
0x08 与现有工具对比
维度 | ysoserial | GadgetInspector | JDD | JGD |
|---|---|---|---|---|
新链发现 | No | 静态候选(误报需人工筛) | 静态+模糊测试 | 静态+动态完整验证 |
动态验证 | No | No | Partial | Yes(五层探针) |
PoC 生成 | 预置模板 | No | No | Yes(自动生成 .ser) |
已知链判定 | No | No | No | Yes(四态判定 + 版本门控) |
交叉验证 | No | No | No | Yes(双模型交叉 + 验收) |
JDK 17+ | Partial | No | No | Yes |
部署复杂度 | 低 | 中 | 高(Soot) | 低(纯 Python + JDK) |
中间决策 | N/A | 人工审查 | 人工审查 | 零 |
0x09 局限与声明
由 Rice 定理,"对任意程序判定其是否含可利用链"不可判定;因此 JGD 只声明目标依赖集合内、覆盖账本逐 gadget 可复算的穷尽
非 JVM 插桩(无 javaagent),观测限于 canary 方法 + 异常栈帧 + 类加载日志
触发类家族已全覆盖(toString / hashCode / equals / compare / compareTo 五类载体);当前语料中 compare 型载体未命中匹配桥类,机制已经独立验证
PoC payload 仅写
/tmp标记文件,无破坏动作
仅用于合法安全研究、教育和授权渗透测试。
参考
GadgetInspector — Netflix, DEF CON 26
JDD — BlackHat Asia 2025, Fudan Secsys Lab
GadgetInspector 改造 — su18
本文转载自微信公众号「Security丨Art」,查看原文。版权归原作者所有,如有侵权请联系删除。