Java反序列化流程总结

admin 2021年6月14日13:04:21评论114 views字数 8592阅读28分38秒阅读模式

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


Java反序列化流程总结

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


Java反序列化流程总结


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


Java反序列化流程总结


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


Java反序列化流程总结



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

Java反序列化流程总结

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

Java反序列化流程总结

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

Java反序列化流程总结


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

Java反序列化流程总结

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

Java反序列化流程总结

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

Java反序列化流程总结


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

Java反序列化流程总结

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

Java反序列化流程总结

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

Java反序列化流程总结

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

Java反序列化流程总结

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

Java反序列化流程总结

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

Java反序列化流程总结

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


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

Java反序列化流程总结

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



Java反序列化流程总结

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


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


Java反序列化流程总结

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

Java反序列化流程总结

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


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


思考:unsharedMarker是什么?


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


接着进入readClassDescriptor方法:


Java反序列化流程总结

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

Java反序列化流程总结

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

Java反序列化流程总结

这些字段信息包括:TypeCodefieldNamefieldType


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


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


Java反序列化流程总结

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


Java反序列化流程总结


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

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

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

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

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

Java反序列化流程总结


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


Java反序列化流程总结

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

Java反序列化流程总结


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

Java反序列化流程总结

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

Java反序列化流程总结

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

Java反序列化流程总结

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

Java反序列化流程总结

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


Java反序列化流程总结

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

Java反序列化流程总结

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


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


Java反序列化流程总结


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


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


Java反序列化流程总结


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


Java反序列化流程总结


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


Java反序列化流程总结


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成员属性


原文始发于微信公众号(技术猫屋):Java反序列化流程总结

  • 左青龙
  • 微信扫一扫
  • weinxin
  • 右白虎
  • 微信扫一扫
  • weinxin
admin
  • 本文由 发表于 2021年6月14日13:04:21
  • 转载请保留本文链接(CN-SEC中文网:感谢原作者辛苦付出):
                   Java反序列化流程总结https://cn-sec.com/archives/902001.html

发表评论

匿名网友 填写信息