前言

参考:

https://boogipop.com/2023/03/02/%E6%B5%85%E5%AD%A6RMI%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96

https://halfblue.github.io/2021/10/26/RMI%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E6%BC%8F%E6%B4%9E%E4%B9%8B%E4%B8%89%E9%A1%BE%E8%8C%85%E5%BA%90-%E6%B5%81%E7%A8%8B%E5%88%86%E6%9E%90/

阅读本文前请先读参考文章或者与参考文章一起阅读,因为本文是学习参考文章的笔记与注释。

以下记录的是各阶段主要操作的代码摘要。按顺序写的,先是服务端、客户端、注册中心的准备工作,然后是注册中心收到请求,最后是服务端处理请求。其中分析DGC相关的部分是独立的,即和顺序没关系。

概要

总结性的东西放在前面,方便理解后续,而且也不妨碍看完后再回来当做总结。

RMI调用主要流程图:

RMI(1

理解这4个重要对象,之后的RMI反序列化攻击都和这些对象有关:RegistryImpl_Stub,RegistryImpl_Skel, UnicastRef,UnicastServerRef。

前两个都与注册中心的操作,如请求的发送和处理有关。第一个用在客户端,比如客户端要从registry获取stub时调用的是RegistryImpl_Stub#lookup,第二个用在服务端,比如注册中心收到请求时调用的是RegistryImpl_Skel#dispatch

后面两个和调用远程方法时的网络请求有关。看名字就知道第一个用在client第二个用在server,如client调用远程对象方法时用的是UnicastRef#invoke,server处理该请求时调用的就是UnicastServerRef#dispatch

还有一些别的对象:

LiveRef、TCPEndoPint、TCPTransport,这些对象都被包含在UnicastRef、UnicastServerRef。

RemoteObjectInvocationHandler,这个对象包含了UnicastRef。这个对象就是上图中的stub。

服务端与注册中心

创建远程对象

new RemoteObjImpl();
return UnicastRemoteObject#exportObject
new UnicastServerRef(port)
new LiveRef(port)
this((new ObjID()), port);
this(objID, TCPEndpoint.getLocalEndpoint(port), true);
super(liveRef)
return exportObject(this,unicastServerRef)
return UnicastServerRef#exportObject(RemoteObjImpl,)
Remote stub = Util.createProxy(implClass, getClientRef(), forceStubUse)
Util.createProxy
//创建动态代理
//RemoteObjectInvocationHandler
//返回值是一个Remote的代理

new Target(impl, this, stub, ref.getObjID(), permanent);

LiveRef#exportObject
TCPEndPoint#exportObject
TCPTransport#exportObject
listen();
super.exportObject(target);
ObjectTable.putTarget(target);
DGCImpl#static
return stub

创建注册中心

LocateRegistry.createRegistry(port);
return new RegistryImpl(port)
LiveRef var2 = new LiveRef(id, var1);
this.setup(new UnicastServerRef(var2));
UnicastServerRef#exportObject
var5 = Util.createProxy(var4, this.getClientRef(), this.forceStubUse);
setSkeleton(var1)
Util.createSkeleton //反射生成sun.rmi.registry.RegistryImpl_Skel

var6 = new Target(var1, this, var5, this.ref.getObjID(), var3);
//后面和上面的一样

绑定stub

很简单

this.bindings.put(var1, var2);

客户端

创建注册中心

LocateRegistry#getRegistry
new LiveRef(new ObjID(ObjID.REGISTRY_ID),new TCPEndpoint(host, port, csf, null),false);
new UnicastRef(liveRef)
return (Registry) Util.createProxy(RegistryImpl.class, ref, false)
return createStub(var3, var1); // 获取的是RegistryImpl_Stub

从注册中心获取远程对象stub

RegistryImpl_Stub#lookup
UnicastRef#newCall

UnicastRef#invoke
StreamRemoteCall#executeCall
DataInputStream rd = new DataInputStream(conn.getInputStream());//和注册中心通信,获取stub的原始数据

// 服务端攻击客户端
case TransportConstants.ExceptionalReturn:in.readObject();


readObject //反序列化得到stub,注册中心攻击客户端

用stub调用远程对象方法

RemoteObjectInvocationHandler#invoke
invokeRemoteMethod
UnicastRef#invoke
var7 = new StreamRemoteCall(var6, this.ref.getObjID(), -1, var4);
var7.executeCall();
this.in.readObject(); //服务端/注册中心攻击客户端
unmarshalValue //这个方法是用来判断值类型的。
readObject() //若类型不是基础类型,则对远程方法的返回值readObject。服务端攻击客户端

注册中心

收到请求

RegistryImpl_Skel#dispatch中根据调用的不同方法进入不同分支。

handleMessages
serviceCall
UnicastServerRef#dispatch
oldDispatch
RegistryImpl_Skel#dispatch //客户端/服务端攻击注册中心
case 0: // bind(String, Remote)
case 1: // list()
case 2: // lookup(String)
in.readObject(); // 对lookup的参数进行反序列化。客户端攻击服务端
case 3: // rebind(String, Remote)
case 4: // unbind(String)

攻击方式:客户端获取到registry后,lookup不传字符串,传一个对象,那么服务端就会反序列化该对象。

服务端

处理请求

UnicastServerRef#dispatch
UnicastServerRef#unmarshalValue // 获取方法参数类型

DGC

客户端

DGCImpl_Stub的生成

RegistryImpl_Stub#lookup
ref.done
StreamRemoteCall#done
ConnectionInputStream#registerRefs
DGCClient#registerRefs
EndpointEntry#lookup
new EndpointEntry//到这一步就生成了DGCImpl_Stub

DGCImpl_Stub具体方法的调用

//入口在EndpointEntry构造函数开启的新线程
RenewCleanThread#run
进入循环
makeDirtyCall
DGCImpl_Stub#dirty
this.ref.newCall()//与远程DGC服务端通信
call.getOutputStream()//获取远程DGC服务端结果
in.readObject //将结果反序列化。DGC客户端被DGC服务端攻击


服务端

处理客户端的dirty的时候,也会对客户端的数据readObject。