Java RMI

RMI全程Remote Method Invocation,远程⽅法调⽤,他的目标和RPC其实是类似的,是java编程语言中,一种实现远程过程调用的应用程序编程接口。存储于java.rmi包中

他的功能是==让一个Java虚拟机上的对象调用另一个Java虚拟机中对象上的方法==,是Java独有的一种机制。使用其方法调用对象时,必须实现Remote远程接口,能够让某个java虚拟机上的对象调用另外一个Java虚拟机中的对象上的方法。两个虚拟机可以运行在相同计算机上的不同进程,也可以是网络上的不同计算机。

在网络传输的过程中,RMI中的对象是通过序列化的方式进行编码传输的,也就意味着,==RMI在接受到序列化编码的对象后会进行反序列化==,而在反序列化攻击中,我们可能很头疼怎么触发反序列化链,因此我们 ==可以利用RMI服务作为反序列化利用链的触发点==

image-20240727161643882

架构

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

RMI依赖的通信协议为JRMP(Java Remote Message Protocol ,Java 远程消息交换协议),工作在应用层。该协议为Java定制,要求服务端与客户端都为Java编写。这个协议就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。

RMI Registry就像⼀个**⽹关**,他⾃⼰是不会执⾏远程⽅法的,但RMI Server可以在上⾯注册⼀个Name 到对象的绑定关系;RMI Client通过Name向RMI Registry查询,得到这个绑定关系,然后再连接RMI Server;最后,远程⽅法实际上在RMI Server上调⽤。

它包含三个部分

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

实例

==这里有个坑,你必须保证服务端和客户端得IRemoteObj这个接口的包名相同,不然无法反序列化==

一个简单的示例如下

远程调用的方法需要 - 一个继承了java.rmi.Remote的接口 - 一个实现了该接口并继承了UnicastRemoteObject的类

package top.longlone;

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IRemoteHelloWorld extends Remote {
    public String hello() throws RemoteException;
}
package top.longlone;

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {
    protected RemoteHelloWorld() throws RemoteException {
        super();
    }

    @Override
    public String hello() throws RemoteException {
        System.out.println("call");
        return "hello world";
    }
}

RMIServer需要创建Registry,并将上面的类实例化后绑定到一个地址。

package top.longlone;

import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
    private void start() throws Exception {
        RemoteHelloWorld helloWorld = new RemoteHelloWorld();
        LocateRegistry.createRegistry(1099);
        Naming.bind("rmi://192.168.135.142:1099/Hello", helloWorld);
    }

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

我们在新建一个RMI Registry的时候,都会直接绑定一个对象在上面,也就是说我们示例代码中的Server其实包含了Registry和Server两部分:

LocateRegistry.createRegistry(1099);
Naming.bind("rmi://127.0.0.1:1099/Hello", new RemoteHelloWorld());

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

Naming.bind 的第一个参数是一个URL,形如: rmi://host:port/name 。其中,host和port就是

RMI Registry的地址和端口,name是远程对象的名字。

如果RMI Registry在本地运行,那么host和port是可以省略的,此时host默认是 localhost ,port默认

是 1099 :

Naming.bind("Hello", new RemoteHelloWorld());

以上就是RMI整个的原理与流程。

RMIClient使用Naming.lookup在Registry中找到HelloWorld的对象,这里也可以看出来接口的重要性: 虽然我们是在远程调用方法,但是我们还是需要实例知道有哪些方法,因此客户端也需要这个接口。

package top.longlone;

import java.rmi.Naming;

public class RMIClient {
    public static void main(String[] args) throws Exception{
        IRemoteHelloWorld helloWorld = (IRemoteHelloWorld) Naming.lookup("rmi://192.168.135.142:1099/Hello");
        String ret = helloWorld.hello();
        System.out.println(ret);
    }
}

结合文章所说的,整个RMI的流程如下:

⾸先客户端连接Registry,并在其中寻找Name是Hello的对象,这个对应数据

流中的Call消息;然后Registry返回⼀个序列化的数据,这个就是找到的Name=Hello的对象,这个对应

数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是⼀个远程对象,地址

在 192.168.135.142:33769 ,于是再与这个地址建⽴TCP连接;在这个新的连接中,才执⾏真正远程

⽅法调⽤,也就是 hello() 。

安全问题

接下来,我们很自然地想到,RMI会给我们带来哪些安全问题

从两个方向思考一下这个问题:

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

攻击RMI Registry

首先,RMI Registry是一个远程对象管理的地方,可以理解为一个远程对象的“后台”。我们可以尝试直

接访问“后台”功能,比如修改远程服务器上Hello对应的对象:

RemoteHelloWorld h = new RemoteHelloWorld();
Naming.rebind("rmi://192.168.135.142:1099/Hello", h);

却爆出了这样的错误:

image-20240801152941995

原来Java对远程访问RMI Registry做了限制,只有来源地址是localhost的时候,才能调用rebind、

bind、unbind等方法。

不过list和lookup方法可以远程调用。

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

String[] s = Naming.list("rmi://192.168.135.142:1099")

lookup作用就是获得某个远程对象。

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

https://github.com/NickstaDB/BaRMIe,其中一个功能就是进行危险方法的探测。

但是显然,RMI的攻击面绝不仅仅是这样没营养。

RMI利用codebase执行任意代码

曾经有段时间,Java是可以运行在浏览器中的,对,就是Applet这个奇葩。在使用Applet的时候通常需要指定一个codebase属性,比如

<applet code="HelloWorld.class" codebase="Applets" width="800" height="600">
</applet>

ps:这段代码是一个 Java Applet 的 HTML 标记,用于在网页中嵌入 Java Applet。Java Applet 是一种可以嵌入网页中的小型 Java 程序,允许在网页上运行 Java 代码并实现动态功能。codebase 是 HTML <applet> 标签中的一个属性,用于指定包含 Java Applet 类文件的目录或 URL。

除了Applet,RMI中也存在远程加载的场景,也会涉及到codebase。

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

如果我们指定 codebase=http://example.com/ ,然后加载 org.vulhub.example.Example 类,则Java虚拟机会下载这个文件 http://example.com/org/vulhub/example/Example.class ,并作为

Example类的字节码。

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

这个时候问题就来了,如果codebase被控制,我们不就可以加载恶意类了吗?

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

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

  • 安装并配置了SecurityManager
  • Java版本低于7u21、6u45,或者设置了 java.rmi.server.useCodebaseOnly=false

其中 java.rmi.server.useCodebaseOnly 是在Java 7u21、6u45的时候修改的一个默认设置:

https://docs.oracle.com/javase/7/docs/technotes/guides/rmi/enhancements-7.html

https://www.oracle.com/technetwork/java/javase/7u21-relnotes-1932873.html

官方将 java.rmi.server.useCodebaseOnly 的默认值由 false 改为了 true 。在

java.rmi.server.useCodebaseOnly 配置为 true 的情况下,Java虚拟机将只信任预先配置好的

codebase ,不再支持从RMI请求中获取。

0%