长亭百川云 - 文章详情

Java反序列化流程总结

技术猫屋

53

2024-07-13

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单参数构造方法,该方法内容如下:

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()方法先验证魔数和序列化的版本是否匹配

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

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

这个方法是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函数;

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

思考:unsharedMarker是什么?

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

接着进入readClassDescriptor方法:

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

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

这些字段信息包括:TypeCodefieldNamefieldType

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

最后再通过流程图回顾一下整个序列化的流程(看不清楚可以点击原文链接):

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

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

相关推荐
关注或联系我们
添加百川云公众号,移动管理云安全产品
咨询热线:
4000-327-707
百川公众号
百川公众号
百川云客服
百川云客服

Copyright ©2024 北京长亭科技有限公司
icon
京ICP备 2024055124号-2