March 2, 2023

浅学RMI反序列化

咕咕了好久,因为最近学的也比较杂,闲得蛋疼会去打Hackthebox,从明天(2.16)就准备把这个学一学了,再不学就太抽象了

RMI概念介绍

RMI全程Remote Method Invocation(远程方法对象),RMI有客户端和服务端,在JAVA
中客户端可以通过RMI调用服务端的方法,怎么想都很危险,流程图如下:
image.png
服务端注册了RMI后会在RMI Registry进行注册,之后客户端调用方法都是直接从注册中心取出即可
RMI分为三个主体部分:

简单使用

需要准备客户端和服务器

服务端

需要准备3个java文件,方法接口,实现类,RMI服务端:

1
2
3
4
5
6
7
8
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IRemoteObj extends Remote {
//sayhello就是客户端需要调用的方法,需要throw异常
public String sayhello(String key) throws RemoteException;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class IRemoteObjImp extends UnicastRemoteObject implements IRemoteObj{
protected IRemoteObjImp() throws RemoteException {
//UnicastRemoteObject.exportObject(this,0);如果不继承手动导出
}

@Override
public String sayhello(String key) throws RemoteException {
String upkey=key.toUpperCase();
System.out.println(upkey);
return upkey;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
IRemoteObj remoteObj=new IRemoteObjImp();
//注册中心
Registry r= LocateRegistry.createRegistry(7788);
//绑定对象到注册中心
r.bind("remoteobj",remoteObj);
}
}

这边需要注意的就是接口继承Remote类,实现类继承Unicastxxx类

客户端

1
2
3
4
5
6
7
8
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface IRemoteObj extends Remote {
//sayhello就是客户端需要调用的方法,需要throw异常
public String sayhello(String key) throws RemoteException;
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.rmi.NotBoundException;
import java.rmi.Rem



oteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry r= LocateRegistry.getRegistry("127.0.0.1",7788);
IRemoteObj remoteObj= (IRemoteObj) r.lookup("remoteobj");
remoteObj.sayhello("helloworld");
}
}

先运行服务端,再运行客户端:
image.png
服务端输出了方法结果,这里留个小坑,客户端的Remote接口能不能去掉,去掉了也是不影响结果的

Step1| 创建远程服务对象

这一步分析对应IRemoteObj remoteObj=new IRemoteObjImp();
第一步当然是创建实现类对象,但是我们的实现类继承了UnicastRemoteObject对象
image.png
因此进入了下一步,调用了UnicastRemoteObject构造方法:
image.png
在构造方法里定义了初始化port,之后导出了我们的远程对象:
image.png
假如我们的实现类不继承UnicastRemoteObject我们就需要手动调用exportObject((Remote) this, port);了,因此得继承一下
接着就跟进exportobject方法中,在exportObject中又调用了另一个exportObject,参数为new UnicastServerRef,这个就是封装RMI的一个入口
image.png
我们跟进UnicastServerRef方法中,调用了父类的构造方法,参数为一个LifeRef,记住这个对象,这是步骤一的核心对象,之后的大部分其他对象都是LifeRef的一个封装
image.png
进入LiveRef构造方法中,进入另外一个构造方法:
image.png
另外一个构造方法又调用了另一个构造方法(套娃呢?),第一个构造方法传入的ObjID只起了一个标识作用,并没啥其他用处,注意TCPEndpoint.getLocalEndpoint(port),这是真正包含了远程服务对象信息的一个类:
image.png
跟进TCPEndpoint.getLocalEndpoint(port)image.png
继续跟进,可以发现一些有关网络请求的信息,host和port,这里port还未进行初始化,因此是默认值0:
image.png
回到LiveRef:this(objID, TCPEndpoint.getLocalEndpoint(port), true)
image.png
接着往下走,对LiveRef的属性进行了赋值,其中ep就是endpoint
image.png
执行完过后就退回了最初的开始UnicastServerRef:super(new LiveRef(port));,调用父类的构造方法,传入我们上面创建好的LiveRef进行封装:
image.png
继续跟进,对ref属性进行赋值,赋值为上述LiveRef,在这里做了第一次封装
image.png
回到最开始的new UnicastServerRef,继续跟进
image.png
可以发现sref实际上就是LiveRef的封装(编号不一样是因为不在一次调试阶段,随后继续导出:
image.png
跟进sref.exportObject,进入了Util.createProxy,这里的getClientRef就是获取一个LiveRef封装:

客户端通过stub获取远程对象,为什么服务端会出现客户端的stub呢?是因为服务端需要先将stub注册到注册中心,之后客户端直接从stub获取即可,可以参考概念介绍中的流程图来分析

image.png
image.png
跟进createProxy,进入了一层判断,前两个条件都为false,关键就在于stubClassExists(remoteClass)是否为真,假如为真就不进去了
image.png
这里的stubClassExists实际上就是判断类名是否加了个_stub后缀,有就返回true,当然这里肯定是没有的,因为是我们自定义的,在jdk的registry包中有自带_stub后缀的类,这里用不上所以不细讲:
image.png
在接下来就是标准的创建动态代理的过程:
image.png
这里的ClientRef还是LiveRef的一层封装:
image.png
创建完stub后就回到了stub = Util.createProxy(implClass, getClientRef(), forceStubUse); ,继续往下走,又创建了Target对象,这里的target对象实际上就是把上面的LifeRef和Stub什么的一并封装:
image.png
跟进构造方法,对属性stub和disp进行赋值,这两个属性也是LiveRef的封装:
image.png
之后进入了LiveRef的exportObject方法,我们跟进:
image.png
接着进入TcpEndpoint的exportobject方法中导出对象:
image.png
TcpEndponit是又一个transport属性的,继续调用了transport的exportobject方法:
image.png
跟进transport.exportObject,进入了Listen方法:
image.png
在里面创建了一个新的Socket:
image.png
跟进这个方法瞅瞅,发现在里面终于对port进行了一波初始化:
image.png
之后就执行完毕退出来了,然后创建了一个多线程的操作,逻辑在AcceptLoop中:
image.png
跟进里面的run方法:
image.png
逻辑在executeAcceptLoop(),就不继续跟进了,然后多线程创建完毕就退出Listen继续往下走了:
image.png
跟进父类的exportobject,在里面保存了我们的Target到一个Map中:
image.png
跟进putTarget方法中,进入ObjectTable类,最后将target放入objtable属性中:
image.png
image.png
到这里第一步流程基本就结束了

Step2| 创建注册中心+绑定

写到这里的时候去打VNCTF了,成绩不错有点感动,继续学习
到这里就是创建注册中心的过程,我们继续分析
第一步直接进入createRegistry方法
image.png
经过一系列的初始化步骤后进入RegistryImpl的构造方法,由于我们port不是默认的1099端口,因此直接过了if到else:
image.png
首先创建一个LiveRef再塞进UnicastServerRef里,这个过程和第一步骤是很相似的,进入setup方法中:
image.png
调用了UnicastServerRef的exportObject方法,这一步其实在第一步也发生过,跟进:
image.png
同样也是先初始化一个stub,跟进createProxy看看:
image.png
在step1中也进入了这里,但是没过if判断,但是这次remoteObject为RegistryImpl类,而我们也说了存在RegistryImpl_stub这个类,因此这次我们进入了If判断:
image.png
这里ClientRef也是liveref的封装,继续跟进:
image.png
这里就是通过forName反射获取ReigistryImpl类,然后执行完毕后跳出方法回到CreateProxy继续往下执行:
image.png
在这里设置了Skeleton(骨架),从一开始的流程图我们知道,RMI是一个对称分布的结构,客户端有stub,服务端就对应有Skeleton,客户端通过stub请求远程方法,服务端就通过Skeleton去调用方法,然后通过Skeleton获取结果,最后传给客户端Stub,客户端就从Stub获取结果,因此继续跟进setSkeleton方法:
image.png 在这里进入createSkeleton
image.png
反射获取了Skeleton,和上面的CreateStub其实很像的,之后退回setSkeleton,继续往下执行,创建了Target:
image.png
这一步大部分就和Step1是完全一致的了,进入构造方法:
image.png
也是进行一系列的赋值,唯一的不同之处就是disp之中这次skel有了值:
image.png
接着进入了liveref的exportobject方法:
image.png
步骤和step1一样,先进入ep.exportobject,再进入transport.exportobject,最后到listen方法,listen方法里面就不分析了也和step1是一样的:
image.pngimage.png
image.png
最后基本就结束了,step2和step1的差别就体现在skeleton的创建上,然后就分析分析bind绑定嘛,其实绑定很简单没什么步骤:
image.png
获取name后就放进bindings属性里,这里的bindings是一个hashtable,然后就没了。。

Step3| 客户端请求注册中心-客户端

image.png
NEXT
image.png
NEXT,还是createproxy:
image.png
然后第一步就结束了QWQ,重点在调用lookup方法,调试进入的第一步就到了一个奇怪的地方:
image.png
可以看一下调用栈:
image.png
由于Regsitry_Stub是class文件无法调试所以跳过了,因此我们直接看看class反编译文件的内容:
image.png

客户端(注册中心Attack)——被攻击点一

那我们就嗯读,先获取了一个输出流,获得服务端序列化结果,之后在后面调用readObject反序列化,这里一看就有漏洞啊:
image.png

客户端(注册中心Attack)——被攻击点二

假如服务端返回的结果是恶意的,那么客户端就Rce了,这是第一个攻击客户端的地方,除了这地方还有别的地方吗?肯定是有的,看看上面是不是调用了一个super.ref.invoke(var2);,这里的ref是UnicastRef,我们跟进去:
image.png
到了UnicastRef里,在里面调用了call.executeCall();,这里才是处理逻辑的主要部分,继续跟进去:
image.png
在末尾的异常处理中调用了in.readObject();,这里本来是想将异常结果反序列化读出来的,但是设计者没考虑安全问题,因此这里也可以被攻击

客户端(注册中心Attack)——被攻击点三-六

那么除了这个地方还有没有其他地方呢,那么就再往上看看,我们这个Demo中调用的是lookup,还有其他方法可以
image.png
这里有很多其他处理远程对象的方法lookup list rebind unbind bind,我们分析一下这些方法里有什么共性:
image.png
list也调用了invoke,因此可以被攻击!
image.png
rebind同理
image.png
unbind同理
image.png
bind同理

Step4| 客户端请求服务端-客户端

客户端(服务端Attack)被攻击点

先跟着调试看看,由于获取的remoteobject是动态代理类,因此会进入代理的invoke方法:
image.png
image.png
然后在invoke方法中有invokeRemoteMethod(proxy, method, args),看名字就知道是方法调用:
image.png
继续跟进该方法,在里面调用了个ref.invoke
image.png
继续跟进该方法,可以看到调用了个marshalValue,跟进该方法,这是判断参数类型的:
image.png
由于类型为String,不是type.isPrimitive()原始类型,而是引用类型,因此进入else,调用writeObject
image.png
然后出来marshalValue后又调用了step3里的call.executeCall()这里的攻击手法和上面一样image.png
然后判断返回值是否为空,这里我们为空,假如不为空,会对结果进行unmarshalValue
image.png
跟进unmarshalValue
image.png
也是判断结果类型,如果不是原始类型就反序列化
image.png
这里就存在反序列化危险了,这里就是服务端攻击客户端了

Step5| 客户端请求服务端-注册中心

注册中心(服务端)被攻击点(By Client)

下断点在哪有点讲究,回顾一下当时服务端创建注册中心的时候的流程吧,首先就是createRegistry
image.png
在进入RegistryImpl
image.png
然后进入setup:
image.png
最后一路调用exportobject到listen监听:
image.png
这里listen的逻辑我们也分析过了,是开过了一个线程,我们再看看:
image.png
跟进:
image.png
跟进:
image.png
这里又开了一个线程池,跟进到ConnectionHandler的run方法:
image.png
到handlemessage这,继续跟:
image.png
继续跟servicecall:
image.png
这里就把target取出来了,也就是在这下断点即可,下完断点客户端调用方法:
image.png
可以看到跟到了这一步,也就是分析正确,那么我们继续调试:
image.png
获取dispatcher分发器,然后在下面调用了disp.dispatch
image.png
小tips,点红框图表直接到下一个断点,可以动态加断点,跟进:
image.png
skel不为空因此到了olddispatch:
image.png
最后也是调用了skel的dispatch方法,下一步就进入了registryimpl_skel.class的dispatch方法中,这也是漏洞所在
image.png
case2直接readObject了,这里是反序列化客户端序列化参数,然后这里case对应情况如下

到这里也就是说假如客户端传入的参数为恶意Object,那么就GG咯

Step6| 客户端请求服务端-服务端(服务端被攻击点)

回到Step5的olddispatch那一段, 继续往下走可以发现也调用了UnmarhalValue方法:
image.png
进到了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方法:
image.png
在这里调用了DGCImpl的静态变量,也就完成了DGCImpl的初始化,因此我们跟进DGCImpl去看看:
image.png
这里面有一些dirty和clean方法,但是我们退回puttarget,可以看到target中创建的stub其实是DGCImpl_stub类,因此调用的是它里面的dirty和clean:
image.png
因此进入DGCImpl_stub
image.png
dirty方法中有readObject反序列化,因此这里也是风险点,到这就算结束了,小结一下

About this Post

This post is written by Boogipop, licensed under CC BY-NC 4.0.

#Java#CTF#RMI