看RMI漏洞时候,对其漏洞原理并不是很理解,所以简单调试了下源码加强下漏洞理解
由于要调试到RegistryImpl_Stub这种动态类,刚开始用的源码版本是JDK8u141,后来发现源码有些地方进行了修改,故此换回了JDK 7u80
以下是源码版本JDK 7u80的源码
创建注册中心
createRegistry
Registry registry = LocateRegistry.createRegistry(1099);
从上面这句代码入手,追溯下去,可以发现服务端创建了一个RegistryImpl对象。
![image-20210721160608629](https://i-blog.csdnimg.cn/blog_migrate/e05794d94c77aea88ec09ca2321a46ad.png)
LiveRef var2 = new LiveRef(id, var1);这里进行了一些数据的封装并赋值给var2
![image-20210720101224511](https://i-blog.csdnimg.cn/blog_migrate/556678a5722de1b36b3ad54fd8553b3d.png)
跟进下一行this.setup()
1.首先创建了个UnicastServerRef对象,把刚刚封装的var2传入了进去
![image-20210721160757652](https://i-blog.csdnimg.cn/blog_migrate/44600d8dd73e2f0cf5743743346b7244.png)
2.跟进下一行setup,把创建UnicastServerRef的对象作为var1传入了setup()中
![image-20210721160852013](https://i-blog.csdnimg.cn/blog_migrate/d42caa635307fcffb354c89dd334009f.png)
在setup()中调用了UnicastServerRef的exportObject()方法
![image-20210721160953929](https://i-blog.csdnimg.cn/blog_migrate/537f95d210de8b0efca748f5ec92de4f.png)
跟进UnicastServerRef的exportObject()方法
![image-20210721161102356](https://i-blog.csdnimg.cn/blog_migrate/80b67d459521434203a831e24a0e491e.png)
第85行创建了RegistryImpl的代理对象RegistryImpl_stub,这个对象就是后面服务于客户端的RegistryImpl的Stub对象
![image-20210721161201211](https://i-blog.csdnimg.cn/blog_migrate/567b35ef6f485afd11b71168e75eb233.png)
第91行传入RegistryImpl,创建出RegistryImpl_Skel对象
![image-20210721161242428](https://i-blog.csdnimg.cn/blog_migrate/b6c10dbbea3e7fe1d0c9a0ac28fa97d9.png)
继续回到前面,看94行,这里用skeleton、stub、UnicastServerRef对象、ObjID和一个boolean值等构造了一个Target对象,也就是这个Target对象基本上包含了全部的信息
![image-20210721161658318](https://i-blog.csdnimg.cn/blog_migrate/2aae4373f2188b41f0a5f929e9a90720.png)
在这上面都是没有进行网络操作,只是进行了一些对象的创建和变量的赋值操作。
接下来就是网络层的一些操作了
调用this.ref的exportObject方法,传入了Target对象,跟进LiveRef的exportObject
![image-20210721161754589](https://i-blog.csdnimg.cn/blog_migrate/3bf7adb7558d02a7700d2a520c972267.png)
又调用了TCPEndpoint的exportObject方法,然后最终到达了TCPTransport的exportObject方法。以下是调用栈
![](https://i-blog.csdnimg.cn/blog_migrate/3a16760cb29189cdc1338e0c5fcbdc4f.png)
TCPTransport的exportObject方法做的事情就是将上面构造的Target对象暴露出去。
![image-20210721161854206](https://i-blog.csdnimg.cn/blog_migrate/67af456933faecf60e09f4e80edced98.png)
此时的this是TCPTransport对象,跟进TCPTransport的listen()方法
![image-20210721161937919](https://i-blog.csdnimg.cn/blog_migrate/9bac28dca70366fb06a94c38fb4461d8.png)
调用TCPEndpoint#newServerSocket方法,会开启端口监听
![image-20210720142736629](https://i-blog.csdnimg.cn/blog_migrate/37d8f98ffbe1ac1fda4f8cc9ad22a58c.png)
接下来的第231行就是启动一条线程,等待客户端的请求
![](https://i-blog.csdnimg.cn/blog_migrate/9bac28dca70366fb06a94c38fb4461d8.png)
回到TCPTransport#exportObject,往下看
![image-20210721162046054](https://i-blog.csdnimg.cn/blog_migrate/493bc728d7079e078a7917475ba6bcf3.png)
调用了父类Transport的exportObject()将Target对象存放进ObjectTable中
![image-20210721162130829](https://i-blog.csdnimg.cn/blog_migrate/f32622a3fd4439d85cce1765d2e3035c.png)
注册中心处理请求
接上,在TCPTransport#listen中
![image-20210721162252201](https://i-blog.csdnimg.cn/blog_migrate/a3d8c48cc958e773f53dbd3e7c386896.png)
跟进AcceptLoop()到executeAcceptLoop
![image-20210721162350489](https://i-blog.csdnimg.cn/blog_migrate/38cf14e6e23c87013c54e5437b43ccc1.png)
这里就是获取了请求中的一些信息,在371行创建一个线程,调用内部类ConnectionHandler来处理请求
![image-20210721162439831](https://i-blog.csdnimg.cn/blog_migrate/eccc6abe69b78948aec94bbbfcf0c5b1.png)
进入ConnectionHandler,此内部类就是用来处理请求的
![image-20210721162657289](https://i-blog.csdnimg.cn/blog_migrate/a4a1c05422f3a5954b6aef078991b1f0.png)
接收请求,在run()中获取的ServerSocket对象
![](https://i-blog.csdnimg.cn/blog_migrate/91efea8ae809b612c1fd4a217d6f1038.png)
跟进上图中的run0()后,handleMessages来处理请求
![image-20210716151754153](https://i-blog.csdnimg.cn/blog_migrate/b46d183deb847238d449369491372f4b.png)
进入handleMessages(),在之前获取一些客户端传过来的数据。然后转到case 80
![image-20210720145438170](https://i-blog.csdnimg.cn/blog_migrate/4aaddfe3685d1d660f3b2f43e422d1ec.png)
先是创建了一个StreamRemoteCall对象,并传入var1,var1是当前连接的Connection对象
![](https://i-blog.csdnimg.cn/blog_migrate/eb3f1abdceb3d75980eb4f7dff7cec89.png)
紧接着跟入下一行TCPTransport#serviceCall
![image-20210721163157301](https://i-blog.csdnimg.cn/blog_migrate/08f892c527047f534962d7a0e45aa84f.png)
这里获取了OBJID,后面会获取到Target对象
最后在下面调用了UnicastServerRef#dispatch来处理请求
![image-20210716152137008](https://i-blog.csdnimg.cn/blog_migrate/6113536ea5e0c71399f88b49054ea256.png)
接着跟dispatch:
![image-20210721163511205](https://i-blog.csdnimg.cn/blog_migrate/321890c7f2a71bf8626e14f00ab0f906.png)
这里传递的两个参数,一个是Target对象,一个是当前连接的StreamRemoteCall对象,
![image-20210721163445933](https://i-blog.csdnimg.cn/blog_migrate/6112f500acf60f004cbb2d0dea90f41f.png)
接着调用到了oldDispatch
![](https://i-blog.csdnimg.cn/blog_migrate/53f53c0f6b9fb086df93c3b425ea46b0.png)
跟进oldDispatch,在最后调用到了this.skel.dispatch
![image-20210721163602416](https://i-blog.csdnimg.cn/blog_migrate/c1239a14c7aac4ebb808ce0b8cf96e90.png)
可以看到this.skel就是之前创建的RegistryImpl_Skel对象,也就是说UnicastServerRef#dispatch最后会调用到RegistryImpl_Skel#dispatch来处理请求
接着跟RegistryImpl_Skel#dispatch方法:
var3是一个int类型的数组,分别对应了
- 0->bind
- 1->list
- 2->lookup
- 3->rebind
- 4->unbind
然后会进入case对方法进行处理,可以看到case 0对应的是bind,有执行readObject,而最后var6.bind中的var6是RegistryImpl对象,由createRegistry获得
比如调用了bind,则会进入RegistryImpl_Skel#dispatch中,先反序列化readObject传过来的序列化对象,之后进行var6.bind来注册服务,而var6则是RegistryImpl对象。也就是说无论是客户端还是服务端,最终其调用注册中心的方法都是通过创建的RegistryImpl对象进行调用的。
获取注册中心
getRegistry
在前面createRegistry获得的是RegistryImpl对象,而getRegistry获得的是RegistryImpl_Stub对象
Registry registry = LocateRegistry.getRegistry("192.168.202.1",1099);
直接查看getRegistry源码最后返回的是一个RegistryImpl_Stub对象
![image-20210720154718641](https://i-blog.csdnimg.cn/blog_migrate/74c3134557dfb3fbbff1689f96303ecb.png)
调用bind绑定到注册中心
在两种方式获取的RegistryImpl和RegistryImpl_Stub对象中,其bind方法有什么区别呢
RegistryImpl#bind
在第一步会checkAccess,里边有一些判断,会对你当前的权限、来源IP进行判断。
之后判断这个键是否已经被绑定过,如果已经被绑定过,则抛出一个AlreadyBoundException的错误,否则将键和对象都put到Hashtable中。
这里的bindings是一个Hashtable,以键-值的方式存储了服务端注册的服务。
![image-20210716154012509](https://i-blog.csdnimg.cn/blog_migrate/12cfd6f0b6525c7a93c2f3bef6475733.png)
RegistryImpl_Stub#bind
进入RegistryImpl_Stub#bind,这里的var3,前面说过,bind方法对于的数字为0,此时的var3就代表了bind方法对应的数字
![image-20210721165241073](https://i-blog.csdnimg.cn/blog_migrate/bd19427484a31d1bcf637111f2649a58.png)
调用UnicastRef#newCall,将RegistryImpl_Stub对象传了进去,RegistryImpl_Stub存了一些服务器相关的信息。
简单来看newCall就是与远程RegistryImpl的Skeleton对象的连接
![image-20210721165333020](https://i-blog.csdnimg.cn/blog_migrate/201c47fd707fb71a185e18ccc97d0085.png)
new newConnection用来创建一个TCPConnection对象,里面写了一些ip、端口
new StreamRemoteCall创建了一个StreamRemoteCall对象,把var6,LiveRef,ObjID,调用bind方法的值var3和4905912898345647071传入了StreamRemoteCall对象
![image-20210721165428771](https://i-blog.csdnimg.cn/blog_migrate/4c67e1e738ad45e3e8a0ee9658321ce0.png)
以上两个都是进行数据的封装
调用完后,回到RegistryImpl_Stub#bind(因为这里RegistryImpl_Stub是动态类,故用了jdk8u141的源码调试的截图)
![image-20210720163445451](https://i-blog.csdnimg.cn/blog_migrate/4b3d8656bd16c128302dc805d9b418f0.png)
序列化两个内容
- 序列化后的var1,var1为我们要绑定远程对象对应的名称
- 序列化后的var2,var2为我们要绑定的远程对象
最后调用invoke方法把请求发出去,注册中心就会接收到请求调用RegistryImpl_Skel#dispatch进行处理。
以下是注册中心的RegistryImpl_Skel#dispatch处理请求
注册中心首先会反序列化readObject两个对象,第一个和第二个对应上面两个writeObject写入的对象,绑定远程对象对应的名称和要绑定的远程对象
接着调用var6.bind来绑定服务,var6即RegistryImpl对象,调用到RegistryImpl#bind把键和对象都put到Hashtable中。
创建远程对象
其实创建远程对象的时候,和创建注册中心是类似。生成其存根stub和skel,并且会绑定随机一个端口发布
在这里下个断点
![image-20210720201106091](https://i-blog.csdnimg.cn/blog_migrate/895dd33c2a3dbb6b34b676cc9fdf35f1.png)
HelloImpl继承了UnicastRemoteObject
![image-20210720201155496](https://i-blog.csdnimg.cn/blog_migrate/07590ca0bcb63cf9571f7aed6743f451.png)
跟进super方法,传入port为0
![image-20210720201218588](https://i-blog.csdnimg.cn/blog_migrate/71b2efad533f6ffc73ba4942b916ab72.png)
这里调用了exportObject,并且传入了HelloImpl对象
![image-20210720201412353](https://i-blog.csdnimg.cn/blog_migrate/4757e64cc5578975fde13166d123935a.png)
跟进exportObject
![image-20210720201936431](https://i-blog.csdnimg.cn/blog_migrate/d652b0cb936acbe6f29ecb6906e4f3c9.png)
new UnicastServerRef()就是封装了一些host,ObjID等
![image-20210720201819801](https://i-blog.csdnimg.cn/blog_migrate/1c2a6a5b631608f83e869d985860ea4f.png)
经过了几次exportObject调用后,到了UnicastServerRef#exportObject
![image-20210721165957623](https://i-blog.csdnimg.cn/blog_migrate/19b6f2dac254837c325d87b7301f6b85.png)
Util.createProxy创建了一个远程对象的代理对象proxy。这点和之前注册中心的并不同,注册中心创建的是RegistryImpl_Stub
跟进Util.createProxy看到,后面返回了动态代理对象,而其invocationHandler值为RemoteObjectInvocationHandler(远程调用该对象时,客户端获取到的其实是该代理对象)
回到UnicastServerRef#exportObject,看到var5的值最终是一个动态代理类
![image-20210721170152141](https://i-blog.csdnimg.cn/blog_migrate/2e17b9cede7a5672b8730441c3e0478f.png)
继续往下看,if判断是否为RemoteStub类,显然RemoteObjectInvocationHandler不属于RemoteStub,返回false,不执行if里面的语句
![](https://i-blog.csdnimg.cn/blog_migrate/2e17b9cede7a5672b8730441c3e0478f.png)
下面构造了一个Target对象,然后调用this.ref的exportObject方法,传入了Target对象
![image-20210721170246121](https://i-blog.csdnimg.cn/blog_migrate/433838679b9a1a6440a1346073edb0b1.png)
下面就是网络层的操作了
跟进exportObject,最终调用 TCPTransport#exportObject 方法
![image-20210721170332390](https://i-blog.csdnimg.cn/blog_migrate/dce88920a9e4ef15fb1725fab81e0a58.png)
跟进listen()
![image-20210721170409240](https://i-blog.csdnimg.cn/blog_migrate/e0360467edcdffda8f975fb1a1624db7.png)
调用newServerSocket开启一个随机端口监听,这里开启的是11061
![image-20210721170425716](https://i-blog.csdnimg.cn/blog_migrate/da2684e92544284ee186a7dbb75c9e30.png)
回到TCPTransport#exportObject。
最终服务暴露(exportObject)时做了两件事,一是如果 socket 没有启动,启动 socket 监听;二是将 Target 实例注册到 ObjectTable 对象中。
通信调用
接下来讲客户端与服务器之间通信是怎么进行的
客户端与服务端的通信
客户端代码:
//获取远程主机对象
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
//查找注册中心得hello对象
Hello hello = (Hello)registry.lookup("hello");
//调用方法
System.out.println(hello.hello("yangyang"));
客户端与服务端的通信发生在调用远程方法时
hello.hello(); //调用方法
前面两行,拿到的hello对象是proxy代理对象,且该代理对象的handler为RemoteObjectInvocationHandler
此时是远程代理对象RemoteObjectInvocationHandler与服务器的Skel进行通信
在Proxy调用方法时候,会进入对应的InvocationHandler的invoke方法中,也就是RemoteObjectInvocationHandler的invoke方法
![image-20210721170640773](https://i-blog.csdnimg.cn/blog_migrate/80eef04b493817fd60ebf81ac4818f29.png)
在这个做了个if判断为false,跳转到else中
跟进invokeRemoteMethod
![image-20210721170849011](https://i-blog.csdnimg.cn/blog_migrate/276a5b8f9979f0b6bd2a940bd4e6aaf8.png)
这里调用了this.ref.invoke,而this.ref则是UnicastRef对象
跟进UnicastRef#invoke
和前面一样newConnection用来创建一个TCPConnection对象,里面写了一些ip、端口
new StreamRemoteCall创建了一个StreamRemoteCall对象,把TCPConnection对象,ObjID,-5976794856777945295传入了StreamRemoteCall对象
然后往下看,for循环条件满足(前提是传递的参数是一个对象)跟入下面的marshalValue
![](https://i-blog.csdnimg.cn/blog_migrate/1328e056181cb8a59c54c4a83c995028.png)
这个方法是把调用方法的参数进行了序列化
![image-20210721171028950](https://i-blog.csdnimg.cn/blog_migrate/42f2e9995a6f1db80d69a6d0b8b9a0fa.png)
回到UnicastRef#invoke,下面调用了executeCall()方法
![image-20210721171101813](https://i-blog.csdnimg.cn/blog_migrate/0c186a2e72e51af83f343e49d2b675b6.png)
跟进StreamRemoteCall#executeCall方法,这个方法就是用来发送给服务端数据的了
![image-20210721171128769](https://i-blog.csdnimg.cn/blog_migrate/23c14a8c50eb0e582a6daaacc3389593.png)
继续跟进releaseOutputStream方法,this.out.flush就是把数据发送给服务端
![image-20210721152551532](https://i-blog.csdnimg.cn/blog_migrate/0df3891835b2a67da8e56bfe015b7ea9.png)
发送完数据后,回到UnicastRef#invoke,下面的unmarshalValue
unmarshalValue则是接受服务端发过来的数据进行readObject读取,然后赋值给var13返回。
![image-20210721153035565](https://i-blog.csdnimg.cn/blog_migrate/f8f58329946468e06c16e236dd2d4814.png)
到这里整个客户端与服务端通信流程就走完了,下面来看下服务端和客户端的通信
服务端和客户端的通信
和注册中心一样,服务端的接收到请求后,最后会进入UnicastServerRef#dispatch进行处理
![image-20210727141831779](https://i-blog.csdnimg.cn/blog_migrate/d63e72299675018c193799d44a7a60ed.png)
这里有两次处理请求:
第一次是在lookup的时候
![image-20210727141747496](https://i-blog.csdnimg.cn/blog_migrate/63ebaeae06c8dae56e4f0a694e19ea3d.png)
当执行lookup的时候,会进入UnicastServerRef#dispatch中,到124行进入oldDispatch
跟进oldDispatch,看到调用了DGCImpl_Skel的dispatch处理
第二次是发生在UnicastServerRef#dispatch中的第151行进行处理
当传入的参数类型是一个Object的时候,这里也会进行unmarshalValue
![image-20210721155635914](https://i-blog.csdnimg.cn/blog_migrate/6a8e6e18cab676b0d82815d9f57b797c.png)
unmarshalValue中进行了readObject,此处就是服务端反序列化触发的一个点