Java-RMI反序列化漏洞
RMI基础
RPC
RPC(Remote Procedure Call)远程过程调用,就是要像调用本地的函数一样去调远程函数。它并不是某一个具体的框架,而是实现了远程过程调用的都可以称之为RPC。比如RMI(Remote Method Invoke 远程方法调用)就是一个实现了RPC的JAVA框架。
一般RPC的过程:Client如果想要远程调用一个方法,就需要通过一个Stub类传递类名、方法名与参数信息给Server端,Server端获取到这些信息后会从本地服务器注册表中找到具体的类,再通过反射获取到一个具体的方法并执行然后返回结果。
RMI
RMI(Remote Method Invocation)为远程方法调用,是允许运行在一个Java虚拟机的对象调用运行在另一个Java虚拟机上的对象的方法。 这两个虚拟机可以是运行在相同计算机上的不同进程中,也可以是运行在网络上的不同计算机中
RMI一个简单的demo
服务器端
服务器端需要实现三个类:
服务器端首先需要定义一个继承自Remote的远程接口IRemoteObj.java:
1 | import java.rmi.Remote; |
在Java中,只要一个类extends了java.rmi.Remote接口,即可成为存在于服务器端的远程对象。其他接口中的方法若是声明抛出了RemoteException异常,则表明该方法可被客户端远程访问调用。
接着需要具体实现该接口,实现的方法就是可以远程访问调用的方法。远程接口实现类,RemoteObjImpl.java
1 | import java.rmi.server.UnicastRemoteObject; |
远程对象必须继承java.rmi.server.UnicastRemoteObject,这样才能保证客户端访问获得远程对象时,该远程对象将会把自身的一个拷贝以Socket的形式传输给客户端,此时客户端所获得的这个拷贝称为“存根”,而服务器端本身已存在的远程对象则称之为“骨架”。其实此时的存根是客户端的一个代理(Stub),用于与服务器端的通信,而骨架也可认为是服务器端的一个代理(skeleton),用于接收客户端的请求之后调用远程方法来响应客户端的请求。
简单来说就是该远程对象创建了Stub和Skeletion,并且把Stub通过Socket给了客户端,客户端就可以使用Stub向远程对象要方法,Skeletion留在自己这里以响应请求。
最后还需要一个实现服务端的类(直接使用Registry实现的RMI,其实也可以通过Naming的方式注册):
1 | import java.rmi.registry.LocateRegistry; |
这个类的作用就是注册远程对象,向客户端提供远程对象服务。将远程对象注册到RMI Service之后,客户端就可以通过RMI Service请求到该远程服务对象的stub了,利用stub代理就可以访问远程服务对象了。
简单来说就是服务端把stub放在注册中心,并绑定名字,然后客户端通过url和名字在注册中心取stub,并使用stub访问远程服务对象。
客户端
客户端相对简单,只需要实现两个类:
第一个依然是和服务端一样的远程接口(本地的情况可以直接包含):
1 | import java.rmi.Remote; |
第二个就是实现客户端:
1 | import java.rmi.registry.LocateRegistry; |
先通过LocateRegistry.getRegistry,使用ip和端口获得Registry对象,再使用lookup寻找远程对象,之后像正常使用本地对象一样调用其方法即可。
总结:流程图如下
RMI反序列化攻击
一句话概括就是由于数据传输经过了序列化,接收方又将其反序列化,故所有数据传输的地方都存在反序列化漏洞,都可以通过CC链等进行攻击。
至于本地操作注册中心和远程操作注册中心的RMI源码逻辑,暂时不在这里细究,有需要可以查阅文底参考文献和组长的视频,打算到时候深学计网再回来慢慢研究。
各种不同场景下的攻击流程图如下:
服务端与客户端攻击注册中心
服务端和客户端攻击注册中心的方式是相同的,都是远程获取注册中心后传递一个恶意对象进行利用。
bind() & rebind()
远程调用bind()绑定服务时,注册中心会对接收到的序列化的对象进行反序列化。所以,我们只需要传入一个恶意的对象即可。
1 | public class UserServerEval { |
需要注意的是bind接收的参数是Remote对象,所以不能直接把AnnotationInvocationHandler
类的对象传进去,需要进行类型转换。
unbind&lookup
注册中心在处理请求时,是直接进行反序列化再进行类型转换
如果我们要控制传递过去的序列化值的话,不能直接传递给lookup
这个方法,因为它的参数是一个String
类型。但是它发送请求的流程是可以直接复制的,只需要模仿lookup
中发送请求的流程,就能够控制发送过去的值为一个对象。
1 | public class UserServerEval2 { |
注册中心攻击服务端与客户端
客户端和服务端与注册中心的参数交互都是把数据序列化和反序列化来进行的,那这个过程中肯定也是存在一个对注册中心返回的数据的反序列化的处理,这个地方也存在反序列化漏洞风险。
可以使用ysoserial生成一个恶意的注册中心,当调用注册中心的方法时,就可以进行恶意利用。
1 | java -cp ysoserial.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections1 'open /System/Applications/Calculator.app' |
12345是端口号,CC1是攻击方式,后面是要执行的命令。
1 | list() |
突然发现这ysoserial真是个好东西。
客户端攻击服务端
如果注册服务的对象接收一个参数为对象,那么可以传递一个恶意对象进行利用。
相当于客户端构造好了一个对象,然后通过远程调用方法把该对象传到服务端
服务端:
客户端:
1 | userClient.dowork(getpayload()); |
服务端攻击客户端
跟客户端攻击服务端一样,在客户端调用一个远程方法时,只需要控制返回的对象是一个恶意对象就可以进行反序列化漏洞的利用了。总之就是互相传序列化好了的恶意对象,让对方反序列化从而执行命令。
服务端:
1 | public Object getwork() throws RemoteException { |
客户端只要远程调用getwork()方法,就会接收到evalObject,从而反序列化执行命令。
参考链接:
Java-RMI反序列化漏洞