反序列化流程分析总结

本文最后更新于 2021.06.13,总计 14669 字 ,阅读本文大概需要 24 ~ 106 分钟
本文已超过 98天 没有更新。如果文章内容或图片资源失效,请留言反馈,我会及时处理,谢谢!

0x01 写在前面

同前一篇的分析方法一样,推荐复制demo代码,然后一步一步跟随笔者的分析进行debug调试跟随,这样跟能够帮助读者理解此文。

0x02 流程分析

在上一篇《 序列化流程分析总结》一文中我提到了

所谓的序列化即是一个将对象写入到IO流中的过程。序列化的步骤通常是首先创建一个ObjectOutputStream输出流,然后调用ObjectOutputStream对象的writeObject方法,按照一定格式(上面提到的)输出可序列化对象。

所以其实反序列化和序列化是一个相反的过程——所谓的反序列化即是从IO流中读出对象的过程。反序列化的步骤通常是首先创建一个ObjectInputStream输入流,然后调用ObjectInputStream对象的readObject方法读出序列化的内容。

如下段demo代码:

package com.panda.alipay;
import java.io.*;
public class Main {
    public static class Demo implements Serializable {
        private String string;
        transient String name = "hello";
        public Demo(String s) {
            this.string = s;
        }
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            Demo demo = new Demo("panda");
            ObjectOutputStream outputStream = new ObjectOutputStream(new FileOutputStream("panda.out"));
            outputStream.writeObject(new Demo("panda"));
            outputStream.close();
            ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("panda.out"));
            inputStream.readObject();

        }
    }
}

整个代码中最关键的两行为:

    ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("panda.out"));
          inputStream.readObject();

这两行其实就包括了整个反序列化的流程。

首先来看ObjectInputStreamObjectInputStreamObjectOutputStream一样,是一个实现了ObjectInput接口的InputStream的子类,其类定义如下:

public class ObjectInputStream 
    extends InputStream implements ObjectInput, ObjectStreamConstants{
...
}

当我们实例化ObjectInputStream后,首先调用的是ObjectInputStream的构造方法。

ObjectInputStreamObjectOutputStream类一样有两个构造方法 —— 一个为public的单参数构造方法,一个为protected的无参构造方法

同样地,当我们实例化ObjectInputStream并传入new FileInputStream("panda.out")参数后,调用的是ObjectInputStream中的public单参数构造方法,该方法内容如下:

1.jpg

ObjectOutputStream的构造方法一样——在该构造函数的开始,首先会调用verifySubclass方法处理缓存信息,要求该类(或子类)进行验证——验证是否可以在不违反安全约束的情况下构造此实例。

然后和ObjectOutputStream不同的是,在ObjectOutputStream中我们初始化的对象是bouthandlessubs以及enableOverride,但是在ObjectInputStream中,我们初始化的对象变成了binhandlesvlist以及enableOverride

 /** filter stream for handling block data conversion */
    private final BlockDataInputStream bin;
    /** validation callback list */
    private final ValidationList vlist;
 /** wire handle -> obj/exception map */
    private final HandleTable handles;
 /** if true, invoke readObjectOverride() instead of readObject() */
    private final boolean enableOverride;

思考:binhandlesvlist以及enableOverride各代表什么意思?

首先对于handlesenableoverride来说其和在ObjectOutputStream中代表的含义相同:

handles:是一个哈希表,表示从对象到引用的映射

enableOverride:布尔型常量,用于决定在反序列化时选用readObjectOverride方法还是readObject方法

而对于bin来说其实同样把它当成bout去理解——因为他们作用基本相同

至于vlist成员属性,它主要用于提供一个callback操作的验证集合

bin被初始化后,也意味着实例化了一个BlockDataInputStream(不理解BlockDataInputStream的可以看我上一篇文章《 序列化流程分析总结》)

在几个成员属性都被初始化后,调用readStreamHeader()方法先验证魔数和序列化的版本是否匹配

2.png

如果不匹配则抛出序列化的StreamCorruptedMismatch异常:

3.jpg

ObjectInputStreampublic构造方法走完后,才会调用readObject()开始写对象数据,该方法的主要代码如下:

4.jpg

这个方法是ObjectInputStream对外的反序列化的入口,但其实它并不是核心方法,只是用于判断应该调用readObjectOverride还是readObject0方法(enableOverride决定)

由于在ObjectInputStreampublic构造方法中已经初始化了enableOverride = false,所以直接跳过第一个if分支(不调用readObjectOverride方法),进入readObject0方法,该方法如下(略长):

/**
     * Underlying readObject implementation.
     */
    private Object readObject0(boolean unshared) throws IOException {
        boolean oldMode = bin.getBlockDataMode();
        if (oldMode) {
            int remain = bin.currentBlockRemaining();
            if (remain > 0) {
                throw new OptionalDataException(remain);
            } else if (defaultDataEnd) {
                /*
                 * Fix for 4360508: stream is currently at the end of a field
                 * value block written via default serialization; since there
                 * is no terminating TC_ENDBLOCKDATA tag, simulate
                 * end-of-custom-data behavior explicitly.
                 */
                throw new OptionalDataException(true);
            }
            bin.setBlockDataMode(false);
        }

        byte tc;
        while ((tc = bin.peekByte()) == TC_RESET) {
            bin.readByte();
            handleReset();
        }

        depth++;
        totalObjectRefs++;
        try {
            switch (tc) {
                case TC_NULL:
                    return readNull();

                case TC_REFERENCE:
                    return readHandle(unshared);

                case TC_CLASS:
                    return readClass(unshared);

                case TC_CLASSDESC:
                case TC_PROXYCLASSDESC:
                    return readClassDesc(unshared);

                case TC_STRING:
                case TC_LONGSTRING:
                    return checkResolve(readString(unshared));

                case TC_ARRAY:
                    return checkResolve(readArray(unshared));

                case TC_ENUM:
                    return checkResolve(readEnum(unshared));

                case TC_OBJECT:
                    return checkResolve(readOrdinaryObject(unshared));

                case TC_EXCEPTION:
                    IOException ex = readFatalException();
                    throw new WriteAbortedException("writing aborted", ex);

                case TC_BLOCKDATA:
                case TC_BLOCKDATALONG:
                    if (oldMode) {
                        bin.setBlockDataMode(true);
                        bin.peek();             // force header read
                        throw new OptionalDataException(
                            bin.currentBlockRemaining());
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected block data");
                    }

                case TC_ENDBLOCKDATA:
                    if (oldMode) {
                        throw new OptionalDataException(true);
                    } else {
                        throw new StreamCorruptedException(
                            "unexpected end of block data");
                    }

                default:
                    throw new StreamCorruptedException(
                        String.format("invalid type code: %02X", tc));
            }
        } finally {
            depth--;
            bin.setBlockDataMode(oldMode);
        }
    }

来一点一点分析

readObject0最开始的地方: oldMode = bin.getBlockDataMode();用于获取当前的读取模式,检查是否是Data Block模式读取,如果检测的结果是Data Block模式,则先计算字节流中剩余的字节数量(currentBlockRemaining),剩余数量大于0或者defaultDataEnd的值为truedefaultDataEnd表示一个数据段的结束,在这里也就是说没有数据了)则抛出java.io.OptionalDataException异常信息

思考:为什么在这两种情况下会抛出java.io.OptionalDataException异常?

因为readObecjt0方法主要负责读取对象类型的数据,这些数据虽然本身是一个Data Block,但是在字节流中它并没有使用TC_BLOCKDATALONGTC_BLOCKDATA标记去表示这段的字节流是可选数据块,所以这个地方一旦发现还存在这两种类型的Data Block数据段,则直接抛出java.io.OptionalDataException异常,举个例子就是没有事先声明你要来我家,结果来了我家里,我就认为你是抢劫,所以要报警(异常)。

经过这些判断后,会在if分支的最后关闭Data Block模式;

开始读取字节流中的内容,如果读到了TC_RESET标记,那么调用handleReset方法去处理,如果没有那么继续向下读:

  • 如果读到了TC_NULL——调用readNull函数;

6.jpg

  • 如果读到了TC_REFERENCE——调用readHandle函数;

7.jpg

  • 如果读到了TC_CLASS——调用readClass函数;

8.jpg

  • 如果读到了TC_CLASSDESCTC_PROXYCLASSDESC——调用readClassDesc函数;

9.jpg

  • 如果读到了TC_STRINGTC_LONGSTRING——调用readString函数;

10.jpg

  • 如果读到了TC_ARRAY——调用readArray函数;

11.jpg

  • 如果读到了TC_ENUM——调用readEnum函数;

12.jpg

  • 如果读到了TC_OBJECT——调用readOrdinaryObject函数;

13.jpg

  • 如果读到了TC_EXCEPTION——调用readFatalExcception函数,然后抛出异常;

14.jpg

  • 如果读到了TC_BLOCKDATATC_BLOCKDATALONG——抛出异常信息,只是Data Block模式不同则抛出的异常信息不一样,开启Data Block模式;

15.jpg

  • 如果读到了TC_ENDBLOCKDATA——抛出异常信息,同上,只是不开启Data Block模式;

16.jpg

  • 其他情况直接抛出异常信息;

17.jpg

在上述过程中,如果遇见了TC_ARRAYTC_ENUMTC_OBJECTTC_STRING以及TC_LONGSTRING标记,那么会调用checkResolve方法以检查反序列化的对象中是否重写了readResolve方法:

18.jpg

若是重写,那么需要执行重写的Resolve流程,若没有重写,则 返回obj对象

在本demo中,最终走到的是readOrdinaryObject方法:

13.jpg

下断点后可以进入readOradinaryObject方法如下:

19.jpg

首先会再次判断读到的标识是不是TC_OBJECT,如果不是,那么直接抛出InternalError错误

然后利用readClassDesc方法从系统中读取当前Java对象所属类的描述信息:

20.jpg

由于 Demo 是一个类对象,那么会走进readNonProxyDesc

21.jpg

同样的,该方法也再次判断是否有TC_CLASSDESC标记,如果没有,那么抛出InternalError错误

然后判断读取模式是什么,如果是unshared,那么从handles对象的映射中读取一个新的desc,如果不是unshared,那么从unsharedMarker中读取对应的对象

思考:unsharedMarker是什么?

unsharedMarker用于存储对象的状态,可以把unsharedMarker当成一个识别unshared状态的标记,在反序列化重建的过程中,其unshared状态的对象和非unshared状态的反序列化步骤不完全相同。

接着进入readClassDescriptor方法:

22.jpg

readClassDescriptor会调用readNonProxy方法读取当前类的元数据信息:

23.jpg

在这个方法里,系统会先从字节流中读取类名信息name = in.readUTF();,其次从字节流中读取serialVersionUID的信息,然后再从字节流中读取各种SC_*标记信息,通过该标记信息设置对应的成员属性,最后从字节流中读取每一个字段的信息:

24.jpg

这些字段信息包括:TypeCodefieldNamefieldType

readNonProxy这里对应的方法是在序列化时使用的writeNonProxy方法,在writeNonProxy中写入的TypeCodefieldNamefieldType在这里被读取。

读取结束以后会依次跳出readNonProxyreadClassDescriptor方法,在获得类信息后会返回readNonProxyDesc接着走完下面的流程:

25.jpg

如上图中的流程,首先开启Data Block模式(bin.setBlockDataMode(true)),然后调用resolveClass方法处理当前类的信息:

26.jpg

之前我在《序列化流程分析总结》一文中提到:

annotateClass是提供给子类实现的方法,通常默认情况下这个方法什么也不做,与此类似的还有ObjectInputStream中的resolveClass方法。

实际上,ObjectInputStream中的resolveClassresolveProxyClassresolveObject这三个方法对应着ObjectOutputStream中定义的annotateClassannotateProxyClassreplaceObject方法,如果ObjectOutputStream的子类重写了这的三个方法,那么要求ObjectInputStream的子类也必须重写这三个方法对应的resolve方法。

在这里,resolveClass方法会根据字节流中读取的类描述信息加载本地类,加载的时候用到的就是我们平时用的Class.forName()的方法,实际上反序列化漏洞根本的原因就是在这里加载了Runtime类,然后执行了exec()方法。

处理完当前类的信息后,会调用filterCheck方法进行检测:

27.jpg

如果非空,那么调用序列化筛选器,这个筛选器调用了serialFilter.checkInput方法检查序列化数据,如果检测出来了异常,那么会令statusStatus.REJECTED状态,filterCheck将会根据serialFilter.checkInput的检查结果来决定是否执行反序列化,如果checkInput()方法返回Status.REJECTED,反序列化将会被阻止,并抛出InvalidClassException()错误:

28.jpg

如果checkInput()方法返回Status.ALLOWED,程序将可执行反序列化

29.jpg

在结束了反序列化内容检测后,会调用skipCustomData方法跳过所有数据块和对象,直到遇到TC_ENDBLOCKDATA标识

30.jpg

接着,会调用ObjectStreamClass中的initNonProxy方法:

31.jpg

在这个方法里会初始化表示非代理类的类描述符:

32.jpg

初始化完毕后会调用handlesfinish方法完成引用Handle的赋值操作:

33.jpg

最后将结果赋值给passHandle成员属性(初始定义为private int passHandle = NULL_HANDLE;

readNonProxyDesc方法结束,将得到的类描述信息赋值给descriptor变量:

34.jpg

经过validateDescriptor的验证后将descriptor作为结果返回给readOrdinaryObject方法。

35.jpg

经过了这么多方法的层层调用后,拿到了描述类信息,然后和序列化开始时类似,同样检测当前处理的对象是否是一个可反序列化的对象(checkDeserialize()),如果是,那么就从系统中读取当前Java对象所属类的描述信息(也叫做类元数据信息)

然后再经过getResolveException判断有无异常信息,若无,那么会返回obj对象,然后经过几个简单的判断后会调用handlesfinish方法完成引用Handle的赋值操作,最后将结果赋值给passHandle成员属性;

36.jpg

完成赋值操作后,在经过一些常规判断后,就结束了readOrdinaryObject方法

此时会返回到readObject0方法,在readObject0方法经过二次checkResolve后会返回readObject方法

37.jpg

在反序列执行完成过后,它会调用vlist成员的doCallbacks来执行完成过后的回调逻辑,然后结束所有的序列化流程。

38.jpg

最后再通过流程图回顾一下整个序列化的流程:

反序列化流程.jpg

0x03 总结

反序列化的流程比序列化的流程要复杂一点,在反序列化读取数据的时候,其中不仅包含了各种标识的读取和判读和各种类描述信息,还要判断所序列化的内容是否安全等。

反序列化是Java安全绕不开的一个话题,亦是Java安全重点之重,因此我认为对于Java的序列化和反序列化的过程,详细了解是很有必要的,本文写的略微臃肿和不足,各位看官轻拍

0x04 参考

https://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html

https://blog.csdn.net/silentbalanceyh/article/details/8294269

https://blog.csdn.net/u011315960/article/details/89963230

「感谢老板送来的软糖/蛋糕/布丁/牛奶/冰阔乐!」

panda

(๑>ڡ<)☆谢谢老板~

使用微信扫描二维码打赏

版权属于:

panda | 热爱安全的理想少年

本文链接:

https://www.cnpanda.net/sec/928.html(转载时请注明本文出处及文章链接)

添加新评论

暂无评论