前言
写这篇文章目的主要在于进一步理解何为java原生反序列化,并且回答的几个问题。
- 为什么就java反序列化使用而言是反序列化类的readObject开始?
- 为什么resolveClass方法可以防御反序列化?
- 为什么在反序列化数据后面插入脏数据会不会影响反序列化?
ps: 碍于水平有限只会在代码表层做宏观和中观的分析,并不会深入特别底层。
测试代码
序列化
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| import java.io.*;
class User implements Serializable { int age = 20; int money = 100; String firstname = "cl0und"; String lastname = "lisan";
User(){ System.out.println("无参数构造方法"); } }
public class Serial { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oops = new ObjectOutputStream(new FileOutputStream("./serializable.txt")); oops.writeObject(new User()); } }
|
反序列化
1 2 3 4 5 6 7 8 9 10 11 12
| import java.io.FileInputStream; import java.io.IOException; import java.io.ObjectInputStream;
public class Unserial { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectInputStream oips = new ObjectInputStream(new FileInputStream("./serializable.txt")); User user = (User) oips.readObject(); System.out.println(user.age); } }
|
序列化
预处理
预处理的意义是拿到类的描述符类ObjectStreamClass,后续对类的内省和写入序列化数据都由他完成或调度。
序列化数据
可以看到一共有三步,首先是TC_OBJECT,其次是类的元信息,最后是类的具体数据。
第一步就写个标志头,比较简单我们下面直接从第二步开始看。
writeClassDesc
在序列化对象的数据之前,首先会有一个对象类型的分类判断,这里分成四种类型来处理即null类型(TC_NULL)、handle类型(TC_REFERENCE)、代理类型(TC_PROXYCLASSDESC)、普通类型(TC_CLASSDESC)。我们这里的User是普通的类所以走最后的writeNonProxyDesc。
writeNonProxyDesc会先调用writeClassDescriptor把自己的属性信息写入,然后递归调用writeClassDesc写入父类元信息。
细看一下writeClassDescriptor,这里面会调用writeNonProxy。
最后调用writeNonProxy,在for循环里面写入属性信息。
这一段的写入如果最后用dump出来,就是下面这个样子。
writeSerialData
把类属性信息写去之后,接下来就是写类属性对应的具体值了。
ps:非原生数据类型指的是字符串、数组、枚举类型、对象。
_
一个完整的dump如下。
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
| STREAM_MAGIC - 0xac ed STREAM_VERSION - 0x00 05 Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 4 - 0x00 04 Value - User - 0x55736572 serialVersionUID - 0xbb 67 4f fb 9b a9 d6 bb newHandle 0x00 7e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 4 - 0x00 04 Fields 0: Int - I - 0x49 fieldName Length - 3 - 0x00 03 Value - age - 0x616765 1: Int - I - 0x49 fieldName Length - 5 - 0x00 05 Value - money - 0x6d6f6e6579 2: Object - L - 0x4c fieldName Length - 9 - 0x00 09 Value - firstname - 0x66697273746e616d65 className1 TC_STRING - 0x74 newHandle 0x00 7e 00 01 Length - 18 - 0x00 12 Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b 3: Object - L - 0x4c fieldName Length - 8 - 0x00 08 Value - lastname - 0x6c6173746e616d65 className1 TC_REFERENCE - 0x71 Handle - 8257537 - 0x00 7e 00 01 classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 02 classdata User values age (int)20 - 0x00 00 00 14 money (int)100 - 0x00 00 00 64 firstname (object) TC_STRING - 0x74 newHandle 0x00 7e 00 03 Length - 6 - 0x00 06 Value - cl0und - 0x636c30756e64 lastname (object) TC_STRING - 0x74 newHandle 0x00 7e 00 04 Length - 5 - 0x00 05 Value - lisan - 0x6c6973616e
|
可以看到在序列化的时候世界上是写入了各字段长度的,所以在后面反序列化读的时候是按照字段长度来进行读取的。这也解释了为什么在反序列化数据后面插入脏数据会不会影响反序列化。
反序列化
预处理
反序列化和序列化互为称操作,其主要的操作在readObject0里面
case到object进行反序列化,整个object的反序化在readOrdinaryObject中
和序列化对称还是分两步,第一步是读取出类的元信息(readClassDesc),第二步是读取出对象属性具体的数据(readSerialData)。
反序列化数据
readClassDesc
在序列化那里提到过,对把类类型分成四个类型处理,这里的User是非代理普通类,就走readNonProxyDesc。
readNonProxyDesc中主要的操作在readClassDescriptor、resolveClass、initNonProxy(注意这里initNonProxy的最后一个传参,这里递归调用了readClassDesc)。
readClassDescriptor读出了类的元数据并把每一个属性名抽象成了ObjectStreamField类
resolveClass对类类型进行了构建,注意这里第二参数为false,意味着并不会执行类的初始化(static代码块不执行)
。
另外注意这里解释了为什么设置反序列化防御的点是在resolveClass,如果想对反序列化设置防御,就需要自己实现一个ObjectInputStream。
1 2 3 4 5 6 7 8 9 10
| class MyObjInputStream extends ObjInputStream { public MyObjInputStream(InputStream in) throws IOException{ super(in); }
protected Class<?> resolveClass(ObjInputStream desc) throws IOException, ClassNotFoundException { System.out.println("defence"); return super.resolveClass(desc) } }
|
initNonProxy又构建了类的元信息。
最后desc(ObjectStreamClass)传出给外层readOrdinaryObject。首先会进行对象的初始化,然后读出数据注入类中。
readSerialData
如果我们自定义了readObject方法,那么这里就会invoke,这里解释了为什么就java反序列化使用而言是反序列化类的readObject开始。
但是在User类中在我们并没有自定义反序列化方法随意还是走默认路线。
defaultReadFields,中第一个循环把原生类型数据赋给obj,第二个循环把数组、枚举类型、对象类型赋给obj。
补充一张整个反序列化的时序图。
从SerialKiller来java反序列化防御
https://github.com/ikkisoft/SerialKiller
原生使用
1 2
| ObjectInputStream ois = new ObjectInputStream(is); String msg = (String) ois.readObject();
|
加上SerialKiller后的使用
1 2
| ObjectInputStream ois = new SerialKiller(is, "/etc/serialkiller.conf"); String msg = (String) ois.readObject();
|
可以看到它的原理就是上面提的自己实现流并且在resolveClass处加上白名单或者黑名单的过滤。
杂
当父子接口同时实现了Serializable接口时
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| class UserBase implements Serializable{ public int age; UserBase(){ System.out.println("UserBase无参构造"); }
UserBase(int age){ this.age = age; System.out.println("UserBase有参构造"); } }
class User2 extends UserBase implements Serializable{
User2(){ System.out.println("User2无参构造"); }
User2(int age){ this.age = age; System.out.println("User2有参构造"); } }
|
控制台输出
1 2 3 4
| UserBase无参构造 User2有参构造 ----------------- 2333
|
当只有子类实现Serializable接口父类没有实现时。
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
| import java.io.*;
public class SerialAndUnserial { public static void main(String[] args) throws IOException, ClassNotFoundException { ObjectOutputStream oops = new ObjectOutputStream(new FileOutputStream("./serializable.txt")); oops.writeObject(new User2(2333));
System.out.println("-----------------"); ObjectInputStream oips = new ObjectInputStream(new FileInputStream("./serializable.txt")); User2 user = (User2) oips.readObject();
System.out.println(user.age); } }
class UserBase { public int age; UserBase(){ System.out.println("UserBase无参构造"); }
UserBase(int age){ this.age = age; System.out.println("UserBase有参构造"); } }
class User2 extends UserBase implements Serializable{
User2(){ System.out.println("User2无参构造"); }
User2(int age){ this.age = age; System.out.println("User2有参构造"); } }
|
1 2 3 4 5
| UserBase无参构造 User2有参构造 ----------------- UserBase无参构造 0
|
总结及一些tip
- 序列化时,当写入类的元数据的时候,是先写子类的类元数据,然后递归调用的写入父类的类元数据(只有实现序列化接口的才会有类元数据)。
- 防御反序列化的原理是在resolveClass处设防而不是readResolve
- 在序列化数据末尾加入脏数据不会影响正常的反序列化。
参考
java中的序列化与反序列化及其源码分析(特别详细)
先知大会议题Java反序列化实战
[从WebLogic看反序列化漏洞的利用与防御](