JNDI 注入原理解析

发布于:2025-05-08 ⋅ 阅读:(23) ⋅ 点赞:(0)

JNDI基础
概述

JNDI(Java Naming and Directory Interface)是Java提供的标准命名和目录接口,通过统一的API使应用程序能够访问各种命名和目录服务。它允许开发人员以统一的方式查找和访问资源,如用户、网络、机器和服务等。另一方面,Java通过JNI(Java Native Interface)实现与C/C++的互操作,弥补了自身在代码安全性和内存操作等方面的不足。尽管JNI功能强大,但自JDK 1.1起便存在,因此在使用时需特别注意可能带来的安全问题。

fb9d120d-81b6-41fe-9b3f-a2ac548a217c

JNDI SPI

SPI(Service Provider Interface,服务供应接口) 是 JNDI 中的一个关键组件,主要作用是为底层的具体目录服务提供统一的接入接口,从而实现目录服务的可插拔式安装。通过 SPI,不同的目录服务实现可以无缝集成到 JNDI 架构中,而无需修改应用层代码。

在 JDK 中,已内置支持多种常见的目录服务,包括:

  • RMI(Java Remote Method Invocation):Java 远程方法调用;
  • LDAP(Lightweight Directory Access Protocol):轻量级目录访问协议;
  • CORBA(Common Object Request Broker Architecture):通用对象请求代理架构,用于 COS 名称服务(Common Object Services);
  • DNS:域名服务。

此外,用户还可以从 Java 官网下载其他目录服务实现。由于 SPI 提供了统一的接口标准,第三方厂商也可以开发自己的私有目录服务实现,并轻松集成进系统中,提升了应用的灵活性和可扩展性。

命名服务

命名服务(Naming Server)是一种通过名称查找实际对象的服务。例如,RMI协议可以通过名称查找并调用远程对象,DNS协议则通过域名查找对应的IP地址。这些服务都属于命名服务。

在命名服务中,有几个关键概念:

Bindings:表示名称与对象之间的绑定关系。例如,在DNS中,域名与IP地址绑定;在RMI中,远程对象与名称绑定;在文件系统中,文件名与文件内容绑定。

Context:上下文表示一组名称到对象的绑定关系。一个上下文可以用于查找对应的对象,例如在文件系统中,一个目录就是上下文,可以在其中查找文件,子目录也可以看作子上下文(SubContext)。

References:对于无法直接存储的对象,它们通常通过引用的形式存储,类似于C/C++中的指针。引用包含获取实际对象所需的信息。例如,在文件系统中,通过文件描述符(fd)来引用文件,内核使用该引用找到磁盘中的文件。

目录服务

目录服务(Directory Service)是命名服务的扩展。除了名称到对象的映射,目录服务还允许对象具有属性(Attributes)。因此,目录服务不仅支持根据名称查找对象并获取其属性,还支持根据属性值进行搜索。

一些常见的目录服务包括:

  • LDAP:轻型目录访问协议
  • Active Directory:为Windows域网络设计,提供多个目录服务,如域名服务和证书服务
  • 其他基于X.500标准实现的目录服务
JNDI演示
查询 DNS 服务

该程序通过 JNDI 查询 DNS 服务,获取 example.com 的 A 记录(IPv4 地址)。它配置了 JNDI 环境,使用 DNS 服务器地址 114.114.114.114,并通过 DirContext 查询指定域名的 A 记录,最后输出查询结果。

// DNSClientSimple.java
import javax.naming.Context;
import javax.naming.directory.*;
import java.util.Hashtable;

public class DNSClientSimple {
    public static void main(String[] args) {
        // 设置 JNDI 环境配置
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.dns.DnsContextFactory");
        env.put(Context.PROVIDER_URL, "dns://114.114.114.114");  // 使用 DNS 服务器地址

        try {
            // 创建 DirContext
            DirContext ctx = new InitialDirContext(env);

            // 查询指定域名的 A 记录(IPv4 地址)
            Attributes res = ctx.getAttributes("example.com", new String[] {"A"});

            // 输出查询结果
            System.out.println(res);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
查询 LDAP 服务条目
// Client.java
import javax.naming.Context;
import javax.naming.NamingException;
import javax.naming.directory.*;
import java.util.Hashtable;

public class Client {
    public static void main(String[] args) {
        // 配置 JNDI 环境
        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, "ldap://localhost:8080");  // LDAP 服务器地址

        try {
            // 创建 DirContext 实例
            DirContext ctx = new InitialDirContext(env);

            // 查找指定的 LDAP 条目
            DirContext lookCtx = (DirContext) ctx.lookup("cn=john,ou=employees,dc=example,dc=com");

            // 获取该条目的属性
            Attributes res = lookCtx.getAttributes("");

            // 输出查询结果
            System.out.println(res);
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

这个程序通过 JNDI 查询 LDAP 服务器,查找特定条目 cn=john,ou=employees,dc=example,dc=com 的属性。程序先配置了 JNDI 环境,指定了 LDAP 服务器的地址(ldap://localhost:8080)。然后创建一个 DirContext 实例,并使用 lookup() 方法查找指定的 LDAP 条目。最后程序获取该条目的所有属性并输出。该程序实现了通过 JNDI 查询 LDAP 服务并检索指定员工条目的相关信息。

动态协议切换
// JNDIDynamicLDAP.java
import javax.naming.*;
import javax.naming.directory.*;
import java.util.Hashtable;

public class JNDIDynamicLDAP {
    public static void main(String[] args) {
        if (args.length != 1) {
            System.out.println("Usage: lookup <username>");
            return;
        }

        Hashtable<String, String> env = new Hashtable<>();
        env.put(Context.INITIAL_CONTEXT_FACTORY, "com.sun.jndi.ldap.LdapCtxFactory");
        env.put(Context.PROVIDER_URL, "ldap://localhost:389");

        try {
            DirContext ctx = new InitialDirContext(env);
            DirContext lookCtx = (DirContext) ctx.lookup("cn=" + args[0] + ",ou=employees,dc=example,dc=com");
            System.out.println(lookCtx.getAttributes(""));
        } catch (NamingException e) {
            e.printStackTrace();
        }
    }
}

这个程序的目的是通过用户输入的域名,动态查询并获取该域名的相关信息。用户在命令行中提供一个域名(比如 example.com),程序将使用 JNDI 查询该域名的 DNS 记录,并输出其 A 记录(即 IPv4 地址)。这样,用户可以根据输入的不同域名获取相应的 DNS 信息。

我们还可以通过在查找参数中直接指定协议,实现协议的动态切换。

java JNDIDynamic "ldap://localhost:389/cn=alice,ou=users,dc=example,dc=com"

这正是 JNDI 注入漏洞的根本原因所在。攻击者可以构造恶意的服务端响应,引导客户端通过 JNDI 加载并解析远程代码,从而实现远程命令执行(RCE)。JDK 默认支持多种协议的自动识别与切换,并为每种协议提供了对应的工厂类,例如:

  • LDAPcom.sun.jndi.ldap.LdapCtxFactory
  • RMIcom.sun.jndi.rmi.registry.RegistryContextFactory
  • DNScom.sun.jndi.dns.DnsContextFactory
  • CORBAcom.sun.jndi.cosnaming.CNCtxFactory
JNDI 注入漏洞

JNDI 注入(JNDI Injection) 是一种利用 Java Naming and Directory Interface(JNDI)接口的漏洞进行攻击的技术。攻击者通过恶意构造 JNDI 查询,尝试通过 JNDI 访问应用程序的命名服务或目录服务,通常用于获取敏感信息、执行远程代码或进行拒绝服务攻击(DoS)。


网站公告

今日签到

点亮在社区的每一天
去签到