1. 前言
随着对rmi反序列化的深入学习,发现它的攻击面并没有一最先明白的浅易。主要照样在看这篇针对RMI服务的九重攻击时,发现攻击中涉及的类和通讯协议足以让我单独花上时间研究一阵子了。因此这篇算是我单独研究rmi挪用流程和源码的总结。
2. 回首
先回首一下rmi事情的流程:
- 注册端开启注册服务
- 服务端在注册端通过
String - Object
的映射关系绑定工具 - 客户端通过一个字符串向注册端查询获取到操作远程工具的stub(?)
- 客户端通过stub来执行远程方式
在整个流程中,涉及了两组关系:
- 客户端——注册端
- 客户端——服务端
这里的两个客户端都是相对而言的,并不是指代同一个工具。好比,注册端开放在192.168.242.1:1099
端口,下述代码的行为就是我说的第一个客户端:
Registry registry = LocateRegistry.getRegistry("192.168.242.1", 1099); String[] lists = registry.list(); registry.lookup("Hello"); registry.bind("las", new myRemoteObj()); // only work for localhost
再好比,当通过lookup方式获取到一个工具后,对该工具的操作就是指代第二种实体关系:
... myInterfACE obj = registry.lookup("Hello"); obj.myMethod("param");
上面两种实体之间的通讯细节和协议,就是我接下来要实验注释的器械。
由于剖析rmi源码也是为了能够更深入的学习rmi平安问题,以是我列出了剖析中的一些关注点以助于明白:
- 当服务端要抛出一个错误时,它的挪用栈是怎样的。这个错误是怎样被发送给客户端的。
- 客户端的
DGCClient
工具提议dirty call
的流程。这里现实上就是ysoserial中JRMPClient载荷行使的地方。 UnicastServerRef,dispatch
方式的两个分支。
3. 客户端—注册端
注册端
简朴来说,注册端监听在1099端口,剖析客户端提议的所有毗邻,判断它们是list
、bind
、rebind
、lookup
、uNBind
这五种行为的哪一种,并挪用响应的方式来处置。不外在此之前,它们还会通报一些数据包用于认证和信息交流。好比,下面是客户端在执行lookup
方式时所发生的流量:
不外注册端不只会处置这些api挪用,后面会看到,注册端还会处置dgc
和一些心跳包的发送。
剖析清晰了注册端的行为,就能搞清晰客户端所做的事情,两者是相对的。
我们首先剖析注册端。
一样平常来说,注册端的要害代码如下:
IRemoteHelloWorld remoteHelloWorld = new RemoteHelloWorldImpl(); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("Hello", remoteHelloWorld);
处置网络数据前的挪用链
进入createRegistry
方式:
这里获取到的是一个
RegistryImpl
工具。需要说明的是,客户端一样平常挪用的是LocateRegistry.getRegsitry(ip,port)
,它获取到的是一个RegistryImpl_Stub
工具。故可知,stub和skel的观点是相对而言的,并不只存在于服务端和客户端之间。
进入RegistryImpl
的组织方式:
可以看到,无论是if照样else语句块中,焦点代码都是:
LiveRef var2 = new LiveRef(id, var1); this.setup(new UnicastServerRef(var2));
这里之以是有个if,是为了保证当程序设置了SecurityManager
后,只有当rmi注册服务开放在1099端口时才气执行焦点代码。在设置SecurityManager
计谋后,程序自己可能会没有特权去执行焦点代码,因此需要通过AccessController.doPrivileged
的形式去赋予特权。关于AccessController可参见毗邻。
总的来说,这里的if相当于提供了一个平安计谋:程序员可以通过设置securityManager
来保证rmi服务只能开放在1099端口。
接下来进入this.setup()
方式:
这里执行了UnicastServerRef
上的exportObject
方式,这也是第一次看到exportObject
方式泛起。执行了它之后,客户端就可以挪用注册端上的api了,就好像注册端(object)被露出(export)在了1099端口一样。
在openjdk代码的注释中注释的则更detAIl:
/** * Export this object, create the skeleton and stubs for this * dispatcher. Create a stub based on the type of the impl, * initialize it with the appropriate remote reference. Create the * target defined by the impl, dispatcher (this) and stub. * Export that target via the Ref. */
说明在这个方式里,会建立注册端响应的stub
工具和skeleton
工具。
继续跟进:
这里先跟进Util.createProxy()
方式:
发现最后执行了createStub
方式,这个方式通过反射实例化了sun.rmi.registryImpl_Stub
工具并将其作为返回值。
这里就知道了,var5
就是一个registryImpl_Stub
工具。
同时这里也挪用了this.setSkeleton
来设置一个registryImpl_Skel
:
接下来回到刚刚的exportObject
方式中,发现它建立了一个Target
工具var6
,然后挪用了this.ref.exportObject(var6)
,这里的ref
,就是我们前面建立UnicastServerRef
时传入的liveRef
工具:
于是跟进liveRef,exportObject()
方式:
跟进:
继续跟进:
这里的this.listen()
方式是重点,执行它之后注册端就最先监听1099端口了。于是我们跟进看它的内部逻辑:
这里会进入到第一个if代码块内。
可以看到这里又泛起了AccessController.doPrivileged()
方式。由于它最终会挪用到TCPTraNSport.AcceptLoop,run
方式,我们直接在这个方式下断点并跟进:
跟进:
下面的代码都是catch
块就不贴出来了。
这里的this.serverSocket
就是在TCPTransport,listen
方式中建立的,它监听的端口就是我们最最先传入的port。
接下来进入try中的代码块,它实在又建立了新线程。跟进ConnectionHandler,run
方式:
再跟进this.run0()
方式。
处置网络数据
需要说明的是,从这个方式最先,就能看到注册端最先读取并剖析客户端通报的TCP数据,凭据字段的类型来执行响应的bind
、list
等操作,并将效果返回。
由于该方式比较长,以是我逐段举行剖析。
前面的一些代码主要是设置TCP的一些参数,不管,看下面的部门:
这里是第一次从输入流读取数据,接下来会凭据var6
的值判断进入哪个if块:
这里的1347375956转为十六进制再转为字符串就是:POST。说明这里的逻辑是判断它是否为http流量,一样平常不会进入这个分支。
我们进入第二个if。1246907721转为十六进制实在是0x4a524d49,这实在就是rmi协议的魔术头,同时第二个short字段示意版本:
在这个if分支里又读取了一个字节存到var15
,然后进入switch
。这里一样平常来说读取到的是0x4b,即75:
进入case后,由于已经剖析完了第一个接受包,注册端最先组织第一个回复包:
直到var10.flush();
,注册端缓冲区的数据被发送出去。
接下来的代码:
String var16 = var5.readUTF(); int var17 = var5.readInt();
用于剖析客户端发送的第二个数据包,不外这个数据包似乎没起到什么作用。第二个数据包内容如下:
接下来进入TCPTransport.this.handleMessages(var14, true)
。注重第二个参数为true,它让接下来的代码中的while语句不停循环。见红框圈处:
这里int var5 = var4.read()
实在已经最先剖析客户端发送的第三个数据包了(说明注册端并没有回复第二个数据包,从流量图也能看出),第三个数据包的内容将在之后贴出。初看这个var5是int好像是读取4个字节,然则跟进var4.read()
能看到实在照样读取的一个字节:
var5
的内容实在是示意该数据包是哪种类型。一样平常来说有下面三种:
80,即
0x50
,示意执行营业方式。这里是挪用注册端的某个方式(如list
、bind
),后面会看到,客户端在执行远程方式时,服务端也会从这里进去。例如下述数据包:
82,即
0x52
,心跳包(也许),这里可以看到注册端回复了一个字节0x53
。例如下面两个数据包:84,即
0x54
,DgcAck
,如下:
这里继续跟进80,也就是挪用注册端api方式的case。进入StreamRemoteCall,serviceCall
:
我们首先贴出第三个数据包的内容:
需要注重的是,我们上面已经读取了一个字节了,接下来的aced
之后的内容,按理来说都是序列化的内容了。
然则注重这里的var39 = ObjID.read(var1.getInputStream());
,它是从什么地方读取的内容呢?
实在内容是在序列化数据的BlockData
块。
读取了ObjID
后,又开启了新线程。进入var6.dispatch()
,也就是UnicastServerRef,dispatch
:
这里实在是进入this.oldDispatch
。继续跟进:
后面在剖析客户端—服务端时可知,若是不进入这个if语句,之后所作的事情实在就是服务端处置客户端挪用远程工具方式的部门。这里留个印象就好。
这里的this.skel
就是在之前挪用UnicastServerRef,exportObject
方式时设置的registryImpl_Skel
。跟进registryImpl_Skel,dispatch
:
终于到这里了!这里就是注册端最后挪用各个api(list
、bind
等)的地方。
看一眼客栈:
不外到这里,我们只跟进了LocateRegistry.createRegistry
的内容。注册端的代码另有一条:
registry.bind("Hello", remoteHelloWorld);
万幸是registryImpl,bind
的逻辑很简朴:
这里只是将String——obj的映射关系放到了registryImpl
的bindings
中。
后续可以看到,客户端执行lookup
方式时,注册端就会从registryImpl
的bindings
中查询工具:
注册端剖析完毕。接下来剖析客户端。
图示
注册端挪用LocateRegistry,createRegistry
的流程比较复杂,以是截了下图便于更直观的看到挪用关系。
每一个线程单独为一张图。
主线程:
线程一:
线程二:
线程三,从这里最先处置网络数据:
线程四,又回到了UnicastServerRef
方式,最终挪用了RegistryImpl_Skel,dispatch
:
客户端
客户端代码如下:
Registry registry = LocateRegistry.getRegistry("192.168.242.1", 1099); registry.lookup("Hello");
开启注册端后最先debug。
首先跟进LocateRegistry,getRegistry
方式:
继续跟进:
最后同样挪用Util,createProxy
建立了一个registryImpl_Stub
工具(这个方式在注册端的UnicastServerRef,exportObject
中曾被挪用)。传入的UnicastRef
中包罗了ObjID
、host
、port
等信息,用于毗邻注册端:
到这里可知,客户端挪用LocateRegistry,getRegistry
方式获取到的工具是RegistryImpl_Stub
。
接下来跟进客户端的第二行代码,即RegistryImpl_Stub,lookup
:
在super.ref.newCall( this, operations, 2, ...);
处会提议前期的握手包(不是tcp的),
然后在super.ref.invoke(var2);
处发送lookup
的数据。
在接下来的var23=(Remote)var6.readObject()
获取到服务端发送的署理工具。
这里的super.ref
就是一个unicastRef
工具。
super.ref.newCall()
这里先跟进super.ref.newCall
:
再跟进this.ref.getChannel().newConnection()
,AKATCPChannel,newConnection
:
跟进this.createConnection()
,这部门代码有些长,分两部门列出:
这里的var3.writeByte(75)
就是去组织了客户端发送的第一个握手包,在注册端的剖析中我们知道它实在示意StreamProtocol
:
至于if块中的var3.writeByte(76)
,应该是在socket不是Reusable时,接纳将握手包和现实数据合在一起的方式(可以看到它没有var3.flush()
)。
接着var3.writeByte(75)
看它之后的代码:
这里是在读取注册端回复的第一个数据包。长这样,发现它把我们的host和port又发给了我们:
从var3.writeUTF
最先,客户端发送第二个数据包:
到这里为止,客户端发送了两次数据包,处置了注册端的第一个回复。
回到super.ref.newCall
:
,欢迎进入Allbet手机版下载(Allbet Game):www.aLLbetgame.us,欧博官网是欧博集团的官方网站。欧博官网开放Allbet注册、Allbe代理、Allbet电脑客户端、Allbet手机版下载等业务。
接下来进入var7 = new StreamRemoteCall(var6, this.ref.getObjID(), var3, var4);
:
这里实在是在组织现实的lookup
包了。然则还没有写入工具,只是将一些信息写到了BlockData中。
到这里为止,super.ref.newCall()
的功效基本剖析完毕。该回到RegistryImpl_Stub,lookup
剖析下个方式super.ref.invoke
。在此之前,需要看到,在lookup
中,它先挪用了var3.writeObject(var1)
将查询的字符串写入了缓冲区:
super.ref.invoke()
跟进UnicastRef,invoke
:
跟进StreamRemoteCall,executeCall()
:
首先在this.releaseOutputStream()
中将适才的写的数据(主要是lookup的字符串)发送了出去:
到这里,客户端发送了第三个数据包。
客户端发送这个数据包之后,是期望获得注册端的回复的,由于我们希望通过lookup
获取到一个工具,然后基于这个工具来操作远程工具。
我们看一下回复数据包长啥样:
不太清晰,用SerializationDumper.jar
打开看下:
可以看到,返回的工具是一个继续了java.rmi.Remote
和org.las.remote.IRemoteHelloWorld
(自定义)接口的署理工具(TC_PROXYCLASSDESC
),同时也需要注重,在TC_BLOCKDATA
中也有器械。后面读的一些内容就是从这里拿的。
接着releaseOutputStream
的代码就最先对注册端回复的这个数据包举行处置。
首先验证了第一个字节是否为0x51
,接着是读取存储在BlockData中的一个字节和UID。
这里我们回过头看一下注册端最后RegistryImpl_skel,dispatch
的部门,由于我们实在并没有剖析它在挪用了api函数后是怎样发送数据包的。我们就以lookup
为例:
首先关注这个var2.getResultStream(true)
,在这个函数里会写入两个字节和UID。
接下来将lookup查询到的var8
写入流中。
然后回到挪用RegistryImpl_skel,dispatch
的上级方式UnicastServerRef,oldDispatch
:
这里挪用releaseInputStream
发送了缓冲的数据。
回过头来,适才剖析到StreamRemoteCall,executeCall
读取了BLOCK_DATA中的数据,但还没有读取要害的署理工具,继续看该方式的代码:
这里之前读到的var1
实在是1,以是我们return
到上一级。不外也能从这里看到,当读到的var2
是2时,是会在这里抛出异常的。
var6.readObject()
重新回到RegistryImpl_Stub,lookup
,这里通过了var23 = (Remote)var6.readObject()
来获取到lookup
查询到的署理工具。
最后把这个var23
返回,这就是lookup
的全过程了:
至于finally中的super.ref.done
大致就是将适才的流释放的操作。
此时的客栈很简练:
总结
- 以客户端定位到注册端并挪用
lookup
为例,客户端与注册端的通讯流程如下:- 客户端发送第一个握手包,注册端回复;
- 客户端发送第二个包罗ip和端口的包,注册端并不回复;
- 客户端发送
lookup
数据包,其内容是字符串的序列化。注册端返回一个序列化的署理工具。
- 注册端底层使用
UnicastServerRef
工具发送请求。与之相对的,客户端使用UnicastRef
。 - 注册端上层获取的工具是
RegistryImpl
,而客户端上层获取的是RegistryImpl_Stub
。
4. 客户端—服务端
编写服务端前首先得提供一个接口和一个远程工具。
我自己编写了一个远程挪用接口:
package org.las.remote; import java.rmi.Remote; import java.rmi.RemoteException; public interface IRemoteHelloWorld extends Remote { public Object helloWorld(Object word) throws RemoteException; }
一个远程工具:
package org.las.remote; import java.rmi.RemoteException; import java.rmi.server.UnicastRemoteObject; public class RemoteHelloWorldImpl extends UnicastRemoteObject implements IRemoteHelloWorld { public RemoteHelloWorldImpl() throws RemoteException{ } public Object helloWorld(Object word) throws RemoteException { System.out.println("Hello world.."); return "las"; } }
我的客户端代码:
import org.las.remote.IRemoteHelloWorld; import java.rmi.registry.LocateRegistry; import java.rmi.registry.Registry; public class RMIServer { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099); IRemoteHelloWorld helloWorld = (IRemoteHelloWorld) registry.lookup("Hello"); helloWorld.helloWorld("las"); } }
在剖析客户端和服务端之前,我想先贴出两者之间发生的流量。
红框框住的地方,是客户端在挪用远程方式间隙,执行的有关DGC
的操作。同时也可以看到,客户端发送的数据包有些太多了。除去握手包,注重第24和26两个包,基本不是我自己发的(远程挪用的数据包是32、34号)而且数据量比较大。通过将24、26的数据反序列化后会发现,它们也是和DGC
有关的。
关于DGC,简朴提一下,全称是distribute garbage collection
。实在它的存在也很合理,当客户端获取远程工具时,服务端需要建立一个工具,以供客户端来挪用其上的方式;然则这些工具也并不是永远存在的,它们也需要垃圾网络,这就引出了DGC
的观点。然则详细是若何实现的我不太清晰,这也是我剖析RMI源码的一个缘故原由,希望接下来的剖析能让我进一步明白它是若何实现的。
这一次我们先从客户端最先剖析。
在此之前,又先抛出另外一个问题。
新鲜的UnicastRemoteObject
我们知道,一个远程工具,要么得继续UnicastRemoteObject
类,要么得通过UnicastRemoteObject,exportObject
举行转换:
public class RMIServer { public static void main(String[] args) throws Exception{ System.setSecurityManager(null); IRemoteHelloWorld remoteHelloWorld = new RemoteHelloWorldImpl(); IRemoteHelloWorld remoteHelloWorld2 = new RemoteHelloWorldImpl2(); Object obj2 = UnicastRemoteObject.exportObject(remoteHelloWorld2,0); System.out.println(remoteHelloWorld); System.out.println(obj2); Registry registry = LocateRegistry.createRegistry(1099); registry.bind("Hello", remoteHelloWorld); registry.bind("Hello2", (Remote) obj2); System.out.println("[*] RMI Server started..."); } }
这里我们将两个工具都打印出来看看:
后者是一个署理工具,前者照样正常的RemoteHelloWorldImpl
。
然则在客户端看看呢:
public class RMIClient { public static void main(String[] args) throws Exception{ Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099); IRemoteHelloWorld helloWorld = (IRemoteHelloWorld) registry.lookup("Hello"); IRemoteHelloWorld helloWorld2 = (IRemoteHelloWorld) registry.lookup("Hello2"); System.out.println(helloWorld); System.out.println(helloWorld2); } }
打印效果:
都是署理工具!
这就新鲜了,在剖析客户端—注册端时,我们讨论了在挪用registry.bind
时只是单纯将工具放到registryImpl
的成员变量bindings
中:
在客户端lookup
时,注册端也只是从这个bindings
里将工具取出,并挪用writeObject
:
以是问题只可能出在writeObject
方式中。
我们跟进var9.writeObject
方式,在下面这个地方发现了敏感的函数replaceObject
,在进入前我们是RemoteHelloWorldImpl
,出来时就是署理工具了:
此时的挪用客栈:
进入MarshalOutputStream,replaceObject
瞧瞧:
这里的ObjectTable
看着怪熟悉的。它在哪个地方似乎泛起过。
实在是在TCPTransport,exportObject
中:
我们之前只跟进了this.listen()
,然则在它下面的super.exportObject(var1)
里:
这里!它将Target放到了ObjectTable
中。
然则到目前为止,我另有两个疑问:
- 服务端在什么时刻进入了
TCPTransport,exportObject
,然后将这个Target
放到了objectTable
里呢? - 在
replaceObject
中,挪用Target
上的getStub
方式获取到的工具和我们在服务端自定义的RemoteHelloWorldImpl
有什么关系呢?
由于我们的工具RemoteHelloWorldImpl
继续了UnicastRemoteObject
类,第一时间想到的就是它的组织函数。我预测也许是在新建RemoteHelloWorldImpl
时,挪用的父类无参组织函数做了些事情:
跟进:
发现它竟然挪用了exportObject
这个静态方式。它是我们建立一个远程工具的第二种方式。继续跟进:
跟进ㄟ( ▔, ▔ )ㄏ:
终于到头了。
这里获得了两个信息:
- 首先,两种导出远程工具的形式最终都市去执行
UnicastServerRef,exportObject
。 - 其次,接纳
UnicastRemoteObject.export(obj, port)
获取的远程工具,实在就是UnicastServerRef,exportObject
的返回值。
而这个方式,我在客户端—注册端实在已经剖析了。它的返回值就是一个stub
,而且在它冗长的挪用链中肯定会执行TCPTransport,exportObject
方式。这里又再贴一下UnicastServerRef,exportObject
方式:
至此,之前的第一个问题解决了。第二个问题:挪用Target
上的getStub
方式获取到的proxy
和我们自己的工具RemoteHelloWorldImpl
有什么关系呢?
进入Target
的组织函数可以看到:
而var3
就是我们刚刚建立的Stub
工具。
回到最最最最先,我们新鲜两种建立远程工具的方式最终在客户端获取到的都是同一种类型的问题也解决了:
- 使用
extends UnicaseRemoteObject
的方式,在writeObject
的时刻,会去获取到响应的Target
上的stub
工具。 - 使用
UnicastRemoteObject.exportObject(obj,0);
的形式,由于它自己返回值就是这个stub
,以是会直接被写入。
此时我仍然疑惑,这个stub
工具究竟是哪一个类?
跟进到UnicastServerRef,exportObject
中的Util.createProxy()
方式:
之前获取的stub工具是在上面if代码块中的createStub()
,
而这里则是建立了一个RemoteObjectInvocationHandler
的署理工具。
客户端
远程方式挪用
经过了上面的剖析,我们知道了客户端的调试需要从RemoteObjectInvocationHandler,invoke
方式最先:
前面几个if都直接跳过,直接进入到invokeRemoteMethod
:
进入到UnicastRef,invoke
方式,这里只贴出较要害的部门:
new StreamRemoteCall()
之前已经跟过一次,就是写入一个版本字节,同时在BLOCK_DATA处写入数据:
在for循环下的marshalValue()
部门,是将客户端执行方式传入的参数一个个写入到流中:
接下来跟进var7.executeCall()
,即StreamRemoteCall,executeCall
方式:
到this.releaseOutputStream();
为止,客户端的挪用请求被发送出去。接下来的地方又是老样子,读取首字节,然后获取BLOCK_DATA信息,若是没有抛异常则正常返回(见之前的剖析)。
紧接着在unmarshalValue()
方式中,将服务端执行命令后的返回工具举行了反序列化读取到var50
中,并作为了UnicastRef,invoke
的返回值。
dgc剖析
等等,是不是什么器械漏了?明显另有DGC这些器械啊。它们的流量似乎在RemoteObjectInvocationHandler,invoke
之前就发出了。
调试后发现,有关DGC
的那段流量,在客户端执行registry.lookup
方式时就发出了:
仔细调试后,定位到执行RegistryImpl_Stub,lookup
finally块中的代码会发出这些流量:
不停跟进,可以来到StreamRemoteCall,releaseInputStream
处:
registerRefs
意味注册引用。看名字有点像是在告诉远端的JVM对工具增添引用的意思。
跟进:
这里获取到的var5
类型是List<LiveRef>
。
跟进DGCClient,registerRefs
:
这里也许有一些难明,然则参考jdk源码的注释则一目了然:
/** * Register the LiveRef instances in the supplied list to participate * in distributed garbage collection. * * All of the LiveRefs in the list must be for remote objects at the * given endpoint. */ static void registerRefs(Endpoint ep, List<LiveRef> refs) { /* * Look up the given endpoint and register the refs with it. * The retrieved entry may get removed from the global endpoint * table before EndpointEntry.registerRefs() is able to acquire * its lock; in this event, it returns false, and we loop and * try again. */ EndpointEntry epEntry; do { epEntry = EndpointEntry.lookup(ep); } while (!epEntry.registerRefs(refs)); }
也就是说,每一个Endpoint
,它可能有不只一个LiveRef
,我们要在这个循环中向Endpoint
注册所有这些LiveRef
。这里,Endpoint
就明白为某个主机上监听在某个端口的历程就行了。
接下来跟进epEntry.registerRefs
,即DGCClient,registerRefs(List)
:
跟进DGCClient,makeDirtyCall
:
跟进DGCImpl_Stub,dirty
:
可以看到,在这里发送了dgc
请求,而且收到服务端的回复是一个java.rmi.dgc.Lease
工具。
而且我们在这里再次看到了“_stub“字眼,再次说明skel和stub的观点不只用于客户端和服务端之间。
这个Lease
工具的作用可见官方注释:
/** * A lease contains a unique VM identifier and a lease duration. A * Lease object is used to request and grant leases to remote object * references. */
发现它确实有一个成员变量来示意工具的存活时间:
回过头来,再看一下DGCClient
这个工具上官方的注释:
/** * DGCClient implements the client-side of the RMI distributed garbage * collection system. * * The external interface to DGCClient is the "registerRefs" method. * When a LiveRef to a remote object enters the VM, it needs to be * registered with the DGCClient to participate in distributed garbage * collection. * * When the first LiveRef to a particular remote object is registered, * a "dirty" call is made to the server-side distributed garbage * collector for the remote object, which returns a lease guaranteeing * that the server-side DGC will not collect the remote object for a * certain period of time. While LiveRef instances to remote objects * on a particular server exist, the DGCClient periodically sends more * "dirty" calls to renew its lease. * * The DGCClient tracks the local reachability of registered LiveRef * instances (using phantom references). When the LiveRef instance * for a particular remote object becomes garbage collected locally, * a "clean" call is made to the server-side distributed garbage * collector, indicating that the server no longer needs to keep the * remote object alive for this client. * * @see java.rmi.dgc.DGC, sun.rmi.transport.DGCImpl * * @author Ann Wollrath * @author Peter Jones */
这里看到,自挪用StreamRemoteCall,releaseInputStream
中的this.in.RegisterRefs
以来,客户端所作的事情,确实是向服务端注册每一个LiveRef
,而且获取到服务端返回的Lease,来告诉服务端短时间内不要接纳这些工具。至于像注释说的,租约lease到期后需要再次发送dirty call
的操作,就不再跟进了。
服务端
我们在客户端—注册端一节,调试LocateRegistry,createRegistry
时曾跟进了UnicastServerRef,exportObject
方式。它的挪用栈会在某个地方执行this.listen()
方式监听在某个端口接受客户端的请求;
回到客户端—服务端,由于前面剖析过,建立一个远程工具无论怎样都市挪用UnicastRemoteObject,exportObject
方式:
在这里也挪用了UnicastServerRef,exportObject
方式。
以是服务端和注册端一样都通过了UnicastServerRef
工具来开放服务。这里就不进一步跟进了。
需要强调的是UnicastServerRef,dispatch
方式,我前面也提到过:
在远程方式挪用时,它是不会进入这个if分支的,而是会继续向下执行。
它首先会从返回数据中读取一个数据类型为Long的变量:
var4 = var39.readLong();
这个long变量实在是客户端想要挪用方式的哈希,当在this.hashToMethod_Map
获取不到时会报错:
在学习使用反序列化攻击rmi服务端时,若是在客户端自己魔改了远程工具的接口方式(好比参数类型原本在服务端是java.lang.String
,但在客户端修改为java.lang.Object
),此时需要修改这个哈希值才气正常攻击。
总结
- dgc流程:
- 客户端通过
lookup
获取到一个工具后,会向服务端提议一次dirty call
,以通知服务端短时间内不要接纳该工具; - 服务端返回给客户端一个
lease
,该工具告诉了客户端接下来多久的时间内该工具是有用的。若是客户端在时间到期后还需要使用该工具,则需要继续挪用dirty call
; DGCClient
会跟踪每一个liveRef
,当他们在客户端已经不再有用后,就会提议clear call
告诉服务端可以接纳有关工具了。
- 客户端通过
- 无论是rmi注册端照样服务端,它们都通过
UnicastServerRef,exportObject
开启指定端口上的服务,最终都市进入TCPTransport,handleMessages
中的循环来监听输入流,而且最后又都市使用UnicastServerRef,dispatch
来挪用注册端或者服务端的功效。
参考链接
- 针对RMI服务的九重攻击
- Java平安-RMI-学习总结
- DGCClient源码
Allbet声明:该文看法仅代表作者自己,与www.allbetgame.us无关。转载请注明:usdt充值接口(caibao.it):深入学习rmi事情原理