JNDI 和反序列化都不是内存马类型,它们只是可能取得初始代码执行的入口。真正的驻留仍然发生在 Filter、Servlet、Valve、Handler、Controller、线程或 JVM 字节码中。
1 | 外部输入 |
只证明 lookup() 被调用,不能证明代码已经执行;只发现一个异常 Filter,也不能反推出入口一定是 JNDI。入口与驻留必须分别取证。
本文用于代码审计和应急分析,不提供远程利用服务、攻击载荷或命令执行代码。
一、入口与驻留为什么必须拆开
一条完整链至少包含六个独立阶段:
| 阶段 | 需要证明的事实 | 主要证据 |
|---|---|---|
| 1. 输入进入 | 哪个参数、Header、消息或配置可控 | 原始请求、消息体、调用日志 |
| 2. 危险解析 | 输入是否到达 lookup 或反序列化 API | 调用栈、断点、审计日志 |
| 3. 对象解析 | Provider 返回什么,如何转换为对象 | DNS/LDAP/RMI、ObjectFactory、过滤器日志 |
| 4. 代码执行 | 哪个类、方法或 Gadget 真正运行 | 类加载事件、线程栈、异常、JFR |
| 5. 运行时注册 | 新对象挂入哪条请求链 | Filter/Servlet/Handler/Mapping 注册表 |
| 6. 长期持有 | 谁持续引用它,是否存在复活点 | 堆引用、ClassLoader、线程、Agent、启动项 |
这六段不会必然全部出现。例如反序列化 Gadget 可能不经过 JNDI;JNDI lookup 也可能只返回一个普通数据对象;代码执行后可能只完成一次操作,没有注册任何驻留组件。
分析时应分别命名:
1 | 植入入口:最初如何获得执行能力 |
删除驻留对象只能解决当前请求入口;修复反序列化漏洞也不会自动移除 JVM 中已经存在的 Handler 或已修改字节码。
二、JNDI lookup 后面到底发生什么
JNDI 是命名与目录服务的统一 API。InitialContext.lookup(name) 只是入口,实际行为由 URL Scheme、Naming Provider、返回对象和对象工厂共同决定。
1 | lookup(name) |
常见分支:
| 返回结果 | 后续行为 | 关键限制 |
|---|---|---|
| 普通对象或属性 | 直接返回调用方 | 通常没有代码加载 |
Reference |
解析工厂类与地址 | 工厂类来源、过滤规则、类加载器 |
本地 ObjectFactory |
调用目标类路径中已有工厂 | 本地依赖是否存在、工厂过滤器 |
| 序列化对象 | 进入对象恢复过程 | JDK 过滤器、Provider 与可用类 |
| 远程 Codebase | 尝试取得外部类定义 | 现代 JDK 更新默认严格限制 |
因此,“关闭远程 Codebase”只切断其中一条分支,不代表本地 ObjectFactory、已有依赖链或危险业务工厂全部安全。真实结果取决于:
1 | JDK 完整更新号 |
代码审计不要只搜索字符串 ldap://。应从数据流反向寻找:
1 | InitialContext.lookup |
安全验证可以使用本机不可达地址,只确认是否发生 lookup 和出站尝试,不返回任何对象:
1 | import javax.naming.InitialContext; |
该探针预期在 1 秒左右连接失败,只用于在隔离环境观察 Socket、JFR 网络事件和调用栈。不要把任意外部地址带入生产 JVM。
三、反序列化与 JSON 类型恢复的真实边界
“反序列化”不只指 ObjectInputStream。只要解析器根据输入选择类型、创建对象并调用构造方法、Setter、readObject() 或其他回调,就可能形成从数据到代码的执行路径。
1 | 输入数据 |
需要区分三类机制:
| 类型 | 入口 | 主要控制点 |
|---|---|---|
| Java 原生序列化 | ObjectInputStream.readObject() |
ObjectInputFilter、允许类型、深度与大小 |
| JSON 多态类型 | @type、默认类型或类型元数据 |
解析器版本、AutoType/SafeMode、允许列表 |
| 框架对象绑定 | Bean 属性、表达式、转换器 | 可绑定字段、目标类、转换回调 |
JDK 9 引入全局反序列化过滤机制,JDK 17 又增加上下文相关过滤能力,但过滤器必须真正配置并覆盖对应入口才有效。第三方 JSON 解析器有自己的类型策略,不能因为 JVM 配置了 jdk.serialFilter 就认为所有 JSON 多态解析都受控。
判断一条链是否成立,至少固定四组变量:
1 | 解析器:精确版本、调用 API、Feature 和类型策略 |
排错也应沿数据流推进:
1 | 类型是否被选择 |
HTTP 500 只能证明链路某处失败,不能直接说明“JDK 太高”或“Tomcat 不兼容”。
四、从入口日志追到最终驻留
应急调查应把不同时间线对齐:
1 | T0 外部请求或消息进入 |
每个时间点应保存:
- 原始请求、消息体和入口线程栈;
- DNS、代理、防火墙及主机网络连接记录;
- 类加载时间、ClassLoader、CodeSource 和 ProtectionDomain;
- JNDI Provider、对象工厂与反序列化过滤器配置;
- Servlet、Filter、Listener、Valve、Handler、Controller 和 Agent 清单;
- 同制品可信节点在相同时间窗口的对照结果。
查杀不能停在封禁某个 LDAP 地址。攻击者可以更换地址,已注册对象也不会因网络阻断自动消失。完整处置顺序是:
1 | 隔离受影响节点 |
最终结论必须同时回答两个问题:入口为什么能执行,以及执行后把什么对象挂到了哪里。任何一项没有证据,调查都还没有闭环。