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();
这两行其实就包括了整个反序列化的流程。
首先来看ObjectInputStream
,ObjectInputStream
和ObjectOutputStream
一样,是一个实现了ObjectInput
接口的InputStream
的子类,其类定义如下:
public class ObjectInputStream
extends InputStream implements ObjectInput, ObjectStreamConstants{
...
}
当我们实例化ObjectInputStream
后,首先调用的是ObjectInputStream
的构造方法。
ObjectInputStream
和ObjectOutputStream
类一样有两个构造方法 —— 一个为public
的单参数构造方法,一个为protected
的无参构造方法
同样地,当我们实例化ObjectInputStream
并传入new FileInputStream("panda.out")
参数后,调用的是ObjectInputStream
中的public
单参数构造方法,该方法内容如下:
和ObjectOutputStream
的构造方法一样——在该构造函数的开始,首先会调用verifySubclass
方法处理缓存信息,要求该类(或子类)进行验证——验证是否可以在不违反安全约束的情况下构造此实例。
然后和ObjectOutputStream
不同的是,在ObjectOutputStream
中我们初始化的对象是bout
、handles
、subs
以及enableOverride
,但是在ObjectInputStream
中,我们初始化的对象变成了bin
、handles
、vlist
以及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;
思考:bin
、handles
、vlist
以及enableOverride
各代表什么意思?
首先对于handles
和enableoverride
来说其和在ObjectOutputStream
中代表的含义相同:
handles
:是一个哈希表,表示从对象到引用的映射
enableOverride
:布尔型常量,用于决定在反序列化时选用readObjectOverride
方法还是readObject
方法
而对于bin
来说其实同样把它当成bout
去理解——因为他们作用基本相同
至于vlist
成员属性,它主要用于提供一个callback
操作的验证集合
当bin
被初始化后,也意味着实例化了一个BlockDataInputStream
(不理解BlockDataInputStream
的可以看我上一篇文章《 序列化流程分析总结》)
在几个成员属性都被初始化后,调用readStreamHeader()
方法先验证魔数和序列化的版本是否匹配
如果不匹配则抛出序列化的StreamCorruptedMismatch
异常:
当ObjectInputStream
的public
构造方法走完后,才会调用readObject()
开始写对象数据,该方法的主要代码如下:
这个方法是ObjectInputStream
对外的反序列化的入口,但其实它并不是核心方法,只是用于判断应该调用readObjectOverride
还是readObject0
方法(enableOverride
决定)
由于在ObjectInputStream
的public
构造方法中已经初始化了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
的值为true
(defaultDataEnd
表示一个数据段的结束,在这里也就是说没有数据了)则抛出java.io.OptionalDataException
异常信息
思考:为什么在这两种情况下会抛出java.io.OptionalDataException
异常?
因为readObecjt0
方法主要负责读取对象类型的数据,这些数据虽然本身是一个Data Block
,但是在字节流中它并没有使用TC_BLOCKDATALONG
或TC_BLOCKDATA
标记去表示这段的字节流是可选数据块,所以这个地方一旦发现还存在这两种类型的Data Block
数据段,则直接抛出java.io.OptionalDataException
异常,举个例子就是没有事先声明你要来我家,结果来了我家里,我就认为你是抢劫,所以要报警(异常)。
经过这些判断后,会在if分支的最后关闭Data Block
模式;
开始读取字节流中的内容,如果读到了TC_RESET
标记,那么调用handleReset
方法去处理,如果没有那么继续向下读:
- 如果读到了
TC_NULL
——调用readNull
函数;
- 如果读到了
TC_REFERENCE
——调用readHandle
函数;
- 如果读到了
TC_CLASS
——调用readClass
函数;
- 如果读到了
TC_CLASSDESC
或TC_PROXYCLASSDESC
——调用readClassDesc
函数;
- 如果读到了
TC_STRING
或TC_LONGSTRING
——调用readString
函数;
- 如果读到了
TC_ARRAY
——调用readArray
函数;
- 如果读到了
TC_ENUM
——调用readEnum
函数;
- 如果读到了
TC_OBJECT
——调用readOrdinaryObject
函数;
- 如果读到了
TC_EXCEPTION
——调用readFatalExcception
函数,然后抛出异常;
- 如果读到了
TC_BLOCKDATA
或TC_BLOCKDATALONG
——抛出异常信息,只是Data Block
模式不同则抛出的异常信息不一样,开启Data Block
模式;
- 如果读到了
TC_ENDBLOCKDATA
——抛出异常信息,同上,只是不开启Data Block
模式;
- 其他情况直接抛出异常信息;
在上述过程中,如果遇见了TC_ARRAY
,TC_ENUM
,TC_OBJECT
,TC_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_*
标记信息,通过该标记信息设置对应的成员属性,最后从字节流中读取每一个字段的信息:
这些字段信息包括:TypeCode
、fieldName
、fieldType
:
readNonProxy
这里对应的方法是在序列化时使用的writeNonProxy
方法,在writeNonProxy
中写入的TypeCode
、fieldName
、fieldType
在这里被读取。
读取结束以后会依次跳出readNonProxy
、readClassDescriptor
方法,在获得类信息后会返回readNonProxyDesc
接着走完下面的流程:
如上图中的流程,首先开启Data Block
模式(bin.setBlockDataMode(true)
),然后调用resolveClass
方法处理当前类的信息:
之前我在《序列化流程分析总结》一文中提到:
annotateClass是提供给子类实现的方法,通常默认情况下这个方法什么也不做,与此类似的还有
ObjectInputStream
中的resolveClass
方法。
实际上,ObjectInputStream
中的resolveClass
、resolveProxyClass
、resolveObject
这三个方法对应着ObjectOutputStream
中定义的annotateClass
、annotateProxyClass
和replaceObject
方法,如果ObjectOutputStream
的子类重写了这的三个方法,那么要求ObjectInputStream
的子类也必须重写这三个方法对应的resolve
方法。
在这里,resolveClass
方法会根据字节流中读取的类描述信息加载本地类,加载的时候用到的就是我们平时用的Class.forName()
的方法,实际上反序列化漏洞根本的原因就是在这里加载了Runtime
类,然后执行了exec()
方法。
处理完当前类的信息后,会调用filterCheck
方法进行检测:
如果非空,那么调用序列化筛选器,这个筛选器调用了serialFilter.checkInput
方法检查序列化数据,如果检测出来了异常,那么会令status
为Status.REJECTED
状态,filterCheck
将会根据serialFilter.checkInput
的检查结果来决定是否执行反序列化,如果checkInput()
方法返回Status.REJECTED
,反序列化将会被阻止,并抛出InvalidClassException()
错误:
如果checkInput()
方法返回Status.ALLOWED
,程序将可执行反序列化
在结束了反序列化内容检测后,会调用skipCustomData
方法跳过所有数据块和对象,直到遇到TC_ENDBLOCKDATA
标识
接着,会调用ObjectStreamClass
中的initNonProxy
方法:
在这个方法里会初始化表示非代理类的类描述符:
初始化完毕后会调用handles
的finish
方法完成引用Handle
的赋值操作:
最后将结果赋值给passHandle
成员属性(初始定义为private int passHandle = NULL_HANDLE;
)
readNonProxyDesc
方法结束,将得到的类描述信息赋值给descriptor
变量:
经过validateDescriptor
的验证后将descriptor
作为结果返回给readOrdinaryObject
方法。
经过了这么多方法的层层调用后,拿到了描述类信息,然后和序列化开始时类似,同样检测当前处理的对象是否是一个可反序列化的对象(checkDeserialize()
),如果是,那么就从系统中读取当前Java
对象所属类的描述信息(也叫做类元数据信息)
然后再经过getResolveException
判断有无异常信息,若无,那么会返回obj
对象,然后经过几个简单的判断后会调用handles
的finish
方法完成引用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