前言
参考:
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调用主要流程图:
理解这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。