咕咕了好久,因为最近学的也比较杂,闲得蛋疼会去打Hackthebox,从明天(2.16)就准备把这个学一学了,再不学就太抽象了
RMI概念介绍
RMI全程Remote Method Invocation(远程方法对象),RMI有客户端和服务端,在JAVA
中客户端可以通过RMI调用服务端的方法,怎么想都很危险,流程图如下:
服务端注册了RMI后会在RMI Registry进行注册,之后客户端调用方法都是直接从注册中心取出即可
RMI分为三个主体部分:
- Client-客户端:客户端调用服务端的方法
- Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果
- Registry-注册中心:其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用,在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功
简单使用
需要准备客户端和服务器
服务端
需要准备3个java文件,方法接口,实现类,RMI服务端:
1 | import java.rmi.Remote; |
1 | import java.rmi.RemoteException; |
1 | import java.rmi.AlreadyBoundException; |
这边需要注意的就是接口继承Remote类,实现类继承Unicastxxx类
客户端
1 | import java.rmi.Remote; |
1 | import java.rmi.NotBoundException; |
先运行服务端,再运行客户端:
服务端输出了方法结果,这里留个小坑,客户端的Remote接口能不能去掉,去掉了也是不影响结果的
Step1| 创建远程服务对象
这一步分析对应IRemoteObj remoteObj=new IRemoteObjImp();
:
第一步当然是创建实现类对象,但是我们的实现类继承了UnicastRemoteObject对象
因此进入了下一步,调用了UnicastRemoteObject
构造方法:
在构造方法里定义了初始化port,之后导出了我们的远程对象:
假如我们的实现类不继承UnicastRemoteObject我们就需要手动调用exportObject((Remote) this, port);
了,因此得继承一下
接着就跟进exportobject方法中,在exportObject中又调用了另一个exportObject
,参数为new UnicastServerRef
,这个就是封装RMI的一个入口
我们跟进UnicastServerRef方法中,调用了父类的构造方法,参数为一个LifeRef
,记住这个对象,这是步骤一的核心对象,之后的大部分其他对象都是LifeRef的一个封装
进入LiveRef构造方法中,进入另外一个构造方法:
另外一个构造方法又调用了另一个构造方法(套娃呢?),第一个构造方法传入的ObjID只起了一个标识作用,并没啥其他用处,注意TCPEndpoint.getLocalEndpoint(port)
,这是真正包含了远程服务对象信息的一个类:
跟进TCPEndpoint.getLocalEndpoint(port)
继续跟进,可以发现一些有关网络请求的信息,host和port,这里port还未进行初始化,因此是默认值0:
回到LiveRef:this(objID, TCPEndpoint.getLocalEndpoint(port), true)
:
接着往下走,对LiveRef的属性进行了赋值,其中ep就是endpoint
执行完过后就退回了最初的开始UnicastServerRef:super(new LiveRef(port));
,调用父类的构造方法,传入我们上面创建好的LiveRef进行封装:
继续跟进,对ref属性进行赋值,赋值为上述LiveRef,在这里做了第一次封装
回到最开始的new UnicastServerRef
,继续跟进
可以发现sref实际上就是LiveRef的封装(编号不一样是因为不在一次调试阶段,随后继续导出:
跟进sref.exportObject
,进入了Util.createProxy
,这里的getClientRef就是获取一个LiveRef封装:
客户端通过stub获取远程对象,为什么服务端会出现客户端的stub呢?是因为服务端需要先将stub注册到注册中心,之后客户端直接从stub获取即可,可以参考概念介绍中的流程图来分析
跟进createProxy,进入了一层判断,前两个条件都为false,关键就在于stubClassExists(remoteClass)
是否为真,假如为真就不进去了
这里的stubClassExists实际上就是判断类名是否加了个_stub
后缀,有就返回true,当然这里肯定是没有的,因为是我们自定义的,在jdk的registry包中有自带_stub后缀的类,这里用不上所以不细讲:
在接下来就是标准的创建动态代理的过程:
这里的ClientRef还是LiveRef的一层封装:
创建完stub后就回到了stub = Util.createProxy(implClass, getClientRef(), forceStubUse);
,继续往下走,又创建了Target对象,这里的target对象实际上就是把上面的LifeRef和Stub什么的一并封装:
跟进构造方法,对属性stub和disp进行赋值,这两个属性也是LiveRef的封装:
之后进入了LiveRef的exportObject方法,我们跟进:
接着进入TcpEndpoint的exportobject方法中导出对象:
TcpEndponit是又一个transport属性的,继续调用了transport的exportobject方法:
跟进transport.exportObject,进入了Listen方法:
在里面创建了一个新的Socket:
跟进这个方法瞅瞅,发现在里面终于对port进行了一波初始化:
之后就执行完毕退出来了,然后创建了一个多线程的操作,逻辑在AcceptLoop中:
跟进里面的run方法:
逻辑在executeAcceptLoop()
,就不继续跟进了,然后多线程创建完毕就退出Listen继续往下走了:
跟进父类的exportobject,在里面保存了我们的Target到一个Map中:
跟进putTarget
方法中,进入ObjectTable类,最后将target放入objtable属性中:
到这里第一步流程基本就结束了
Step2| 创建注册中心+绑定
写到这里的时候去打VNCTF了,成绩不错有点感动,继续学习
到这里就是创建注册中心的过程,我们继续分析
第一步直接进入createRegistry
方法
经过一系列的初始化步骤后进入RegistryImpl的构造方法,由于我们port不是默认的1099端口,因此直接过了if到else:
首先创建一个LiveRef再塞进UnicastServerRef里,这个过程和第一步骤是很相似的,进入setup方法中:
调用了UnicastServerRef的exportObject方法,这一步其实在第一步也发生过,跟进:
同样也是先初始化一个stub,跟进createProxy看看:
在step1中也进入了这里,但是没过if判断,但是这次remoteObject为RegistryImpl类,而我们也说了存在RegistryImpl_stub
这个类,因此这次我们进入了If判断:
这里ClientRef也是liveref的封装,继续跟进:
这里就是通过forName反射获取ReigistryImpl类,然后执行完毕后跳出方法回到CreateProxy继续往下执行:
在这里设置了Skeleton(骨架),从一开始的流程图我们知道,RMI是一个对称分布的结构,客户端有stub,服务端就对应有Skeleton,客户端通过stub请求远程方法,服务端就通过Skeleton去调用方法,然后通过Skeleton获取结果,最后传给客户端Stub,客户端就从Stub获取结果,因此继续跟进setSkeleton方法:
在这里进入createSkeleton
:
反射获取了Skeleton,和上面的CreateStub其实很像的,之后退回setSkeleton
,继续往下执行,创建了Target:
这一步大部分就和Step1是完全一致的了,进入构造方法:
也是进行一系列的赋值,唯一的不同之处就是disp之中这次skel有了值:
接着进入了liveref的exportobject方法:
步骤和step1一样,先进入ep.exportobject,再进入transport.exportobject,最后到listen方法,listen方法里面就不分析了也和step1是一样的:
最后基本就结束了,step2和step1的差别就体现在skeleton的创建上,然后就分析分析bind绑定嘛,其实绑定很简单没什么步骤:
获取name后就放进bindings属性里,这里的bindings是一个hashtable,然后就没了。。
Step3| 客户端请求注册中心-客户端
NEXT
NEXT,还是createproxy:
然后第一步就结束了QWQ,重点在调用lookup方法,调试进入的第一步就到了一个奇怪的地方:
可以看一下调用栈:
由于Regsitry_Stub是class文件无法调试所以跳过了,因此我们直接看看class反编译文件的内容:
客户端(注册中心Attack)——被攻击点一
那我们就嗯读,先获取了一个输出流,获得服务端序列化结果,之后在后面调用readObject反序列化,这里一看就有漏洞啊:
客户端(注册中心Attack)——被攻击点二
假如服务端返回的结果是恶意的,那么客户端就Rce了,这是第一个攻击客户端的地方,除了这地方还有别的地方吗?肯定是有的,看看上面是不是调用了一个super.ref.invoke(var2);
,这里的ref是UnicastRef,我们跟进去:
到了UnicastRef里,在里面调用了call.executeCall();
,这里才是处理逻辑的主要部分,继续跟进去:
在末尾的异常处理中调用了in.readObject();
,这里本来是想将异常结果反序列化读出来的,但是设计者没考虑安全问题,因此这里也可以被攻击
客户端(注册中心Attack)——被攻击点三-六
那么除了这个地方还有没有其他地方呢,那么就再往上看看,我们这个Demo中调用的是lookup,还有其他方法可以
这里有很多其他处理远程对象的方法lookup list rebind unbind bind
,我们分析一下这些方法里有什么共性:
list也调用了invoke,因此可以被攻击!
rebind同理
unbind同理
bind同理
Step4| 客户端请求服务端-客户端
客户端(服务端Attack)被攻击点
先跟着调试看看,由于获取的remoteobject是动态代理类,因此会进入代理的invoke方法:
然后在invoke方法中有invokeRemoteMethod(proxy, method, args)
,看名字就知道是方法调用:
继续跟进该方法,在里面调用了个ref.invoke
:
继续跟进该方法,可以看到调用了个marshalValue
,跟进该方法,这是判断参数类型的:
由于类型为String,不是type.isPrimitive()
原始类型,而是引用类型,因此进入else,调用writeObject
然后出来marshalValue
后又调用了step3里的call.executeCall()
这里的攻击手法和上面一样
然后判断返回值是否为空,这里我们为空,假如不为空,会对结果进行unmarshalValue
:
跟进unmarshalValue
:
也是判断结果类型,如果不是原始类型就反序列化
这里就存在反序列化危险了,这里就是服务端攻击客户端了
Step5| 客户端请求服务端-注册中心
注册中心(服务端)被攻击点(By Client)
下断点在哪有点讲究,回顾一下当时服务端创建注册中心的时候的流程吧,首先就是createRegistry
:
在进入RegistryImpl
:
然后进入setup:
最后一路调用exportobject到listen监听:
这里listen的逻辑我们也分析过了,是开过了一个线程,我们再看看:
跟进:
跟进:
这里又开了一个线程池,跟进到ConnectionHandler的run方法:
到handlemessage这,继续跟:
继续跟servicecall:
这里就把target取出来了,也就是在这下断点即可,下完断点客户端调用方法:
可以看到跟到了这一步,也就是分析正确,那么我们继续调试:
获取dispatcher分发器,然后在下面调用了disp.dispatch
:
小tips,点红框图表直接到下一个断点,可以动态加断点,跟进:
skel不为空因此到了olddispatch:
最后也是调用了skel的dispatch方法,下一步就进入了registryimpl_skel.class
的dispatch方法中,这也是漏洞所在
case2直接readObject了,这里是反序列化客户端序列化参数,然后这里case对应情况如下
- bind : 0
- list : 1
- lookup : 2
- rebind : 3
- unbind : 4
到这里也就是说假如客户端传入的参数为恶意Object,那么就GG咯
Step6| 客户端请求服务端-服务端(服务端被攻击点)
回到Step5的olddispatch那一段, 继续往下走可以发现也调用了UnmarhalValue方法:
进到了unmarshalvalue方法后就是判断参数类型是否为基本类型,demo传的是string,假如传入一个Object同样GG
Step7| 客户端请求服务端-dgc
DGC就是RMI里垃圾回收机制,具体介绍如下:
分布式垃圾回收,又称 DGC,RMI 使用 DGC 来做垃圾回收,因为跨虚拟机的情况下要做垃圾回收没办法使用原有的机制。我们使用的远程对象只有在客户端和服务端都不受引用时才会结束生命周期。
而既然 RMI 依赖于 DGC 做垃圾回收,那么在 RMI 服务中必然会有 DGC 层,在 yso 中攻击 DGC 层对应的是 JRMPClient,在攻击 RMI Registry 小节中提到了 skel 和 stub 对应的 Registry 的服务端和客户端,同样的,DGC 层中也会有 skel 和 stub 对应的代码,也就是 DGCImpl_Skel 和 DGCImpl_Stub,我们可以直接从此处分析,避免冗长的 debug。
这东西呢,比较不好说,DGC就是用来回收远程对象的,他在RMI反序列化有什么作用呢?
它在bypassJEP290有作用,具体的JEP290是什么,我现在还不是很想去写,大概就到这,接下里就看看DGC的创建和风险点:
在之前Listen方法后的一系列export之后会进入puttarget方法:
在这里调用了DGCImpl的静态变量,也就完成了DGCImpl的初始化,因此我们跟进DGCImpl去看看:
这里面有一些dirty和clean方法,但是我们退回puttarget,可以看到target中创建的stub其实是DGCImpl_stub
类,因此调用的是它里面的dirty和clean:
因此进入DGCImpl_stub
:
dirty方法中有readObject反序列化,因此这里也是风险点,到这就算结束了,小结一下
About this Post
This post is written by Boogipop, licensed under CC BY-NC 4.0.