JEP290 Bypass
什么是JEP?
JDK Enhancement Proposal
简称JEP
,是 JDK 增强提议的一个项目,目前索引编号已经达到了JEP415,本文重点来谈谈什么是JEP290
,JEP290
做了哪些事,JEP290
绕过的方法总结等。
什么是JEP290?
JEP290
的描述是Filter Incoming Serialization Data
,即过滤传入的序列化数据
F Clo 9 | core/io:serialization | 290 | Filter Incoming Serialization Data |
---|---|---|---|
JEP290 是 Java 为了防御反序列化攻击而设置的一种过滤器,其在 JEP 项目中编号为290,因而通常被简称为
JEP290
JEP290的适用范围
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)
JEP290 的作用
- 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. [定义一个可配置的过滤机制,比如可以通过配置 properties文件的形式来定义过滤器]
JEP290 具体内容
1、限制的情况:
- 反序列化类数组时的数组元素数 ( arrayLength )
- 每个嵌套对象的深度( depth )
- 当前数量对象引用 ( references ) 的数量
- 当前消耗的字节数 ( streamBytes )
2、支持 3 种配置过滤器的方式
- 自定义过滤器
- 进程范围过滤器(也称为全局过滤器)
- 用于 RMI 注册表和分布式垃圾收集 (DGC)使用的内置过滤器
3、自定义过滤器
当反序列化要求与整个应用程序中的任何其他反序列化过程不同时,就会出现自定义过滤器的配置场景;可以通过实现ObjectInputFilter
接口并覆盖checkInput(FilterInfo filterInfo)
方法来创建自定义过滤器:
如以下示例:
static class VehicleFilter implements ObjectInputFilter {
final Class<?> clazz = Vehicle.class;
final long arrayLength = -1L;
final long totalObjectRefs = 1L;
final long depth = 1l;
final long streamBytes = 95L;
public Status checkInput(FilterInfo filterInfo) {
if (filterInfo.arrayLength() < this.arrayLength || filterInfo.arrayLength() > this.arrayLength
|| filterInfo.references() < this.totalObjectRefs || filterInfo.references() > this.totalObjectRefs
|| filterInfo.depth() < this.depth || filterInfo.depth() > this.depth || filterInfo.streamBytes() < this.streamBytes
|| filterInfo.streamBytes() > this.streamBytes) {
return Status.REJECTED;
}
if (filterInfo.serialClass() == null) {
return Status.UNDECIDED;
}
if (filterInfo.serialClass() != null && filterInfo.serialClass() == this.clazz) {
return Status.ALLOWED;
} else {
return Status.REJECTED;
}
}
}
4、进程范围(全局)过滤器
可以通过将jdk.serialFilter
设置为系统属性或安全属性来配置进程范围的过滤器(其实就是在启动Java应用时添加命令行参数,如:-Djdk.serialFilter=<白名单类1>;<白名单类2>;!<黑名单类>
)。如果定义了系统属性,则用于配置过滤器;否则过滤器会检查安全属性(JDK 8、7、6: $JAVA_HOME/lib/security/java.security
;JDK 9 及更高版本: $JAVA_HOME/conf/security/java.security
)以配置过滤器
此外,也可以在启动Java应用时设置-Djava.security.properties=<黑白名单配置文件名>
具体来说,通过检查类名或传入字节流属性的限制,jdk.serialFilter
的值被过滤器被配置为一系列模式,每个模式要么与流中类的名称匹配,要么与限制匹配。模式由分号分隔,空格也被认为是模式的一部分。无论模式序列的配置顺序如何,都会在类之前检查限制。以下是可在配置期间使用的限制属性:
maxdepth=value
— 图的最大深度maxrefs=value
— 内部参考的最大数量maxbytes=value
— 输入流中的最大字节数maxarray=value
— 允许的最大数组大小
其他模式与Class.getName()
返回的类或包名称匹配*。Class/Package
模式也接受星号 (*
)、双星号 (**
)、句点 (.
) 和正斜杠 (/
) 符号。以下是可能发生的几种模式场景:
//匹配特定的类并拒绝非列表中的类
"jdk.serialFilter=org.example.Vehicle;!*"
//匹配包和所有子包中的类并拒绝非列表中的类
- "jdk.serialFilter=org.example.**;!*"
// 匹配包中的所有类并拒绝非列表中的类
- "jdk.serialFilter=org.example.*;!*"
// 匹配任何以设置样式为前缀的类
- "jdk.serialFilter=*;
5、内置过滤器
内置过滤器用于RMI Registry
、RMI 分布式垃圾收集器(DCG)和 Java 管理扩展(JMX)
RMI Registry
有一个内置的白名单过滤器,允许将对象绑定到注册表中。它包括的情况如下:
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
内置过滤器包括大小限制:
maxarray=1000000,maxdepth=20
RMI 分布式垃圾收集器有一个内置的白名单过滤器,它接受一组有限的类。它包括的情况如下:
java.rmi.server.ObjID
- ``java.rmi.server.UID`
java.rmi.dgc.VMID
java.rmi.dgc.Lease
内置过滤器包括大小限制:
maxarray=1000000,maxdepth=20
除了这些类之外,用户还可以使用sun.rmi.registry.registryFilter
(针对RMI Registry
)和sun.rmi.transport.dgcFilter
(针对DGC)系统或安全属性添加自己的自定义过滤器
对于JMX 过滤器
,可以在进行RMIServer.newClient
远程调用以及通过 RMI 向服务器发送反序列化参数时,指定要使用的反序列化过滤器模式字符串;还可以使用该management.properties
文件向默认代理提供过滤器模式字符串
JEP290 值得注意的点
- JEP290需要手动设置,只有设置了之后才会有过滤,没有设置的话就还是可以正常的反序列化漏洞利用
- JEP290默认只为 RMI 注册表(RMI Register层)、 RMI分布式垃圾收集器(DGC层)以及 JMX 提供了相应的内置过滤器
JEP290的实际限制
来一个RMIServer
package com.ch1e;
import java.rmi.Naming;
import java.rmi.registry.LocateRegistry;
public class RMIServer {
public static String HOST = "127.0.0.1";
public static int PORT = 1099;
public static String RMI_PATH = "/hello";
public static final String RMI_NAME = "rmi://" + HOST + ":" + PORT + RMI_PATH;
public static void main(String[] args) {
try {
// 注册RMI端口
LocateRegistry.createRegistry(PORT);
// 创建一个服务
Hello hello = new HelloImpl();
// 服务命名绑定
Naming.rebind(RMI_NAME, hello);
System.out.println("启动RMI服务在" + RMI_NAME);
} catch (Exception e) {
e.printStackTrace();
}
}
}
然后来个Hello接口
package com.ch1e;
import java.rmi.Remote;
import java.rmi.RemoteException;
public interface Hello extends Remote {
String hello() throws RemoteException;
String hello(String name) throws RemoteException;
String hello(Object object) throws RemoteException;
}
来个Hello接口的实现类
package com.ch1e;
import java.rmi.RemoteException;
import java.rmi.server.UnicastRemoteObject;
public class HelloImpl extends UnicastRemoteObject implements Hello{
protected HelloImpl() throws RemoteException {
}
@Override
public String hello() throws RemoteException {
return "hello world";
}
@Override
public String hello(String name) throws RemoteException {
return "hello"+name;
}
@Override
public String hello(Object object) throws RemoteException {
System.out.println(object);
return "hello "+object.toString();
}
}
在java8u65的环境下用CC1打RMI,攻击成功。
切换成java8u341就会报错
信息: ObjectInputFilter REJECTED: class sun.reflect.annotation.AnnotationInvocationHandler, array length: -1, nRefs: 8, depth: 2, bytes: 296, ex: n/a
JEP290的过滤过程
本地先写一个Client客户端
package com.ch1e;
import java.rmi.NotBoundException;
import java.rmi.RemoteException;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
public class Client {
public static void main(String[] args) throws NotBoundException, RemoteException {
Registry registry= LocateRegistry.getRegistry("127.0.0.1",1099);
HelloImpl remote = (HelloImpl) registry.lookup("rmi://127.0.0.1:1099/hello");
System.out.println(remote.hello());
}
}
在RMI的流程中,远程引用层和客户端服务端两个交互的类是RegistryImpl_Skel和RegistryImpl_Stub,在服务端的RegistryImpl_Skel
类中,向注册中心进行bind、rebind操作时均进行了readObject操作以此拿到Remote远程对象引用。
在RegistryImpl_Skel
类的dispatch方法下断点,会调用到ObjectInputStream#readObject方法,然后回调用到readObject0
方法
在readObject0中会对tc有一个switch循环,然后调用了readOrdinaryObject方法
调用到readClassDesc方法
继续调用readProxyDesc方法
接着走到了filterCheck方法,继续跟进
继续调用checkInput方法
来看看代码,有些地方是返回了REJECTED,说明被过滤了,大概就是深度超过20以及return String.class != var2 && !Number.class.isAssignableFrom(var2) && !Remote.class.isAssignableFrom(var2) && !Proxy.class.isAssignableFrom(var2) && !UnicastRef.class.isAssignableFrom(var2) && !RMIClientSocketFactory.class.isAssignableFrom(var2) && !RMIServerSocketFactory.class.isAssignableFrom(var2) && !ActivationID.class.isAssignableFrom(var2) && !UID.class.isAssignableFrom(var2) ? Status.REJECTED : Status.ALLOWED;
为真的情况下
JEP290的绕过
在RMI远程方法调用过程中,方法参数需要先序列化,从本地JVM发送到远程JVM,然后在远程JVM上反序列化,执行完后,将结果序列化,发送回本地JVM,而在本地的参数是我们可以控制的,我们可以控制参数是反序列化链
如果目标的RMI服务暴漏了Object参数类型的方法,我们就可以注入payload进去。主要是在sun.rmi.server.UnicastRef#unmarshalValue中判断了远程调用方法的参数类型