0%

JEP-290 相关学习

官网介绍: https://openjdk.java.net/jeps/290

JEP = JDK Enhancement Proposals
就是等于改进提案. python 也有类似的, 就是大家喜闻乐见的 PEP (Python Enhancement Proposals).

在这个 290 里面, 干的事情就是新增一个自带的反序列化过滤器.

1
Security guidelines consistently require that input from external sources be validated before use. The filter mechanism will allow object-serialization clients to more easily validate their inputs, and exported RMI objects to validate invocation arguments.

jdk 源码里面可以看到

src/java.rmi/share/classes/sun/rmi/registry/RegistryImpl.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
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
private static final ObjectInputFilter registryFilter =
AccessController.doPrivileged((PrivilegedAction<ObjectInputFilter>)RegistryImpl::initRegistryFilter);

/**
* Initialize the registryFilter from the security properties or system property; if any
* @return an ObjectInputFilter, or null
*/
@SuppressWarnings("deprecation")
private static ObjectInputFilter initRegistryFilter() {
ObjectInputFilter filter = null;
String props = System.getProperty(REGISTRY_FILTER_PROPNAME);
if (props == null) {
props = Security.getProperty(REGISTRY_FILTER_PROPNAME);
}
if (props != null) {
filter = SharedSecrets.getJavaObjectInputFilterAccess().createFilter2(props);
Log regLog = Log.getLog("sun.rmi.registry", "registry", -1);
if (regLog.isLoggable(Log.BRIEF)) {
regLog.log(Log.BRIEF, "registryFilter = " + filter);
}
}
return filter;
}

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

public RegistryImpl(int port)
throws RemoteException
{
if (port == Registry.REGISTRY_PORT && System.getSecurityManager() != null) {
// grant permission for default port only.
try {
AccessController.doPrivileged(new PrivilegedExceptionAction<Void>() {
public Void run() throws RemoteException {
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref, RegistryImpl::registryFilter));
return null;
}
}, null, new SocketPermission("localhost:"+port, "listen,accept"));
} catch (PrivilegedActionException pae) {
throw (RemoteException)pae.getException();
}
} else {
LiveRef lref = new LiveRef(id, port);
setup(new UnicastServerRef(lref, RegistryImpl::registryFilter)); // 这一行, 将过滤器传给了 UnicastServerRef
}
}

注意有两个 registryFilter, 一个是静态函数, 一个是静态对象.
静态对象在类定义时被赋值, registryFilter 静态函数在函数体里面调用了 registryFilter 对象来检查.

其中 registryFilter 对象来源应该就是从 JEP 290 加的属性 jdk.serialFilter 里面读取过滤规则. 因为 ObjectInputStream 里面只能设置一个 ObjectInputFilter. 如果要自定义, 就只能先读取原来的, 然后在自己新定义的 filter 中调用原来的. 否则原来的规则就没了, 所以底下的就是 RMI 真正新增的过滤.

可以看到过滤规则主要就是:

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

检查了反序列化对象是否是 String 或者是 Number, Remote, Proxy, UnicastRef, RMIClientSocketFactory, RMIServerSocketFactory, ActivationID, UID 的子类, 否则就拒绝反序列化, 可以看到是白名单, 还是挺靠谱的. 也确实效果挺好, 能 bypass 的也只有 JRMPClient.

不过这只对 bind 等操作生效. 因为在真正调用远程对象时, 处理函数调用的是 Service Provider 而不是 Registry. Registry 是 RegistryImpl, 而 Service Provider 得到的是 RegistryImpl_Stub. 其在默认情况下是没有 filter 的,

1
2
3
public RegistryImpl_Stub(java.rmi.server.RemoteRef ref) {
super(ref);
}

这也是 JEP-290 能被绕过的原因. 其只对 Registry 做了防护, 而 Service Provider 也会进行反序列化, 却没有做反序列化的防护.
当然, Service Provider 确实无法进行白名单层面的防护, 因为函数的参数类型是各种各样的, 估计也是出于对兼容性和黑名单被绕过的考虑, 也没进行黑名单防护.

参考链接

[1] https://mogwailabs.de/blog/2019/03/attacking-java-rmi-services-after-jep-290/
[2] http://www.codersec.net/2018/09/%E4%B8%80%E6%AC%A1%E6%94%BB%E5%87%BB%E5%86%85%E7%BD%91rmi%E6%9C%8D%E5%8A%A1%E7%9A%84%E6%B7%B1%E6%80%9D/

  • 本文作者: rmb122
  • 本文链接: https://rmb122.com/2020/03/22/JEP-290-相关学习/
  • 版权声明: 本博客所有文章仅仅记录学习过程, 并不保证文章完全正确, 如果你有任何疑问或者找出问题, 欢迎在评论区指正.