Java反序列化漏洞之JAVA RMI原理、流程(2)
2023-06-15 17:10:40 # Web Security # Java Deserialization

什么是JAVA RMI

RMI ( Remote Method Invocation , 远程方法调用 ) 能够让在某个 Java虚拟机 上的对象像调用本地对象一样调用另一个Java虚拟机 中的对象上的方法 , 这两个 Java虚拟机 可以是运行在同一台计算机上的不同进程, 也可以是运行在网络中不同的计算机上

为什么要使用RMI?

  • 一般我们想要在本地调用一个object的方法的时候,会采用object.method()的方式来调用,但如果提供object的并不是本地的JAVA虚拟机,而是远程的JAVA虚拟机呢?我们本地并没有这个对象,那么我们就需要用到RMI

  • 当我们服务器拥有一系列服务,想要把它们提供给客户端,但问题是,服务器只想提供它想要提供给客户端的方法,它不可能把自己所有的对象和方法都发送给客户端,让客户端去调用一个对象的所有方法,这是极其不安全的。

  • 假设客户端(JVMA)想要获取服务端(JVMB)上的某个对象ObjectA,但下次程序修改后,实例对象的名称可能会发生改变,这时候客户端(JVMA)并不知道实例对象的名称是什么,也就无法获取到了,又失去了与服务端(JVMB)的联系。

  • 假设客户端找到了想要的资源,但数据传输又成了问题。这时候获取到的是一个完整的对象,在远程调用的时候,如果先把对象分解成基本类型的数据传输给客户端后,再把这些基本类型的数据拼接成对象,这样会大大增加代码复杂度。

  • 另外,如果程序需要频繁的远程调用,开发人员不可能为每一次调用都设计一套调用方法,所以一个统一而规范的接口十分重要

RMI工作流程

image-20210725210630532

Image

上面这个流程图说的很清楚。

RMI有三个角色参与,分别是Client、Registry、ServerServerClient提供服务,但它不愿意将所有的对象、方法都交给Client去调用,所以它需要一个“中间人” - RegistryServer把它想要提供的服务告诉Registry,让RegistryClient去交流(获取、发送请求),所以它相当于是服务器的代理,负责帮Server办事。

那么Registry怎么向Client提供服务呢?Registry上会绑定需要提供给客户端的服务,绑定的时候,会生成对应stub(存根),所以Registry里有各种服务的stub。这时候Client如果想要获取服务器上的某个服务,它可以直接在Registry中查找,如果找到了对应的stub,就返回这个stub的拷贝,这样他就可以直接把stub当作一个object,然后调用里面的方法了。所以,stub相当于远程对象在客户端的代理。

Registry除了会生成stub,还会生成skeleton(相当于server的代理),Skeleton用于处理stub发过来的请求,然后去调用服务端的方法,再返回给stub。但在jdk1.2以后,反射API替代了skeleton的作用,它可以直接把请求发给服务端,所以就不再用skeleton了。

客户端调用stub的方法后,stub会将客户端想要调用的方法名及其参数序列化,利用远程引用层传输层Socket通信)发给ServerServer那边的远程引用层接收到数据后发给Skeleton反序列化后在Server执行被调用的方法,然后将方法的返回值或异常打包(序列化后)返回给客户端,客户端再以同样的方法反序列化解析返回数据。

这里需要注意的是,远程方法的调用最后都是在Server执行的,最后只是返回序列化后的结果而已。

案例讲解

定义远程接口

Server想要提供服务,那么就需要为这些提供的服务定义一个接口,让Client可以访问到它。该接口必须继承Remote,这样该接口才能成为一个远程对象,才可以被JAVA虚拟机所调用。

1
2
3
4
5
6
7
8
9
10
11
12
import java.rmi.Remote;
import java.rmi.RemoteException;

//这个接口不仅要给server用,还要给client用,client中通常是把服务端接口打包成jar包使用。
public interface HelloService extends Remote {
//继承Remote接口以后,该Class成为Server的一个远程对象,供Client访问并提供一定服务
//Remote只是一个标识接口,不含任何方法,继承该接口的类可以被远程的JAVA虚拟机调用

//因为远程调用会涉及到网络通信,容易出现网络异常,所以需要抛出RemoteException
public String hello() throws RemoteException;//事实上,RemoteException也是继承于IOException的
//修饰符必须为public 否则会报错
}

定义Service Implementation Class

定义好了远程接口后,需要写出具体的方法内容即实现类。该类需要继承UnicastRemoteObject,而这个父类会生成stubskeleton

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
//用来调用远程接口, 这样客户端访问远程对象的时候,远程对才会把自身的一个拷贝(stub存根)以socket的形式传输给客户端

//UnicastRemoteObject 将生成stub(远程对象在本地的代理)和skeleton(服务端的一个代理,用来处理stub发来的请求)
public class HelloServiceImp extends UnicastRemoteObject implements HelloService {

public HelloServiceImp() throws RemoteException {
super();
//如果父类的无参构造函数抛出了异常,则子类的无参构造函数不能省略不写,并且必须抛出父类的异常或父类异常的父类
}

@Override
public String hello() throws RemoteException {
return "这是远程服务器发来的消息";
}
}

Registry Class

写好了服务接口,那么我们就需要一个中间代理人 - Registry。

我们需要创建并启动RMIService,并把我们提供的实现类绑定在上面。

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
import server.HelloService;
import server.HelloServiceImp;

import java.net.MalformedURLException;
import java.rmi.AlreadyBoundException;
import java.rmi.Naming;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;

//中间代理人
public class RMIRegister {
public static void main(String[] args) throws RemoteException {
try {
HelloService helloService = new HelloServiceImp();//imp会自动调用父类的构造方法来生成返回stub
LocateRegistry.createRegistry(1099);//在本地创建并启动RMIService,被创建的RMIService服务将会在指定的端口上监听请求。
Naming.bind("rmi://localhost:1099/helloService", helloService);
//执行这个方法后相当于发布了RMI服务,把远程服务的实现类绑定到指定的RMI地址上
System.out.println("远程对象绑定成功!");
} catch (AlreadyBoundException e) {
System.out.println("发生重复绑定对象异常");
e.printStackTrace();
} catch (MalformedURLException e) {
System.out.println("发生URL协议异常");
e.printStackTrace();
}catch (RemoteException e){
System.out.println("创建远程对象异常");
e.printStackTrace();
}
}
}

Client

直接使用Naming.lookup()可以找到我们想要的stub

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
import server.HelloService;

import java.net.MalformedURLException;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class HelloClient {
public static void main(String[] args) {
//在远程对象注册表registry中寻找指定name的对象,并返回reference
try {
//获取register
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
for(String i : registry.list()){
System.out.println("注册的服务:"+i);
}
HelloService helloService = (HelloService) Naming.lookup("rmi://localhost:1099/helloService");//返回的就是stub
System.out.println(helloService.hello());
} catch (NotBoundException e) {
e.printStackTrace();
} catch (MalformedURLException e) {
e.printStackTrace();
} catch (RemoteException e) {
e.printStackTrace();
}
}
}

Client上的接口

即使我们在服务端上设计了一套接口HelloService,但是Client是不知道的,所以我们需要在Client端设定一模一样的接口,这样Client才知道哪些方法能够被他们所调用。

运行测试

先运行RMIRegister

image-20210725220714621

可以看到,RMIService成功绑定。

再运行Client:

image-20210725220758857

成功收到远程服务器上的方法返回值。

RMI的安全问题

根本原因

RMI在数据传输过程中,涉及到序列化和反序列化,这就有构造恶意对象的风险。

伪造LocateRegister攻击client

我们客户端获取stub的方式是通过**Naming.lookup()**来查找指定url

例如

1
2
3
4
5
/*Client.java*/

HelloService helloService = (HelloService) Naming.lookup("rmi://localhost:1099/helloService");//返回的就是stub
System.out.println(helloService.hello());
//此处我们是通过查找RMI服务地址localhost:1099中的helloService对象

如果该lookup()方法中的rmi地址(也就是LocateRegister)是可控的,让Client获取到的service是我们构造的恶意service(攻击者搭建的RMI server和实现的恶意对象),我们就能执行恶意helloService中的hello()了

RMI的利用方法远远不止如此,更多相关内容可见Java反序列化漏洞之利用链分析集合(4)

Reference

Java RMI原理及反序列化学习

JAVA安全基础(四)– RMI机制

Java安全之RMI反序列化

Java-RMI

[改善Java代码]不要在构造函数中抛出异常

Java 反序列化漏洞(1) – Java RMI 原理/流程