lea*_*ner 7 java security denial-of-service treemap deserialization
如何通过 Java 防止 DoS 攻击TreeMap?
我的代码有一个接受Map对象的 API 。现在我想阻止客户端发送Map一定长度的对象。
现在maxarrayinjdk.serialFilter能够阻止客户端发送HashMap大小 >的对象maxarray。
我也想这样做TreeMap。但是maxarrayfield 对TreeMap. 它无法拒绝该请求。
我也设置了maxdepth尺寸。但没有任何效果。
任何人都可以帮我解决这个问题吗?
这是探索处理 TreeMap 序列化代码的整个冒险,但我设法找到了金子。对于黄金(代码),一直滚动到答案的底部。如果您想遵循推论过程以便您可以在其他课程中执行此操作,则必须努力解决我的杂乱无章的问题。
我可能让它更简洁,但我只花了 7 个小时阅读代码和实验,我现在已经厌倦了,这篇文章可能对希望进行这次冒险的其他人有指导意义。
我的攻击途径是,反序列化整个事物占用太多内存,分配您可能不想使用的对象或占用内存。所以我想只读取原始数据,并检查 TreeMap 大小。这样我们就有了唯一需要的数据,以评估我们是否应该接受。是的,这意味着如果数据被接受,则读取数据两次,但这是您希望使用它时需要进行的权衡。这段代码跳过了很多 java 使用的验证步骤,因为我们对此不感兴趣。我们只需要一种可靠的方法来获得 TreeMap 大小,而不必加载包含所有数据的整个树形图。
通常你会加载所有数据,读取整个文件/字节流并使用它来初始化,我们只需要读取文件开头的部分内容。减少需要完成的工作和需要浪费的时间。我们只需要以一种可靠的方式向前移动文件读取指针,这样我们就可以始终为我们的进程获取正确的字节。这大大减少了 java 进程的工作流。通过快速的正常文件读取检查大小后,它可以通过实际的序列化过程,或者被丢弃。与完成的正常工作相比,这只是一点点开销,但可以作为一个有效的障碍,同时在您接受的标准中保持灵活性。
查看TreeMap的源代码https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/TreeMap.java#L123我们看到大小是一个瞬态价值。这意味着它不会在序列化数据中编码,因此无法通过从发送的字节中读取字段值来快速检查它。
但是……并不是所有的希望都破灭了。因为如果我们检查writeObject()我们看到大小是编码https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/TreeMap.java#L2268
这意味着我们有字节值可以检查发送的原始数据!。
现在让我们检查 defaultReadObject 它做了什么。
L492首先它检查它是否正在反序列化,如果不是它阻塞。好吧,对我们来说并不有趣。
L495然后它想要对象实例,SerialCallbackContext是用这个初始化的,所以它不执行读取。
L496然后它从SerialCallbackContext获取一个 ObjectStreamClass 实例,所以现在我们将使用 ObjectStream。
L497一些模式被改变了,但随后我们去阅读这些领域。
好吧,再次移动到 ObjectInputStream
L1944一个类引用,该类引用提供给对象流实例化器(快速纲要L262,它在L442 中设置),因此它不执行读取。
L1949得到与默认字段的大小getPrimDataSize,这是在设置computeFieldOffsets方法。这对我们很有用,唯一的遗憾是......它不可访问,所以让我们弄清楚如何模拟它,作为一个注释。
L1255它使用字段变量。这是在getSerialFields设置的,遗憾的是它也是私有的。在这一点上,我得到的印象是我在搞乱我不应该接触的权力。但我继续前进,无视禁止标志,冒险等待!
getDeclaredSerialFields和getDefaultSerialFields在这个方法中被调用,所以我们可以使用它的内容来模拟它的功能。
分析getDeclaredSerialFields,我们看到它只有在TreeMap类中声明了 serialPersistentFields 时才有效。TreeMap 或其父AbstractMap都不包含此字段。所以我们忽略了getDeclaredSerialFields方法。到获取默认序列字段
因此,如果我们采用该代码,摆弄它,我们可以获得有意义的数据,并且我们看到 TreeMap 有一个字段,现在我们有一种动态方法来“模拟”获取默认字段,无论出于何种原因,情况都会发生变化。
https://ideone.com/UqqKSG(我给类名留下了完整路径,这样更容易查看我正在使用哪些类)
java.lang.reflect.Field[] clFields = TreeMap.class.getDeclaredFields();
ArrayList<java.lang.reflect.Field> list = new ArrayList<>();
int mask = java.lang.reflect.Modifier.STATIC | java.lang.reflect.Modifier.TRANSIENT;
for (int i = 0; i < clFields.length; i++) {
// Check for non transient and non static fields.
if ((clFields[i].getModifiers() & mask) == 0) {
list.add(clFields[i]);
System.out.println("Found field " + clFields[i].getName());
}
}
int size = list.size();
System.out.println(size);
Run Code Online (Sandbox Code Playgroud)
找到的字段比较器
1
L1951回到 ObjectInputStream 中,我们看到这个大小用于创建一个数组,用作读取的缓冲区,然后将它们完全读取,参数为空数组、偏移量 0、字段长度 (1) 和 false。该方法在BlockDataInputStream 中调用,false 表示不会被复制。这只是一个使用PeekInputStream(in)处理数据流的辅助方法,我们可以在流上使用相同的方法进行一些摆弄,尽管我们现在不需要它,因为没有存储原始类型在树图中。所以我将把这个思路留给这个答案。
L1964调用readObject0,它读取 TreeMap 中使用的比较器。它检查 oldMode,它返回是否以块数据模式读取流,我们可以看到它在readFields 中设置为流模式(false),因此我将跳过该部分。
L1315简单检查递归不会发生不止一次,而是偷看一个字节。让我们看看 TreeMap 必须为此提供什么。这花了我比预期更长的时间。我不能在这里发布代码,它太长了,但我在ideone和gist上有它。
- 基本上你需要复制内联类 BlockDataInputStream,
- 添加
private static native void bytesToFloats(byte[] src, int srcpos, float[] dst, int dstpos, int nfloats);private static native void bytesToDoubles(byte[] src, int srcpos, double[] dst, int dstpos, int ndoubles);到 BlockDataInputStream。如果您确实需要使用这些方法,请用 Java 替换它们。它会给出运行时错误。- 复制内联类 PeekInputStream
- 复制 java.io.Bits 类。
- TC_ 引用需要指向
java.io.ObjectStreamConstants.TC_
BlockDataInputStream bin = new BlockDataInputStream(getTreeMapInputStream());
bin.setBlockDataMode(false);
byte b = bin.peekByte();
System.out.println("Does b ("+String.format("%02X ", b)+") equals TC_RESET?" + (java.io.ObjectStreamConstants.TC_RESET == b ? "yes": "no"));
Run Code Online (Sandbox Code Playgroud)
b (-84) 是否等于 TC_RESET?否
我们看到我们读取了一个 0xAC,让我们走捷径,在java.io.ObjectStreamConstants 中查看它是什么。没有纯粹 0xAC 的条目,但它似乎确实是标头的一部分。
让我们从readStreamHeader进行完整性检查,并在我们的 peekByte 代码之前插入该方法的内容,再次更新 TC_ 引用。我们现在得到 0x73 的输出。进步!
0x73 是 TC_OBJECT 所以让我们跳到L1347
在那里我们发现readOrdinaryObject被调用,它做了一个 readByte()。
然后读取classDescription并跳转到readNonProxy
然后我们调用 readUTF()、readLong()、readByte()、readShort,以读取字段...,然后对每个字段调用readByte()、readUTF()。
所以,让我们模仿一下。我遇到的第一件事是它试图读取超出类名的字符串长度(29184 个字符类名?不这么认为),所以我错过了一些东西。我不知道此时我错过了什么,但我在 ideone 上运行它,也许它在 java 版本上运行,在那里他们在读取 UTF 之前添加了一个额外的字节。老实说,我懒得去查了。它有效,我很高兴。无论如何,在读取一个额外的字节后,它运行得很好,我们就在我们想要的地方。TODO:找出读取额外字节的位置
BlockDataInputStream bin = new BlockDataInputStream(getTreeMapInputStream());
bin.setBlockDataMode(false);
short s0 = bin.readShort();
short s1 = bin.readShort();
if (s0 != java.io.ObjectStreamConstants.STREAM_MAGIC || s1 != java.io.ObjectStreamConstants.STREAM_VERSION) {
throw new StreamCorruptedException(
String.format("invalid stream header: %04X%04X", s0, s1));
}
byte b = bin.readByte();
if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
bin.readByte();
String name = bin.readUTF();
System.out.println(name);
System.out.println("Is string ("+name+")it a java.util.TreeMap? "+(name.equals("java.util.TreeMap") ? "yes":"no"));
bin.readLong();
bin.readByte();
short fields = bin.readShort();
for(short i = 0; i < fields; i++) {
bin.readByte();
System.out.println("Read field name "+bin.readUTF());
}
}
Run Code Online (Sandbox Code Playgroud)
现在我们继续在第 1771 行,看看在阅读类描述后阅读了什么。在此之后,有很多对象实例化检查等......就像意大利面条一样,我不想深入研究。让我们开始黑客攻击并分析数据。
数据作为字符串
tLjava/util/Comparator;xppwsrjava.lang.Integer??????8Ivaluexrjava.lang。数字??????xptData1sq~tData5sq~tData4sq~tData2sq~FtData3x -74 -00 -16 -4C -6A -61 -76 -61 -2F -75 -74 -69 -6C -2F -43 -6F -6D -70 -61 -72 -61 -74 -6F -72 -3B -78 -70 -70 -77 -04 -00 -00 -00 -05 -73 -72 -00 -11 -6A -61 -76 -61 -2E -6C -61 -6E -67 -2E -49 -6E -74 -65 -67 -65 -72 -12 -E2 -A0 -A4 -F7 -81 -87 -38 -02 -00 -01 -49 -00 -05 -76 -61 -6C -75 -65 -78 -72 -00 -10 -6A -61 -76 -61 -2E -6C -61 -6E -67 -2E -4E -75 -6D -62 -65 -72 -86 -AC -95 -1D -0B -94 -E0 -8B -02 -00 -00 -78 -70 -00 -00 -00 -01 -74 -00 -05 -44 -61 -74 -61 -31 -73 -71 -00 -7E -00 -03 -00 -00 -00 -02 -74 -00 -05 -44 -61 -74 -61 -35 -73 -71 -00 -7E -00 -03 -00 -00 -00 -04 -74 -00 -05 -44 -61 -74 -61 -34 -73 -71 -00 -7E -00 -03 -00 -00 -00 -17 -74 -00 -05 -44 -61 -74 -61 -32 -73 -71 -00 -7E -00 -03 -00 -00 -00 -46 -74 -00 -05 -44 -61 -74 -61 -33 -78-61 -33 -78-61 -33 -78
T是我们知道元素的大小写在元素之前。Data1 - Date5 字段是存储在地图中的值。所以当 Data1sq 部分出现时,一切都没有实际意义。让我们向地图添加一个项目,看看哪个值发生了变化!
74 -00 -16 -4C -6A -61 -76 -61 -2F -75 -74 -69 -6C -2F -43 -6F -6D -70 -61 -72 -61 -74 -6F -72 -3B - 78 -78 -70 -70 -77 -04 -00 -00 -00 -05 -73 -72 -00 -11 -6A -61 -76 -61 -2E
74 -00 -16 -4C -6A -61 -76 -61 -2F -75 -74 -69 -6C -2F -43 -6F -6D -70 -61 -72 -61 -74 -6F -72 -3B -78 -78 -70 -70 -77 -04 -00 -00 -00 -06 -73 -72 -00 -11 -6A -61 -76 -61 -2E
好的,现在我们知道我们还需要屠宰多少口。让我们看看我们是否可以用给定的值在这里推导出一些逻辑。
第一个值是 74。检查 ObjectStreamConstants 我们看到它代表一个字符串。让我们读取该字节,然后读取 UTF。
现在我们还有剩下的,-70 -70 -77 -04 -00 -00 -00 -06
让我们把它放在常量之外。
NULL - NULL - BLOCKDATA - 值 4 - 值 0 - 值 0 - 值 0 - 值 6
我们可以在这里推理:
在块数据之后,写入一个整数。一个整数是四个字节。因此四个。接下来的四个位置构成整数。
让我们看看如果我们向树状图中添加一个比较器会发生什么。
xpsr'java.util.Collections$ReverseComparatord??SNJ?xpwsrjava.lang.Integer??????8I
-78 -70 -73 -72 -00 -27 -6A -61 -76 -61 -2E -75 - 74 -69 -6C -2E -43 -6F -6C -6C -65 -63 -74 -69 -6F -6E -73 -24 -52 -65 -76 -65 -72 -73 -65 -43 -6F - 6D -70 -61 -72 -61 -74 -6F -72 -64 -04 -8A -F0 -53 -4E -4A -D0 -02 -00 -00 -78 -70 -77 -04 -00 -00 - 00 -06
我们看到 END_BLOCK、NULL、OBJECT
好的。所以现在我们知道第二个 Null 是 Comparator 数据的持有者。所以我们可以偷看那个。我们需要跳过两个字节,然后查看它是否是对象字节。如果是,我们需要读取对象数据,以便到达我们想要的位置。
让我们停下来回顾一下到目前为止的代码:https : //ideone.com/ma6nQy
BlockDataInputStream bin = new BlockDataInputStream(getTreeMapInputStream());
bin.setBlockDataMode(false);
short s0 = bin.readShort();
short s1 = bin.readShort();
if (s0 != java.io.ObjectStreamConstants.STREAM_MAGIC || s1 != java.io.ObjectStreamConstants.STREAM_VERSION) {
throw new StreamCorruptedException(
String.format("invalid stream header: %04X%04X", s0, s1));
}
byte b = bin.peekByte();
if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
Ideone.readObject(bin,true);
}
if(bin.readByte() == java.io.ObjectStreamConstants.TC_STRING) {
String className = bin.readUTF();
System.out.println(className + "starts with L "+(className.charAt(0) == 'L' ? "yes": "no"));
if(className.charAt(0) == 'L') {
// Skip two bytes
bin.readByte();
bin.readByte();
b = bin.peekByte();
if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
System.out.println("reading object");
Ideone.readObject(bin,true);
}
else {
// remove the null byte so we end up at same position
bin.readByte();
}
}
}
int length = 50;
byte[] bytes = new byte[length];
for(int c=0;c<length;c++) {
bytes[c] = bin.readByte();
System.out.print((char)(bytes[c]));
}
for(int c=0;c<length;c++) {
System.out.print("-"+String.format("%02X ", bytes[c]));
}
}
public static void readObject(BlockDataInputStream bin, boolean doExtra) throws Exception {
byte b = bin.readByte();
if(b == java.io.ObjectStreamConstants.TC_OBJECT) {
if(doExtra) {
bin.readByte();
}
String name = bin.readUTF();
System.out.println(name);
System.out.println("Is string ("+name+")it a java.util.TreeMap? "+(name.equals("java.util.TreeMap") ? "yes":"no"));
bin.readLong();
bin.readByte();
short fields = bin.readShort();
for(short i = 0; i < fields; i++) {
bin.readByte();
System.out.println("Read field name "+bin.readUTF());
}
}
}
Run Code Online (Sandbox Code Playgroud)
找到字段比较器
1
java.util.TreeMap
字符串 (java.util.TreeMap) 是 java.util.TreeMap 吗?是
读取字段名称比较器
Ljava/util/Comparator;以 L 开头是
读取对象
java.util.Collections$ReverseComparator
字符串 (java.util.Collections$ReverseComparator) 是 java.util.TreeMap 吗?没有
xpwsrjava.lang.Integer??????8Ivaluexr
-78 -70 -77 -04 -00 -00 -00 -06 -73 -72 -00 -11 -6A -61 -76 -61 -2E -6C - 61 -6E -67 -2E -49 -6E -74 -65 -67 -65 -72 -12 -E2 -A0 -A4 -F7 -81 -87 -38 -02 -00 -01 -49 -00 -05 - 76 -61 -6C -75 -65 -78 -72
可悲的是,我们最终并没有在时间轴上达到相同的点。
当有一个比较器时,我们以:
-78 -70 -77 -04 -00 -00 -00 -06
当比较器被移除时,我们以:
-77 -04 -00 -00 -00 -06
嗯。BLOCK END 和 NULL 看起来很熟悉。这些是我们在读取比较器时跳过的相同字节。这两个字节总是被删除,但显然,比较器也广告他们自己的 BLOCK END 和 NULL 值。
所以,如果有一个比较器,删除两个尾随字节,这样我们就得到了我们想要的,一致的。https://ideone.com/pTu8Fd
-77 -04 -00 -00 -00 -06
然后我们跳过下一个BLOCKDATA标记(77)并到达黄金!
添加额外的行,我们得到我们的输出:https : //ideone.com/wy0uF2
System.out.println(String.format("%02X ", bin.readByte()));
if(bin.readByte() == (byte)4) {
System.out.println("The length is "+ bin.readInt());
}
Run Code Online (Sandbox Code Playgroud)
77
长度为6
我们有我们需要的神奇数字!
好的。推断完成,让我们清理它
可运行片段:https : //ideone.com/J6ovMy
完整代码也作为要点:https : //gist.github.com/tschallacka/8f89982e9569d0b9974dff37d8f45faf
/**
This is dual licensed under MIT. You can choose wether you want to use CC-BY-SA or MIT.
Copyright 2020 Tschallacka
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the follo
不知道您的 API,但通常您会限制应用程序服务器接受的帖子大小。在 WildFly 中,您可以将该属性添加max-post-size到您的http/https-listener. 这将限制您的服务器愿意接收的数据量,从而限制每个请求可以处理的数据量。
另一种方法是引入速率限制之类的东西 - 当您的客户端执行太多查询时,您可以拒绝对数据的任何处理。这是限制个人客户消耗的处理能力的常见方法。由于您的 API 似乎没有开放(至少您没有说是),因此您可以在客户级别定义速率限制。对于您的情况,这可能是最好的方法。
对于您的方法:当您的服务器知道 Map 有多大时,它实际上已经接受并接收了数据,因此该资源已经消失(尽管处理可能受到限制)。
最后,您必须为您的用例选择合适的方式。你的情况听起来不像网络是瓶颈,而是计算能力。因此,我认为针对您的情况,有限的帖子大小和速率限制的组合将是最好的选择。
| 归档时间: |
|
| 查看次数: |
328 次 |
| 最近记录: |