RMI_1
前言
今日开始java中RMI的学习
先记录一下RMI的坑点:RMI攻击手法只能在jdk8u121之前可以使用,因为在8u121之后bind rebind unbind这三个方法只能对localhost进行攻击
环境准备
- jdk8u65
RMI基础
RMI介绍
RMI (Remote Method Invocation) 模型是一种分布式对象应用,使用 RMI 技术可以使一个 JVM 中的对象,调用另一个 JVM 中的对象方法并获取调用结果。这里的另一个 JVM 可以在同一台计算机也可以是远程计算机。因此,RMI 意味着需要一个 Server 端和一个 Client 端。
Server 端通常会创建一个对象,并使之可以被远程访问。
这个对象被称为远程对象。Server 端需要注册这个对象可以被 Client 远程访问。
Client 端调用可以被远程访问的对象上的方法,Client 端就可以和 Server 端进行通信并相互传递信息。
说到这里,是不是发现使用 RMI 在构建一个分布式应用时十分方便,它和 RPC 一样可以实现分布式应用之间的互相通信,甚至和现在的微服务思想都十分类似。

来解释一下这个图:client,server,registry server将地址和端口注册在registry,client通过获取registry上的目标地址以及端口,对server进行一个远程的调用
RMI的实现
server
先是定义了一个IRemoteOBJ接口,这个接口要求public,并且继承Remote接口
1 | import java.rmi.Remote; |
然后因为是服务端,被调用的地方这里肯定要写一个实现类RemoteOBJ的
这里继承的UnicastRemoteObject,用于生成Stub(存根) skeleton(骨架)
1 | import java.rmi.Remote; |
最后是我们需要去注册,方便客户端的使用
默认端口是1099
1 | import java.rmi.RemoteException; |
client
一样客户端也要写一个接口
1 | import java.rmi.Remote; |
然后写一个RMIclient,通过这个类来连接服务端,从而进行一个调用
1 | import java.rmi.NotBoundException; |

在服务端这里调用成功,并且得到了HELLO
其实在这整个过程里面都是通过序列化和反序列化实现的
从wireshark分析通信原理
客户端与注册表之间的交互

先是tcp的三次握手
然后接下来每一个RMI包后会跟一个tcp包,这个tcp包是一个确认包

这里客户端向注册中心询问rmiserver的地址和端口


这里给予回应,可以看到是明显的序列化
ac de 00 05 就是序列化的标志
客户端另起一个端口与服务端的交互
客户端与服务器进行一个交互,同样是使用序列化进行传输

1 | Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099); |
这应该是建立一个远程连接,然后到
参数的传输

发送hello

最后调用成功得到HELLO
其实在整个过程中我们能看到,客户端与服务端发送数据包的内容都是序列化之后的数据流,那么客户端与服务端肯定都有反序列化的功能。
总结
在整个过程中建立了两次tcp的连接,第一次是客户端去连接1099的,第二次是服务端发送给客户端的。
第一次连接中,客户端会连接到注册中心,并传入序列化的name也就是要找的rmiserver,然后注册中心反序列化读取这个字节流。
第二次连接中,服务端发送给客户端 Call 的消息。客户端反序列化该对象,发现该对象是⼀个远程对象,并得到它的地址,客户端与其进行连接,实现远程调用。
其实就是,服务端创建远程对象,bind远程对象到注册中心上时,会给一个name,然后客户端带着name在注册中心上取得这个远程对象,建立连接,从而能够远程调用。
从代码方面分析通信原理
注册中心:存储名字和远程对象
服务端,客户端和注册中心,三者相互通信,所以一共有6个过程

1.创建远程对象
创建远程对象这一块是不存在漏洞的。
远程对象的创建点在这里,我们在这边打个断点开始调试

到这边是对远程对象进行一个发布
发布远程对象
这里我们主要研究的是如何将远程对象发布到网络上
首先RemoteOBJ这个类继承UnicastRemoteObject这个类,所以这边会到父类的构造函数

然后会先到第一个点的位置,这里传的参数是0,所以继续向下运行,会将0赋值给port也就是第二点的位置,这里port为0也就相当于我们这里发布的远程服务会随机调用一个端口。注意点:这里的端口为远程对象的端口和1099没有关系,1099是注册中心开放的端口

这里的exportObject类,叫做发布对象/导出对象,从名字就可以看到这是一个发布远程对象的关键类,这也是上述远程对象类,可以不继承un那个类而直接可以手动调用exportObject这个类的原因。

这里第一个参数就是我们要实现逻辑的这样一个对象,第二个参数调用了Unicaji

继续跟进

我们可以看跟进看一这个方法调用了什么

方法返回一个TCPEndpoint, 看看它的构造函数,其实很明了,这个类就是处理网络请求的一个类。

然后我们回到LiveRef的构造方发中,可以看到endpoint(接口),也就是刚刚返回的TCPEndpoint,这里包含了端口和ip。

这里才是真正处理网络请求的东西,前面的几个类其实都是封装,这里重点标记一下这个类(LiveRef)
然后就是在不同的类里调用exportObject
这里会创建这个stub(客户端的一个代理),为什么stub这个代理会出现在这里呢?其实是服务端,在发布远程对象的时候创建了这个代理,并将这个代理放到了注册中心,然后客户端从操作中心拿,拿到后,通过这个stub调用服务端的一个代理,最后调用到远程对象。

跟到这里看如何创建这个代理的

这里封装了远程对象

到这边创建一个动态代理

我们接着走到Target,这是一个总的封装类

到这边我们可以看到,服务端的ref(处理网络请求的引用)和stub(客户端代理)的ref是同一个ref,也就是说,客户端和服务端要进行连接通信,用的是同一个东西。

同时,这里Target的id使用的也是LiveRef的id,所以核心就是LiveRef

封装完成之后,对封装的Target进行一个对象的发布,我们看一下它实现了什么逻辑


这边调用了这个1051的TCPTransport,这边会正式的处理网络请求,listen会开启一个端口,跟进。

开启一个新的服务端的Socket服务,等待连接。

开启服务,等待连接之后的逻辑,应该是处理连接之后做什么,这边开启了新的线程来处理连接之后的逻辑
注意点:这里开启了新的线程,说明网络请求的线程和代码的线程是独立的。

出了listen之后我们可以看到端口变成了65152,进行了一个端口的随机分配,这里可以往回看一下,具体的一个端口分配。

在创建一个新的Socket服务的时候,端口被分配了随机的值

实际上已经完成发布了,这里会进行一个记录,两个Table是两个map,target是我们刚刚讲的一个总的封装,这里使用put,把这样整个东西,存在系统的一个静态的表里(相当于日志记录)。
然后自此发布远程对象的调试就结束了。
来一个小小的总结:
其实在分析过程中,虽然步入了很多方法,这些方法实现了各自的功能,但是如果从整体来看,整个的过程其实就是在使用exportObject方法,可以说整个调试的过程,是一个逐渐细化的一个过程,找到到底是哪里真正发布远程对象的一个过程。
2.创建注册中心
创建注册中心

创建注册中心,跟进后有一个安全检查,我们直接跳了。

跳过之后在进,然后向下调,我们可以来到这里,创建一个LiveRef,并且封装进UnicastServerRef,这里应该很熟悉和我们刚刚发布服务端相同的一个操作。
我们可以回过头来看一下发布远程对象的操作,来巩固一下

这里呢,和发布对象有一个明显的区别就是
发布远程对象时,这里传的一个参数是false
但是,在创建注册中心时,这里却用了true

这里的参数名时permanent,中文意思是永久的,那么就很明显了,我们之前自定义的那个远程对象是临时的远程对象,而现在的这个远程对象是永久的远程对象,这是两者之间的差别。
然后继续向下调就开始创建代理了,这里不同于发布远程对象的地方是,发布远程对象时,是没有stub的(使用动态代理),但是我们现在创建注册中心是有stub的(直接调用creatStub方法)
看一下这里的判断,如果有返回true,如果没有返回false
至于在哪里呢?
可以开全局搜索一下
所以这里是可以进入到creatstub这个方法的,(发布远程对象是没有进入到这个方法,调用了一个动态代理)
creatStub这个方法向里存了一个ref,发布远程对象时也是一样存了ref
总结:其实就是创建Stub的方法不同,一个是使用动态代理,一个是使用creatStub方法。

然后会走到setSkeleton这个方法,Skeleton是服务端的一个代理。

进到这里,在啰嗦一遍哈,服务端与客户端要进行通信,两边都需要一个网络代理,客户端的代理叫做stub,而服务端的代理叫做skeleton。

和刚刚的stub一样是直接创建的,不是使用动态代理。这个skel是一个内部变量

到这里,impl中就会添加一个skel的这样一个变量
在这之后就是全装到Target里,然后发布,记录,我们直接看最后都记录了什么
所有创建好的远程对象都会放在objTable里面

这里有三个远程对象,为什么有三个,这里先放一下
我们先来看第二个
先看DGC(分布式垃圾回收),这不是我们创建的,但是它有,是默认创建的。
在看第三个
这个是我们之前创建的远程服务的一个远程对象(使用的动态代理)
这里面是没有skel的

unicastServerRef和stub开启的端口也是一样的
然后我们看第一个
这里肯定是有一个skel的,然后端口也都是1099,就不看了。
绑定的流程

检查是在本地绑定的
其实就是一个hashtable,把远程对象put进去,name就是自己取的名字
服务端的整体过程:创建远程对象,创建注册中心,绑定。
3.客户端请求注册中心-客户端
客户端做的两件事:1.从注册中心获取远程对象的代理,2.通过远程代理对服务端进行远程调用
在印象中可能会觉得获取远程对象是通过序列化和反序列化,但是实际上这里是进行了一个创建,然后注册中心只要给参数就可以了。然后就是创建了一个liveRef,封装了一下。

这里创建一个代理,然后继续向下调一下

调出来之后,我们看这里的liveRef,这里就获取了注册中心的stub对象。
下一步就开始获取远程对象
这里把名字给过去,然后去获取远程对象的代理。(在注册中心上)
这边就走不到想要走的地方,也就是lookup方法上,调试不了(这里是版本有点bug),只能去看class文件了。
创建一个连接

1的位置传入一个字符串(这个字符串就是我们查找的名称),2的位置将这个字符串序列化,客户端这边又一个序列化,那么到注册中心那里肯定就又一个反序列化了(待会会分析)。
然后会调用到invoke(一个激活的方法),在invoke里会调用到executeCall方法

他这个方法是客户端真正处理网络请求的方法。
在接着还是回到之前的那个类里
进行网络连接之后,这边获取到了返回值,然后进行了一个反序列化。这里的var23就是传回来的远程对象的动态代理。所以客户端想注册中心获取远程对象的代理是通过序列化和反序列化来实现的。这时,如果有一个恶意的注册中心,就可以使用这个点来对客户端进行攻击(这是第一个点)。
来调一下executeCall这个方法。
在这个函数里面有一个处理异常的方法,这里会进行一个反序列化。如果是一个异常类,这里会通过反序列化获取一个具体的信息。所以,如果注册中心返回了一个恶意的流,到客户端这里会被反序列化(第二点,这里更隐蔽,更容易被利用,因为所有的stub里,调用网络请求都会调用到这个方法)。也就是说这第二点中,如果RegistryImpl_Stub这个类里的方法被调用,然后这个方法又调用到了invoke,就有可能调用到readObject,从而被反序列化。
小总结:这里主要是客户端连接注册中心,首先,客户端拿stub对象,这里不是通过序列化和反序列化,而是通过客户端的创建,注册中心的传参完成,然后就是客户端获取服务端的远程对象,这里是通过序列化和反序列化进行完成的(漏洞点)。
4.客户端请求服务端-客户端

remoteOBJ是一个动态代理,动态代理无论你调用什么方法,都会走到你调用处理器的invoke方法
走到这边。
进入这个方法里,在去看invoke方法的逻辑
调到这里,这里的marshalValue方法会序列化一个字符串
这里实际上次就是这个参数
然后这里又调用了executeCall方法,所有客户端的,如果要处理网络请求都要走这个方法。(一样,这里就有可能被攻击)(调用了jrmp协议进行攻击)
到这边会判断有没有返回值然后调用unmarshalValue,这里我们是有返回值的调用到unmarshalValue,然后跟一下他
服务端会传来一个序列化的值,客户端通过反序列化来获取。
小总结:还是有两个漏洞点,第一个是调用到了executeCall进行攻击,也就是使用jrmp协议进行攻击,第二个就是在服务端返回参数时进行的反序列化攻击。
5.客户端请求注册中心-注册中心
要开始分析注册中心了,那么就要考虑怎么调试,怎么下断点呢,先回到刚开始给的图中
我们刚才在对客户端的分析当中,使用的都是stub,而现在我们要操作服务端,所以要使用skel,skel是封装进target了,所以这边断点要下在target了。
一步步细化找到target的位置,然后打下断电,从这里开始分析。
我们直接开服务端,然后客户端发送请求就断在我们要断的位置了。
在这里我们来看一下target里是什么
首先是我们创建的stub,然后在disp里也有skel
那么接下来我们就跟disp
这边调用了dispatch方法
到oldDispatch,还是跟着skel走
调用了skel的dispatch方法,这里和客户端一样调试不了,所以只能静态的去看一下源码逻辑。
注意点:注册中心是一个特殊的服务端
进入skel的dispatch方法,我们可以看到有许多的case,不同的case值对应不同的方法
0->bind
1->list
2->lookup
3->rebind
4->unbind
这边我们调用的是case2,可以回过去看一下参数
然后这里
会调用一个反序列化,我们在分析客户端的时候说到客户端会向注册中心传送一个序列化的对象(远程对象的名称)以此获取远程对象,那么注册中心这边的接受肯定就是一个反序列化喽。
从0-4的case值中如果有readObject方法的,都是可以被用来攻击的。




6.客户端请求服务端-服务端
还是接着上面的看去找发布远程对象时创建的target
这时服务端创建的stub是一个动态代理
依旧dispatch
但是这里的skel是空的
获取到输入流
获取到了method,也就是我们要调用到的远程方法

然后这里会调用到unmarshalValue,分析客户端的时候会将参数值序列化传入进去,所以服务端就会,反序列化来获取这个参数值

这里进行真正的远程调用
这边是返回值的序列化,然后传到客户端进行反序列化。
小总结:客户端序列化参数传给服务端,服务端反序列化获得参数值。执行完结果之后,服务端序列化返回值传给客户端,客户端在反序列化获得返回值。客户端比服务端这里多了一个jrmp协议攻击。
DGC的stub
DGC应该也很熟悉吧,这是前面提到的分布式垃圾回收。
所以我们先=现在要分析一下怎么创建出来
实际上开始是从这里,这里的dgcLog是一个静态变量,当调用一个静态变量是会完成一个类的初始化。初始化的时候实际上会调用它的静态代码块。
所以他就会走到这边。断在这里,我们重新调试一下。
在向下调试就会进入这里,和之前创建注册中心的stub时是一样的

从这边就可以看到判断完成后进行一个实例化。
DGC也会开启一个服务,注册中心是远程注册的服务,这里DGC是一个远程的回收的服务,他也开放了端口号,但是不像注册中心,他的端口号是随机的。
最后就给put进表里
这就是DGC的创建过程,他的调用过程也是进入到dispatch方法,和服务端的类似,这里我们就不看了,我们主要分析一下,DGCImpl_Stub和DGCImpl_Skel
先看DGCImpl_stub他里面有clean和dirty两个方法一个弱的清除一个强的清除吧。
我们会看到dirty方法里会调用到invoke方法
clean中也有,这里就会被jrmp攻击
这边也有一个反序列化的点,从服务端获取到一个东西,进行反序列化。
然后在看一下DGCImpl_skel
有两个case

也都存在反序列化的点
小总结:DGC和注册中心差不多,它的客户端和服务端也都会被攻击。
总结
- 服务端创建远程对象,发布远程对象,并留下记录,在这个过程中服务端会创建stub。
- 注册中心被创建(创建skel和stub),并监听1099端口。
- 服务端把第一步生成的Stub(远程对象),通过网络发给注册中心。注册中心通过自己的skel接受请求。(bind)
- 客户端请求注册中心,在此期间会获得注册中心的stub(通过传参获得和反序列化无关),建立连接,并通过传入name(和序列化有关)获取远程对象,也就拿到服务端创建的stub。
- 客户端利用这个远程对象和服务端建立通信,这时候客户端使用的是服务端创建的stub,服务端使用的是自己的skel进行连接。完成函数的调用。(仅个人理解)
