java - jdk.serialFilter 不能用于限制 Java 中 TreeMap 的深度(防止通过 Java 进行 DoS 攻击)

标签 java security deserialization treemap denial-of-service

如何通过 Java TreeMap 防止 DoS 攻击?

我的代码有一个接受 Map 对象的 API。现在我想阻止客户端发送一定长度的 Map 对象。

现在 maxarray 中的 jdk.serialFilter 能够阻止客户端发送大小 > HashMapmaxarray 对象。

我也想对 TreeMap 做同样的事情。但是 maxarray 字段不适用于 TreeMap 。它无法拒绝该请求。

我也设置了 maxdepth 大小。但没有任何效果。

任何人都可以帮我解决这个问题吗?

最佳答案

TL; 博士;

这是探索处理 TreeMap 序列化代码的整个冒险,但我设法找到了金子。对于黄金(代码),一直滚动到答案的底部。如果您想遵循推论过程以便您可以在其他类(class)中执行此操作,则必须努力解决我的杂乱无章的问题。

我可能会使它更简洁,但我只花了 7 个小时阅读代码和实验,我现在已经厌倦了,这篇文章可能对希望进行这次冒险的其他人有所启发。

介绍

我的攻击途径是,反序列化整个事物占用太多内存,分配您可能不想使用的对象或占用内存。所以我想只读取原始数据,并检查 TreeMap 大小。这样我们就有了唯一需要的数据,以评估我们是否应该接受。是的,这意味着如果数据被接受,则读取数据两次,但这是您希望使用它时需要进行的权衡。这段代码跳过了很多 java 使用的验证步骤,因为我们对此不感兴趣。我们只需要一种可靠的方法来获得 TreeMap 大小,而不必加载包含所有数据的整个树形图。

通常你会加载所有数据,读取整个文件/字节流并使用它来初始化,我们只需要读取文件开头的部分内容。减少需要完成的工作和需要浪费的时间。我们只需要以一种可靠的方式向前移动文件读取指针,这样我们就可以始终为我们的进程获取正确的字节。这大大减少了 java 进程的工作流。通过快速的正常文件读取检查大小后,它可以通过实际的序列化过程,或者被丢弃。与完成的正常工作相比,这只是一点点开销,但可以作为一个有效的障碍,同时在您接受的标准中保持灵活性。

normal work vs checking work

开始冒险

查看 TreeMap https://github.com/openjdk-mirror/jdk7u-jdk/blob/master/src/share/classes/java/util/TreeMap.java#L123 的源代码,我们看到大小是一个 transient 值。这意味着它不会在序列化数据中编码,因此无法通过从发送的字节中读取字段值来快速检查它。

但是……并不是所有的希望都破灭了。因为如果我们检查 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 ,遗憾的是它也是私有(private)的。在这一点上,我得到的印象是我在搞乱我不应该接触的权力。但我继续前进,无视禁止标志,冒险等待!
在这个方法中调用了 getDeclaredSerialFieldsgetDefaultSerialFields,所以我们可以使用它的内容来模拟它的功能。
分析 getDeclaredSerialFields 我们看到它只有在 TreeMap 类中声明了 serialPersistentFields 时才有效。 TreeMap 或其父 AbstractMap 都不包含此字段。所以我们忽略 getDeclaredSerialFields 方法。到 getDefaultSerialFields

因此,如果我们使用该代码,摆弄它,我们可以获得有意义的数据,并且我们看到 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);

Found field comparator
1



L1951 回到 ObjectInputStream 我们看到这个大小是用来创建一个数组作为读取缓冲区,然后将它们完全读取,参数为空数组、偏移量 0、字段长度 (1) 和 false。该方法在 BlockDataInputStream 中调用,false 表示不会被复制。这只是用于处理具有 PeekInputStream(in) 的数据流的辅助方法,我们可以在流上使用相同的方法进行一些摆弄,尽管我们现在不需要它,因为 TreeMap 中没有存储原始类型。所以我将把这个思路留给这个答案。

L1964 调用 readObject0 读取 TreeMap 中使用的比较器。它检查 oldMode,它返回是否以块数据模式读取流,我们可以看到它在 readFields 中设置为流模式(false),因此我将跳过该部分。
L1315 简单检查递归不会发生不止一次,而是偷看一个字节。让我们看看 TreeMap 必须为此提供什么。
这花了我比预期更长的时间。我不能在这里发布代码,它太长了,但我把它放在 ideonegist 上。

  • Basically you need to copy over the inline class BlockDataInputStream,
  • add 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); to BlockDataInputStream. If you actually need to use these methods substitute them with something Java. It will give a runtime error.
  • copy over the inline class PeekInputStream
  • copy over the java.io.Bits class.
  • The TC_ references need to point to 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"));

Does b (-84) equals TC_RESET?no



我们看到我们读取了一个 0xAC,让我们走捷径,看看 java.io.ObjectStreamConstants 是什么。没有纯粹 0xAC 的条目,但它似乎确实是 header 的一部分。
让我们从 readStreamHeader 进行完整性检查,并在我们的 peekByte 代码之前插入该方法的内容,再次更新 TC_ 引用。我们现在得到 0x73 的输出。 Progress !
0x73 是 TC_OBJECT 所以让我们跳到 L1347
在那里我们发现调用了 readOrdinaryObject,它执行了 readByte()。
然后读取 classDescription 跳转到 readNonProxy
然后我们调用 readUTF()、readLong()、readByte()、readShort,读取字段...,然后对于每 field 个 readByte(),readUTF()。

所以,让我们模仿一下。我 encounter 的第一件事是它试图读取超出类名的字符串长度(29184 个字符类名?不这么认为),所以我错过了一些东西。我不知道此时我缺少什么,但我在 ideone 上运行它,也许它在 java 版本上运行,在那里他们在读取 UTF 之前添加了一个额外的字节。老实说,我懒得去查了。它有效,我很高兴。
无论如何,在读取一个额外的字节后,它运行得很好,我们是 right where we want to be 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());
        }
    }

现在我们继续在 Line 1771 上查看读取类描述后读取的内容。在此之后,有很多对象实例化检查等……就像意大利面条一样,我不想深入研究。让我们开始黑客攻击并分析数据。

数据作为字符串

tLjava/util/Comparator;xppwsrjava.lang.Integer¬ᅠᄂ￷チヌ8Ivaluexrjava.lang.Numberニᆲユヤ¢ヒ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



T 是
我们知道元素的大小写在元素之前。
Data1 - Date5 字段是存储在 map 中的值。所以当 Data1sq 部分出现时,一切都没有实际意义。
让我们向 map 添加一个项目,看看哪个值发生了变化!

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 - value 4 - value 0 - value 0 - value 0 - value 6



我们可以在这里推理:

After block data, an integer is written. An integer is four bytes. hence the four. The next four positions make up the integer.



让我们看看如果我们向树状图中添加一个比较器会发生什么。

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

Found field comparator
1
java.util.TreeMap
Is string (java.util.TreeMap)it a java.util.TreeMap? yes
Read field name comparator
Ljava/util/Comparator;starts with L yes
reading object
java.util.Collections$ReverseComparator
Is string (java.util.Collections$ReverseComparator)it a java.util.TreeMap? no
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());
    }

77
The length is 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 following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
import java.util.*;
import java.lang.*;
import java.io.*;

/* Name of the class has to be "Main" only if the class is public. */
class Ideone
{
    public static void main (String[] args) throws java.lang.Exception
    {

        doTest(1,true);
        doTest(1,false);

        doTest(20,true);
        doTest(20,false);

        doTest(4,true);
        doTest(19,false);
    }

    public static void doTest(int size, boolean comparator) throws java.lang.Exception {
        SerializedTreeMapAnalyzer analyzer = new SerializedTreeMapAnalyzer();
        System.out.println(analyzer.getSize(Ideone.getTreeMapInputStream(size,comparator)));
    }

    public static ByteArrayInputStream getTreeMapInputStream(int size, boolean comparator) throws Exception {
      TreeMap<Integer, String> tmap = 
             new TreeMap<Integer, String>(comparator?Collections.reverseOrder():null);

      /*Adding elements to TreeMap*/
      for(int i = 0; size > 0 && i < size; i++) {
        tmap.put(i, "Data"+i);
      }

      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      ObjectOutputStream oos = new ObjectOutputStream( baos );
      oos.writeObject( tmap );
      oos.close();
      return  new ByteArrayInputStream(baos.toByteArray());
    }
}

class SerializedTreeMapAnalyzer 
{
    public int getSize(InputStream stream) throws IOException, StreamCorruptedException, Exception {
        BlockDataInputStream bin = new BlockDataInputStream(stream);
        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) {
            this.readObject(bin,true);
        }

        if(bin.readByte() == java.io.ObjectStreamConstants.TC_STRING) {
            String className = bin.readUTF();

            if(className.charAt(0) == 'L') {
                // Skip two bytes
                bin.readByte();
                bin.readByte();
                b = bin.peekByte();
                if(b == java.io.ObjectStreamConstants.TC_OBJECT) {

                    this.readObject(bin,true);
                    bin.readByte();
                    bin.readByte();
                }
                else {
                    // remove the null byte so we end up at same position
                    bin.readByte();
                }
            }
        }
        bin.readByte();
        if(bin.readByte() == (byte)4) {
            return bin.readInt();
        }
        return -1;
    }

    protected 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();
            bin.readLong();
            bin.readByte();
            short fields = bin.readShort();
            for(short i = 0; i < fields; i++) {
                bin.readByte();
                bin.readUTF();
            }
        }
    }
}

1
1
20
20
4
19

关于java - jdk.serialFilter 不能用于限制 Java 中 TreeMap 的深度(防止通过 Java 进行 DoS 攻击),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/57253282/

相关文章:

php - 使用Javascript XmlHttpRequest和PHP保护Web服务调用的安全

java - 如何在不同的程序中使用相同的 IvParameterSpec 对象?

asp.net-core - ASP.NET Core 从主体反序列化,带默认值

java - 在 Java 中使用序列化(使用 writeObject 方法)写入文件

java - 如何使用 iText 阅读多列 pdf?

php - 如何在 PHP 中使用 Blowfish 创建和存储密码哈希

java - 如何为扩展 ArrayAdapter<HashMap<String, String>> 的 Activity 扩展 AppCompatActivity?

c# - 使用 RestSharp 反序列化 JSON 数组

java - 在 jar 中列出并加载类

java - 如何在同一个方法中运行2个线程而不卡住?