介绍

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

(1)什么是JEP?

JEPJDK Enhancement Proposal (JDK增强提议),目前索引编号已经达到了 JEP415

(2)什么是JEP290?

JEP290 允许过滤输入流中对象的序列化数据,以提高程序的安全性和稳健性。

(2)JEP290的影响范围

最主要的分水岭还是JDK8u121:

  • Java™ SE Development Kit 8, Update 121 (JDK 8u121)
  • Java™ SE Development Kit 7, Update 131 (JDK 7u131)
  • Java™ SE Development Kit 6, Update 141 (JDK 6u141)

目的

直接引用官方的话:

  • Provide a flexible mechanism to narrow the classes that can be deserialized from any class available to an application down to a context-appropriate set of classes.[提供一种灵活的机制,将可以反序列化的类从应用程序可用的任何类缩小到适合上下文的类集。]
  • Provide metrics to the filter for graph size and complexity during deserialization to validate normal graph behaviors.[在反序列化期间向过滤器提供图大小和复杂性的指标,以验证正常的图行为。]
  • Provide a mechanism for RMI-exported objects to validate the classes expected in invocations.[为 RMI 导出的对象提供一种机制,以验证调用中预期的类。]
  • The filter mechanism must not require subclassing or modification to existing subclasses of ObjectInputStream.[过滤器机制不得要求对 ObjectInputStream 的现有子类进行子类化或修改。]
  • Define a global filter that can be configured by properties or a configuration file.[定义可以通过属性或配置文件配置的全局过滤器。]

可以看到,从 JEP290 开始,对 RMI 反序列化开始进行了限制。 JEP290 影响最大的还是 RMI 反序列化。

注意点

  • JEP290需要手动设置,只有设置了之后才会有过滤,没有设置的话就还是可以正常的反序列化漏洞利用;
  • JEP290默认只为 RMI 注册表(RMI Register层)、 RMI分布式垃圾回收器(DGC层)以及 JMX 提供了相应的内置过滤器;

所以从这里也可以看出, JEP290 影响最大的是 RMI 反序列化。

JEP290具体内容

JEP290主要是支持三种过滤器配置:

  • 自定义过滤器;
  • 全局过滤器;
  • 内置过滤器(RMI Register、RMI DGC等内置过滤器);

(1)自定义过滤器[ObjectInputFilter Interface]

自定义过滤器需要实现 ObjectInputFilter 接口,并实现其 checkInput(FilterInfo filterInfo) 方法,一个例子如下:

 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
/**
 * @author : p1n93r
 * @date : 2021/9/23 15:08
 * 自定义一个反序列化过滤器
 */
public class CustomObjectInputFilter implements ObjectInputFilter {
    private static final Class<?> CLAZZ = User.class;
    /**
     * 限制反序列化类数组的数组大小
     */
    private static final long ARRAY_LENGTH = -1L;

    /**
     * 限制反序列化时,引用的数量
     */
    private static final long TOTAL_OBJECT_REFS = 1L;

    /**
     * 限制反序列化的深度
     */
    private static final long DEPTH = 1l;

    /**
     * 限制反序列化流的大小
     */
    private static final long STREAM_BYTES = 95L;

    @Override
    public Status checkInput(FilterInfo filterInfo) {
        if (filterInfo.arrayLength() != ARRAY_LENGTH
            || filterInfo.references() != TOTAL_OBJECT_REFS
            || filterInfo.depth() != DEPTH
            || filterInfo.streamBytes() != STREAM_BYTES
        ) {
            return Status.REJECTED;
        }

        if (filterInfo.serialClass() == null) {
            return Status.UNDECIDED;
        }

        if (filterInfo.serialClass() != null && filterInfo.serialClass() == CLAZZ) {
            return Status.ALLOWED;
        }
        return Status.REJECTED;
    }
}

然后就可以使用如下代码为程序内所有的 ObjectInputStream 设置反序列化过滤器:

1
ObjectInputFilter.Config.setSerialFilter(new CustomObjectInputFilter());

或者使用如下代码为某个特定的 ObjectInputStream 设置反序列化过滤器:

1
2
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("xxxx/xxx"));
ObjectInputFilter.Config.setObjectInputFilter(objectInputStream,new CustomObjectInputFilter());

(2)全局过滤器[Process-wide Filter]

全局过滤器可以通过系统属性或者配置文件来配置,如果配置了系统属性,那么会覆盖配置文件的配置:

  • 系统属性: jdk.serialFilter
  • 配置文件: $JAVA_HOME/conf/security/java.properties (JDK9及以上)或者 $JAVA_HOME/lib/security/java.security (JDK6/7/8);

配置系统属性,其实就是在Java程序启动前,加上JVM选项:

1
-Djdk.serialFilter=<白名单类1>;<白名单类2>;!<黑名单类>

选项的值,是一系列规则(模式),以封号分隔:

  • 如果模式以 ! 开头,如果模式的其余部分匹配,则拒绝该类,否则接受该类
  • 如果模式包含 / ,则 / 之前的非空前缀是模块名称。如果模块名称与类的模块名称匹配,则剩余模式与类名称匹配。如果没有 / ,则不比较模块名称。
  • 如果模式以 .** 结尾,则匹配包和所有子包中的任何类
  • 如果模式以 .* 结尾,则匹配包中的任何类
  • 如果模式以 * 结尾,则匹配任何以该模式为前缀的类。
  • 如果模式等于类名,则匹配。
  • 否则,状态未定。

一个例子如下:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
# 匹配特定的类并拒绝非列表中的类
-Djdk.serialFilter=org.example.Test;!*

# 匹配包和所有子包中的类并拒绝非列表中的类
-Djdk.serialFilter=org.example.**;!*

# 匹配包中的所有类并拒绝非列表中的类
-Djdk.serialFilter=org.example.*;!*

# 匹配任何以设置样式为前缀的类
-Djdk.serialFilter=*;

(3)内置过滤器

对于JEP290而言,存在默认开启的内置过滤器,这个内置过滤器适用于RMI 注册表(RMI Register层)、 RMI分布式垃圾回收器(DGC层)以及 JMX 。

这个过滤器影响比较大,对于 RMI Register 的限制,代码主要体现在 RegistryImpl#registryFilter ,其中过滤逻辑是使用白名单,反序列化白名单类如下:

  • java.rmi.Remote
  • java.lang.Number
  • java.lang.reflect.Proxy
  • java.rmi.server.UnicastRef
  • java.rmi.activation.ActivationId
  • java.rmi.server.UID
  • java.rmi.server.RMIClientSocketFactory
  • java.rmi.server.RMIServerSocketFactory

并且还存在两个限制:

1
2
maxarray=1000000
maxdepth=20

对于 RMI DGC 的限制,代码主要体现在 sun.rmi.transport.DGCImpl_Stub#dirty() 方法中,其中的主要过滤逻辑也是白名单,反序列化白名单如下:

  • java.rmi.server.ObjID
  • java.rmi.server.UID
  • java.rmi.dgc.VMID
  • java.rmi.dgc.Lease

同样存在深度和数组大小限制:

1
2
maxarray=1000000
maxdepth=20

一些思考

一般人们常说的 Bypass JEP290 都是基于一个条件:没有配置全局过滤器;

JEP290 中的默认过滤器,影响最大的还是RMI,除了上面提到的默认内置过滤器外,还加了一些限制:

1
2
3
java.rmi.server.useCodebaseOnly default true
com.sun.jndi.rmi.object.trustURLCodebase default false
com.sun.jndi.ldap.object.trustURLCodebase default false

这些限制,禁止从远程加载Class。但是JEP290并不是一出来就是完善的,例如我们熟知的 JRMP Gadget 就是为了绕过JEP290下的RMI反序列化限制的(JDK8u121 <= version < JDK8u231)。

此外还有一个Gadget可以绕过:UnicastRemoteObject Gadget(JDK8u231 <= version < JDK8u241),我没有细细研究这个Gadget,因为一般情况下就直接使用LDAP代替RMI进行绕过了,很少会用到 UnicastRemoteObject 这个 Gadget。