JNDI注入攻击原理与利用技术详解
前言
JNDI(Java Naming and Directory Interface)注入是一种常见的Java安全漏洞,攻击者通过操纵JNDI查找操作来执行恶意代码。本文将全面分析JNDI注入的攻击原理、利用方式以及不同JDK版本的防御措施。
JNDI注入基本概念
JNDI注入主要通过RMI和LDAP两种协议进行利用,分为三种主要利用方式:
- 远程类加载:RMI/LDAP请求VPS远程加载恶意class,不需要本地依赖
- 直接反序列化:RMI/LDAP请求VPS直接反序列化gadgets执行代码
- 本地工厂类调用:RMI/LDAP请求VPS调用本地工厂类来执行代码
注意:RMI client和server之间确实是通过序列化传输数据的,但LDAP不是,LDAP使用标准协议传输。
LDAP注入分析
入口点分析
LDAP注入的入口在LdapCtx#c_lookup方法,通过this.doSearchOnce请求LDAP server获取LdapResult。
LdapResult可能包含以下属性(忽略大小写):
javaSerializedDatajavaClassName
关键属性解析
JNDI定义了以下关键属性:
JAVA_ATTRIBUTES = [
"objectClass", // 0
"javaSerializedData", // 1
"javaClassName", // 2
"javaFactory", // 3
"javaCodeBase", // 4
"javaReferenceAddress",// 5
"javaClassNames", // 6
"javaRemoteLocation" // 7
]
JAVA_OBJECT_CLASSES = [
"javaContainer", // 0
"javaObject", // 1
"javaNamingReference", // 2
"javaSerializedObject",// 3
"javaMarshalledObject" // 4
]
三种利用分支
com.sun.jndi.ldap.Obj.class#decodeObject方法有三种处理分支:
static Object decodeObject(Attributes var0) throws NamingException {
String[] var2 = getCodebases(var0.get(JAVA_ATTRIBUTES[4])); // javaCodeBase
try {
Attribute var1;
// 分支1: javaSerializedData
if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
ClassLoader var3 = helper.getURLClassLoader(var2);
return deserializeObject((byte[])((byte[])var1.get()), var3);
}
// 分支2: javaRemoteLocation
else if ((var1 = var0.get(JAVA_ATTRIBUTES[7])) != null) {
return decodeRmiObject((String)var0.get(JAVA_ATTRIBUTES[2]).get(),
(String)var1.get(), var2);
}
// 分支3: objectClass
else {
var1 = var0.get(JAVA_ATTRIBUTES[0]);
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) &&
!var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ?
null : decodeReference(var0, var2);
}
} catch (IOException var5) {
NamingException var4 = new NamingException();
var4.setRootCause(var5);
throw var4;
}
}
分支1:反序列化利用
当属性包含javaSerializedData时进入此分支:
if ((var1 = var0.get(JAVA_ATTRIBUTES[1])) != null) {
ClassLoader var3 = helper.getURLClassLoader(var2);
return deserializeObject((byte[])((byte[])var1.get()), var3);
}
关键点:
getURLClassLoader会检查trustURLCodebase系统属性trustURLCodebase默认值为false(高版本JDK限制)
利用条件:
- LDAP server需要返回两个属性:
javaClassName:任意值javaSerializedData:存储反序列化利用链
分支2:javaRemoteLocation
此分支实际利用价值有限,因为无法初始化classFactory和classFactoryLocation。
分支3:引用类远程加载
当objectClass属性值为javaNamingReference时进入此分支:
var1 = var0.get(JAVA_ATTRIBUTES[0]); // objectClass
return var1 == null || !var1.contains(JAVA_OBJECT_CLASSES[2]) &&
!var1.contains(JAVA_OBJECT_CLASSES_LOWER[2]) ?
null : decodeReference(var0, var2);
decodeReference方法会:
- 获取
javaFactory值生成Reference对象 - 通过
DirectoryManager.getObjectInstance进行实例化 - 调用
NamingManager.getObjectFactoryFromReference获取ObjectFactory对象
利用条件:
- LDAP server需要返回以下属性:
javaClassName:任意值javaCodeBase:远程类加载地址(如http://x.x.x.x/)objectClass:固定值为javaNamingReferencejavaFactory:远程类加载的类名(如exp,需提供exp.class文件)
JDK高版本限制:
com.sun.jndi.ldap.object.trustURLCodebase默认为false
本地工厂类利用
当无法使用远程类加载时,可以寻找本地存在的工厂类:
- 类需实现
ObjectFactory接口 getObjectInstance方法中有可利用逻辑
常见可利用工厂类:
org.apache.naming.factory.BeanFactory(Tomcat容器)- 可配合
javax.el.ELProcessor#eval(String)使用 - 或
groovy.lang.GroovyShell#evaluate(String)(SpringBoot 1.2.x)
- 可配合
利用条件:
- 本地classpath中存在目标类
- 类有无参构造方法
- 有可执行代码的方法(仅接受一个String参数)
RMI注入分析
RMI与LDAP的区别
- RMI使用序列化传输数据,LDAP使用标准协议
- RMI存在"反制server端"的可能性
RMI利用方式
1. 直接反序列化
利用点:StreamRemoteCall.class#executeCall()
调用栈:
executeCall:252, StreamRemoteCall (sun.rmi.transport)
invoke:375, UnicastRef (sun.rmi.server)
lookup:119, RegistryImpl_Stub (sun.rmi.registry)
lookup:132, RegistryContext (com.sun.jndi.rmi.registry)
lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
lookup:417, InitialContext (javax.naming)
2. 引用类远程加载
类似LDAP方式,但仅适用于低版本(<8u113)
调用栈:
com.sun.jndi.rmi.registry.RegistryContext#lookup
sun.rmi.registry.RegistryImpl_Stub#lookup
com.sun.jndi.rmi.registry.RegistryContext#decodeObject
javax.naming.spi.NamingManager#getObjectInstance
javax.naming.spi.NamingManager#getObjectFactoryFromReference
高版本限制:
com.sun.jndi.rmi.object.trustURLCodebase默认为false(JDK 6u132、7u122、8u113开始)
3. 动态类加载
RMI核心特性之一,但限制严格:
- 需要安装
RMISecurityManager并配置java.security.policy java.rmi.server.useCodebaseOnly必须为false(JDK 6u45、7u21开始默认为true)
4. 本地工厂类
原理与LDAP相同,调用NamingManager#getObjectFactoryFromReference获取工厂类后触发。
版本限制总结
| 利用方式 | LDAP限制版本 | RMI限制版本 |
|---|---|---|
| 远程类加载 | <8u191 | <8u113 |
| 直接反序列化 | 无版本限制 | 无版本限制 |
| 本地工厂类 | 无版本限制 | 无版本限制 |
关键版本变更:
- JDK 5U45、6U45、7u21、8u121:
java.rmi.server.useCodebaseOnly默认为true - JDK 6u132、7u122、8u113:
com.sun.jndi.rmi.object.trustURLCodebase默认为false - JDK 11.0.1、8u191、7u201、6u211:
com.sun.jndi.ldap.object.trustURLCodebase默认为false
防御建议
- 升级JDK到最新版本
- 设置系统属性:
com.sun.jndi.ldap.object.trustURLCodebase=falsecom.sun.jndi.rmi.object.trustURLCodebase=falsejava.rmi.server.useCodebaseOnly=true
- 避免使用不可信的JNDI查找
- 检查并移除危险的本地工厂类