JNDI注入笔记
JNDI 的二三事
什么是 JNDI
数据库是一种资源,我们使用 JDBC 对数据库作包装,然后通过适当的 JDBC URL 连接到数据库
1 | DriverManager.getConnection("jdbc:mysql://127.0.0.1:3306?user=qsdz&password=qsdzyyds") |
文件也是一种资源,比如说
1 | "file:///home/qsdz/flag.txt" |
网络资源也是一种资源,比如说
1 | "http://www.baidu.com" |
Java 就是一门极其注重抽象的语言,那么我们是否可以对这些资源和资源的定位进行一种抽象?
答案是是的,JNDI 全称 Java Naming and Directory Interface,分为 Naming Server 和 Directory Server 两大部分。
套用在文件资源中,Naming Server 就是负责解析文件路径,获取文件对象的服务;Directory Server 就是负责获取文件数据、信息的服务。
套用在网络资源中,Naming Server 就是负责解析域名的系统,例如
DNS;Directory Server 就是负责访问具体网络资源的服务,例如
/index.html
,/../../../../etc/passwd
。
通俗易懂地说,JNDI 就是 Java 中负责资源管理的抽象接口,在这套系统中,我们可以给资源取一个名字,然后通过名字来查找资源,获取资源的属性。
SPI
JNDI 原生支持的一部分协议如下表所示
协议 | schema | Context |
---|---|---|
DNS |
dns:// |
com.sun.jndi.url.dns.dnsURLContext |
RMI |
rmi:// |
com.sun.jndi.url.rmi.rmiURLContext |
LDAP |
ldap:// |
com.sun.jndi.url.ldap.ldapURLContext |
LDAP |
ldaps:// |
com.sun.jndi.url.ldaps.ldapsURLContextFactory |
IIOP |
iiop:// |
com.sun.jndi.url.iiop.iiopURLContext |
IIOP |
iiopname:// |
com.sun.jndi.url.iiopname.iiopnameURLContextFactory |
IIOP |
corbaname:// |
com.sun.jndi.url.corbaname.corbanameURLContextFactory |
而针对这些不同自定义协议的解析,是通过 SPI,Service Provider Interface,服务供应接口进行实现的,通过实现 SPI,我们就能够自定义各种不同的协议方式通过 URL 的方式访问不同类型的资源。
RMI
RMI,Remote Method Invocation,远程方法调用,这是 Java 分布式编程中一个十分重要的接口,能够使得客户端远程调用服务端的对象资源。
比如说我们有这么一个接口
1 | package com.qsdz.interfaces; |
服务端对外屏蔽实现
1 | package com.qsdz.server; |
服务端的 RMI 服务
1 | package com.qsdz.server; |
这里将 hello
对象资源绑定到命名为 hello
的
RMI Registry 中。
客户端的 RMI 服务
1 | package com.qsdz.client; |
这样就实现了远程对象获取,对客户端屏蔽了实现,仅需服务端进行垃圾回收和资源管理,而多台客户端可以大量获取服务端资源而减少性能损耗,为分布式提供可能。
这里调用的 hello.hello()
在本地并没有相应的执行过程,而是远程对象的远程方法调用,具体执行过程是在服务端,客户端仅获取到服务端的运行结果。
而 JNDI 下的 RMI,通过 SPI 实现协议接口,可以通过 Naming Server 直接获取 RMI 资源,例如
服务端代码改写为
1 | package com.qsdz.server; |
而客户端代码改写为
1 | package com.qsdz.client; |
Reference
Reference 嘞表示对存在于命名/目录系统外的对象的引用。
比如说
1 | Reference refObj = new Reference("refClassName", "insClassName", "http://example.com:12345/"); |
当有客户端通过 lookup 查询 refObj
资源获取远程对象时,会获得一个 Reference
类的存根,而对于该类客户端会寻找 refClassName
匹配的类名。
javax.naming.Reference
构造方法为:Reference(String className, String factory, String factoryLocation)
,
className
- 远程加载时所使用的类名classFactory
- 加载的class
中需要实例化类的名称classFactoryLocation
- 提供classes
数据的地址可以是file/ftp/http
等协议
Reference 的使用场景在于实例化对象的引用,对于某些数据类我们是希望在本地执行过程而非在远程执行的,所以我们可以通过 Reference 类获取类字节码,随后在本地实例化它。
而 ReferenceWrapper 是包装类,用于保证 Reference 可以被 RMI 协议传输。
JNDI 注入
防御措施
那么如果说 JNDI 客户端的 lookup
参数可控,服务端
Reference
的 classFactoryLocation
可控,那么我们就可以使其执行一些恶意代码。
利用流程如下:
- 目标代码中调用了
InitialContext.lookup(URI)
,且URI为用户可控; - 攻击者控制URI参数为恶意的RMI服务地址,如:
rmi://evil_server/name
; - 攻击者RMI服务器向目标返回一个
Reference
对象,Reference
对象中指定某个精心构造的Factory
类; - 目标在进行
lookup()
操作时,会动态加载并实例化Factory类,接着调用factory.getObjectInstance()
获取外部远程对象实例; - 攻击者可以在Factory类文件的构造方法、静态代码块、
getObjectInstance()
方法等处写入恶意代码,达到RCE的效果
为了防止 JNDI 注入,下文是 Java 作的一些防御措施
- JDK 6u45、7u21之后:java.rmi.server.useCodebaseOnly的默认值被设置为true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前JVM的java.rmi.server.codebase指定路径加载类文件。使用这个属性来防止客户端VM从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
- JDK 6u141、7u131、8u121之后:增加了com.sun.jndi.rmi.object.trustURLCodebase选项,默认为false,禁止RMI和CORBA协议使用远程codebase的选项,因此RMI和CORBA在以上的JDK版本上已经无法触发该漏洞,但依然可以通过指定URI为LDAP协议来进行JNDI注入攻击。
- JDK 6u211、7u201、8u191之后:增加了com.sun.jndi.ldap.object.trustURLCodebase选项,默认为false,禁止LDAP协议使用远程codebase的选项,把LDAP协议的攻击途径也给禁了。
RMI 利用
低版本
首先编写客户端和服务端代码,假定 Reference 的 location 可控,那么一份客户端代码可能如下
1 | package com.qsdz.rmi.server; |
这里 location 端口选择 8000 是因为 Python 的 http.server
的默认启动端口是 8000。
而客户端那边省略无关代码,剩下的框架如下
1 | package com.qsdz.rmi.client; |
远程利用的情况下(通过可控 location 下载字节码),需要新建一个模块进行复现,在新建的模块中编写恶意类代码
1 | import javax.naming.Context; |
在不开启 http.server
的情况下,客户端可以成功运行,但不会执行恶意类代码(因为找不到)。
在开启 http.server
的情况下,客户端看可以成功运行,会从
location 下载字节码,可以观察到日志如下
1 | ::ffff:127.0.0.1 - - [24/Apr/2024 20:04:21] "GET /RMIEvilClass.class HTTP/1.1" 200 - |
需要知道的是,实例化类时加载优先级为:
1 | 静态块>IIB块>构造函数>getObjectInstance |
由于 getObjectInstance
返回值为 null
,故
lookup
结果为 null
。
高版本
为了绕过这里 ConfigurationException 的限制,我们有三种方法:
- 令
ref
为空 - 令
ref.getFactoryClassLocation
() 为空 - 令
trustURLCodebase
为true
第一种为空会导致无法实例化远程 RMI,第三种为空不太现实,这时只能选择第二种方法。
此时选择 org.apache.naming.factory.BeanFactory
,这是
tomcat 会使用到的工厂类,用于实例化 bean。
LDAP 利用
低版本
LDAP 服务作为一个树形数据库,可以通过一些特殊的属性来实现 Java 对象的存储。
1 | ObjectClass: javaNamingReference |
这就相当于 RMI 中的 Reference,配置其中的
className
,classFactory
饿
classFactoryLocation
,其实本质与 RMI 服务器无差异。