一、JNDI 简介

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

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

二、JNDI 实现

三、JNDI 注入

3.1 分析漏洞如何产生

此处断点调试,跟进 lookup

InitialContext.lookup

GenericURLContext.lookup

RegistryContext.lookup

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

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

3.2 Jndi + RMI

复现:

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

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

服务端:

然后用客户端访问,

弹出计算器:

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

调试:

在客户端的 lookup 处断点

跟到 RegistryImpl_Stub 这里,

继续跟进

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

步入 decodeObject ,

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

继续跟进 getObjectInstance

这里使用强转将 refInfo 转为 Reference

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

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

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

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 的依赖。

Ldap 服务端:

代码搭建

客户端:

用 python 开一个服务监听 8000端口

接下来启动服务端,启动客户端,弹出计算器

使用 ApacheDirectoryStudio 搭建 LDAP 服务

注意:系统的 java 环境使用 jdk 11,jdk 8 的版本都运行不了 LDAP 环境!

新建一个 LDAP 服务,

这样就搭建成功了:

注意一点就是,LDAP+Reference的技巧远程加载Factory类不受RMI+Reference中的 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase等属性的限制,所以适用范围更广。但在JDK 8u191、7u201、6u211之后,com.sun.jndi.ldap.object.trustURLCodebase属性的默认值被设置为 false,对 LDAP Reference 远程工厂类的加载增加了限制。

所以,当JDK版本介于 8u191、7u201、6u211 与 6u141、7u131、8u121 之间时,我们就可以利用LDAP+Reference 的技巧来进行JNDI注入的利用。

因此,这种利用方式的前提条件就是目标环境的JDK版本在JDK8u191、7u201、6u211以下。

3.3 jndi 结合 CORBA

一个简单的流程是:resolve_str 最终会调用到 StubFactoryFactoryStaticImpl.createStubFactory 去加载远程 class 并调用 newInstance 创建对象,其内部使用的 ClassLoader 是 RMIClassLoader,在反序列化 stub 的上下文中,默认不允许访问远程文件,因此这种方法在实际场景中比较少用。所以就不深入研究了。

3.4 绕过 jdk 高版本

3.4.1 8u191 之前

这里的版本为 jdk 8u121 < temp < 8u191

这个之间版本绕过方法便是上文所述的 ldap 的 jndi 漏洞

3.4.2 8u191 之后

**8u191 之后,在使用 URLClassLoader 加载器加载远程类时,**通过添加 trustURLCodebase 的值是否为 true ,让我们无法加载 codebase,也就是无法进行 URLClassLoader 的攻击。

要想绕过就要找到这么一个类:

  • 服务端本地 ClassPath 中存在恶意 Factory 类可被利用来作为 Reference Factory 进行攻击利用
  • Factory 类必须实现 javax.naming.spi.ObjectFactory 接口,可利用该接口的 getObjectInstance() 方法

我们找到 org.apache.naming.factory.BeanFactory类,其满足上述条件并存在于 Tomcat8 依赖包中,应用广泛。该类的 getObjectInstance() 函数中会通过反射的方式实例化 Reference 所指向的任意 Bean Class(Bean Class 就类似于我们之前说的那个 CommonsBeanUtils 这种),并且会调用 setter 方法为所有的属性赋值。而该 Bean Class 的类名、属性、属性值,全都来自于 Reference 对象,均是攻击者可控的。

绕过一:利用本地恶意 Class

环境:

首先是 tomcat 环境,以下三个必须存在并且版本尽量选在 9.0.64 以前的,(9.0.64 以后的版本大多数漏洞都被修复了,不能利用)

源码复现:

参考:https://drun1baby.top/2022/07/28/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BJNDI%E5%AD%A6%E4%B9%A0/#2-jdk-%E7%89%88%E6%9C%AC%E5%9C%A8-8u191-%E4%B9%8B%E5%90%8E%E7%9A%84%E7%BB%95%E8%BF%87%E6%96%B9%E5%BC%8F

​

分析:

前面的流程还是进入 lookup 方法,到 RegistryContext 类的 decodeObject() 方法,这个方法当中调用了 getObjectInstance()。然后来到 getObjectFactoryFromReference 开始跟:

然后通过 loadClass 加载 org.apache.naming.factory.BeanFactory并赋值给 clas

将 clas 强转为 **ObjectFactory **类型

然后经过一系列复杂的赋值,最终在 ref 的 className 中获取到了 “javax.el.ELProcessor” ,classFactory 获取到了"org.apache.naming.factory.BeanFactory"

getObjectInstance

到了 getObjectInstance 之后便是整理变量,准备执行 invoke 方法

ra 通过利用 Java 的脚本引擎(JavaScript )在运行时构造并调用 ProcessBuilder,最终在目标主机上执行系统命令 calc,就是获取 beanClass 即 javax.el.ELProcessor 类的 eval() 方法并和 x 属性

可以看到这里的一个 value 中封装的就是恶意代码

最终代码在 method.invoke 处,通过method.invoke()即反射调用的来执行
"".getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("JavaScript").eval("new java.lang.ProcessBuilder['(java.lang.String[])'](['calc']).start()")。弹出计算器。

绕过二:LDAP返回序列化数据,触发本地Target

LDAP 服务端除了支持 JNDI Reference 这种利用方式外,还支持直接返回一个序列化的对象。如果 Java 对象的 javaSerializedData 属性值不为空,则客户端的obj.decodeObject() 方法就会对这个字段的内容进行反序列化。此时,如果服务端 ClassPath 中存在反序列化咯多功能利用 Gadget 如 CommonsCollections 库,那么就可以结合该 Gadget 实现反序列化漏洞攻击。

复现:

分析:

在这里下断点调试

首先还是经过 lookup 的方法调用

InitialContext.lookup

ldapURLContext.lookup

ldapURLContext.lookup

GenericURLContext.lookup

GenericURLContext.lookup

PartialCompositeContext.lookup.p_lookup

ComponentContext.p_lookup.c_lookup

LdapCtx.c_lookup

从上面是通过 p_lookup.c_lookup 进入到 decodeObject 中,这里是重点要关注的,

decodeObject

进入 decodeObject,先要进入一个 getURLClassLoader ,

getURLClassLoader 中的 trustURLCodebase 默认是 false ,不执行 newInstance 实例化,这里虽然已经获取到字节码了,只是不实例化就无法加载,也就无法命令执行。

接着往下走,来到了 deserializaObject ,

decodeObject.deserializaObject

而 deserializaObject 对象中恰好有 readObject ,字节码在此处被反序列化造成漏洞。

deserializaObject.readObject

参考文章:

https://drun1baby.top/2022/07/28/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BJNDI%E5%AD%A6%E4%B9%A0/#2-Jndi-%E7%BB%93%E5%90%88-ldap

http://101.36.122.13:4000/2025/03/08/JNDI%E4%B8%93%E9%A2%98/

https://www.bilibili.com/video/BV1ct4y1h79t