0x00 前言 推荐先看一下笔者的上一篇文章:Java反序列化原理篇01-原生序列化流程分析 。
0x01 demo 给Person类写一个readObject方法,先注释该方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 import java.io.ObjectInputStream;import java.io.Serializable;public class Person implements Serializable { public String name; private int age; private transient String hobby = "Handsome" ; public Person () {} public Person (String name, int age) { this .name = name; this .age = age; } }
主类Main:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 import java.io.ByteArrayInputStream;import java.io.ByteArrayOutputStream;import java.io.ObjectInputStream;import java.io.ObjectOutputStream;public class Main { public static void main (String[] args) throws Exception{ Person person = new Person ("Pax" , 666 ); byte [] serialize = serialize(person); Person deserialize = (Person) deserialize(serialize); } public static <T> byte [] serialize(T o) throws Exception { ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream (); ObjectOutputStream out = new ObjectOutputStream (byteArrayOutputStream); out.writeObject(o); return byteArrayOutputStream.toByteArray(); } public static <T> T deserialize (byte [] ser) throws Exception { ObjectInputStream in = new ObjectInputStream (new ByteArrayInputStream (ser)); return (T) in.readObject(); } }
0x02 创建对象输入流 断点打在如下位置:
跟进,该构造方法与对象输出流的构造方法类似:
属性名
类型
作用
bin
ObjectInputStream.BlockDataInputStream
与bout类似,存放反序列化的对象数据
handles
HandleTable
一个哈希表,表示从对象到引用的映射
vlist
ObjectInputStream.ValidationList
提供一个callback操作的验证集合
serialFilter
ObjectInputFilter
获取序列化过滤器
enableOverride
boolean
决定在反序列化时选用readObjectOverride方法还是readObject方法
0x03 readObject方法 流程到这里:
先是enableOverride的判断。然后创建反序列化深度句柄outerHandle,用于在处理嵌套对象时,能够跟踪外层对象的句柄,以便正确地进行反序列化和管理对象之间的关系。这样可以确保在读取嵌套结构时,不会混淆不同层级的对象。
跟进readObject0方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 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) { 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(); 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); } }
流程走到while循环,该循环跳过所有的 TC_RESET 标记,并调用 handleReset 方法来处理重置操作。循环结束后,tc 变量保存了第一个非 TC_RESET 的字节
depth 是一个计数器,用于跟踪当前反序列化调用的深度。
接着就是一个switch分支,根据tc的值不同,调用对应的处理方法。重点看看tc = TC_OBJECT的逻辑:
先进入readOrdinaryObject方法。首先会再次判断读到的标识是不是TC_OBJECT,如果不是,那么直接抛出InternalError错误:
紧接着:
1 ObjectStreamClass desc = readClassDesc(false );
跟进。该方法正如快速文档所说:
读取并返回一个类描述符(可能为 null)。同时,将 passHandle 设置为这个类描述符的句柄。如果这个类描述符无法在本地虚拟机中找到对应的类,就会抛出一个 ClassNotFoundException。
由于Person类是一个常规的Class,走到readNonProxyDesc方法,开头就是一个标识符判断:
然后判断读取模式是什么,如果是unshared,那么从handles对象的映射中读取一个新的desc,如果不是unshared,那么从unsharedMarker中读取对应的对象。
然后进入readClassDescriptor方法:
该方法的返回值类型ObjectStreamClass,或者说desc,实际上就与Person.class有关,分装了Person类的相关信息。
跟进readNonProxy方法,获取类名,序列化版本号,是否为代理:
接着,从字节流中读取每一个字段的信息。这些字段信息包括:TypeCode、fieldName、fieldType。
读取结束以后会依次跳出readNonProxy、readClassDescriptor方法,在获得类信息后会返回readNonProxyDesc,接着走:
关注这个判断:
1 if ((cl = resolveClass(readDesc)) == null )
这里要拓展一下resolveClass方法和annotateClass方法:
annotateClass是提供给子类实现的方法,通常默认情况下这个方法什么也不做,与此类似的还有ObjectInputStream中的resolveClass方法。
实际上,ObjectInputStream中的resolveClass、resolveProxyClass、resolveObject这三个方法对应着ObjectOutputStream中定义的annotateClass、annotateProxyClass和replaceObject方法,如果ObjectOutputStream的子类重写了这的三个方法,那么要求ObjectInputStream的子类也必须重写这三个方法对应的resolve方法。
在这里,resolveClass方法会根据字节流中读取的类描述信息加载本地类,加载的时候用到的就是我们平时用的Class.forName()的方法,实际上反序列化漏洞根本的原因就是在这里加载了Runtime类,然后执行了exec()方法。
接着调用序列化筛选器:
接着,会调用ObjectStreamClass中的initNonProxy方法:
1 desc.initNonProxy(readDesc, cl, resolveEx, readClassDesc(false ));
初始化完毕后会调用handles的finish方法完成引用Handle的赋值操作:
1 handles.finish(descHandle);
终于在经过了这么多方法的层层调用后,拿到了描述类信息desc。然后和序列化开始时类似,同样检测当前处理的对象是否是一个可反序列化的对象(checkDeserialize()),如果是,那么就从系统中读取当前Java对象所属类的描述信息(也叫做类元数据信息)。
接着初始化一个Person类对象:
然后经过几个简单的判断后会调用handles的finish方法完成引用Handle的赋值操作,最后将结果赋值给passHandle成员属性。
下面就是反序列化对属性赋值的操作:
跟进,如果反序列化对象的类存在readObject方法,则反射调用该方法,否则调用默认的defaultReadFields方法:
defaultReadFields方法的赋值过程与序列化对应的赋值过程类似,不叙:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 private void defaultReadFields (Object obj, ObjectStreamClass desc) throws IOException { Class<?> cl = desc.forClass(); if (cl != null && obj != null && !cl.isInstance(obj)) { throw new ClassCastException (); } int primDataSize = desc.getPrimDataSize(); if (primVals == null || primVals.length < primDataSize) { primVals = new byte [primDataSize]; } bin.readFully(primVals, 0 , primDataSize, false ); if (obj != null ) { desc.setPrimFieldValues(obj, primVals); } int objHandle = passHandle; ObjectStreamField[] fields = desc.getFields(false ); Object[] objVals = new Object [desc.getNumObjFields()]; int numPrimFields = fields.length - objVals.length; for (int i = 0 ; i < objVals.length; i++) { ObjectStreamField f = fields[numPrimFields + i]; objVals[i] = readObject0(f.isUnshared()); if (f.getField() != null ) { handles.markDependency(objHandle, passHandle); } } if (obj != null ) { desc.setObjFieldValues(obj, objVals); } passHandle = objHandle; }
最后调用vlist成员的doCallbacks来执行完成过后的回调逻辑,然后结束所有的序列化流程。
至此,结束。
0x04 参考 Java反序列化流程总结