0x00 前言
序列化是常见Java漏洞的起源机制,在学习了反射机制之后肯定是要再接着探索下序列化的机制。
0x01 什么是序列化
序列化是指把一个Java对象变成二进制内容,本质上就是一个byte[]数组。
序列化后可以把byte[]保存到文件中,或者把byte[]通过网络传输到远程,这样,就相当于把Java对象存储到文件或者通过网络传输出去了。
有序列化,就有反序列化,即把一个二进制内容(也就是byte[]数组)变回Java对象。有了反序列化,保存到文件中的byte[]数组又可以“变回”Java对象,或者从网络上读取byte[]并把它“变回”Java对象。
0x02 序列化有什么作用
根据原理特点,可以归纳为一下三点:
(1)永久性保存对象,保存对象的字节序列到本地文件或者数据库中;
(2)通过序列化以字节流的形式使对象在网络中进行传递和接收;
(3)通过序列化在进程间传递对象;
具体的一些应用场景:
(A) http参数,cookie,sesion,存储方式可能是base64(rO0),压缩后的base64(H4sl),MII等
(B) Servlets HTTP,Sockets,Session管理器 包含的协议就包括JMX,RMI,JMS,JNDI等(\xac\xed)
(C) xml Xstream,XMLDecoder等(HTTP Body:Content-Type:application/xml)
(D) json(Jackson,fastjson) http请求中包含
0x03 序列化机制的特性
在使用序列化时需要注意以下几个特性:
1、Java对象如果需要进行序列化,必须实现一个特殊的java.io.Serializable接口。{
Serializable接口没有定义任何方法,它是一个空接口。我们把这样的空接口称为“标记接口”(Marker Interface),实现了标记接口的类仅仅是给自身贴了个“标记”,并没有增加任何方法;
}
2、反序列化时,由JVM直接构造出Java对象,不调用构造方法,构造方法内部的代码,在反序列化时根本不可能执行。
3、若可序列化的类的成员不是基本类型,也不是String类型,那这个引用类型也必须是可序列化的(引用的这个类也必须继承Serializable接口)。
4、Java序列化同一对象,并不会将此对象序列化多次得到多个对象。{
Java序列化算法:
① 有保存到磁盘的对象都有一个序列化编码号;
② 当程序试图序列化一个对象时,会先检查此对象是否已经序列化过,只有此对象从未(在此虚拟机)被序列化过,才会将此对象序列化为字节序列输出;
③ 如果此对象已经序列化过,则直接输出编号即可;
}
5、如果序列化一个可变对象(对象内的内容可更改)后,更改了对象内容,再次序列化,并不会再次将此对象转换为字节序列,而只是保存序列化编号。
6、被transient关键字修饰的字段,将不会被序列化。{
使用transient修饰的属性,java序列化时,会忽略掉此字段,所以反序列化出的对象,被transient修饰的属性是默认值。对于引用类型,值是null;基本类型,值是0;boolean类型,值是false;
}
7、如果该类的某个属性标识为static类型的,则该属性不能序列化。
8、若被序列化的类有父类,则存在以下几种情况。{
① 如果该父类已经实现了可序列化接口,则其父类的相应字段及属性的处理和该类相同;
② 如果该类的父类没有实现可序列化接口,则该类的父类所有的字段属性将不会序列化,并且反序列化时会调用父类的默认构造函数来初始化父类的属性,而子类却不调用默认构造函数,而是直接从流中恢复属性的值;
③ 当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;
}
0x04 实现序列化的过程
1、把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
| 文件名:Person.java
package serialize_test;
import java.io.Serializable;
public class Person implements Serializable { private String name; private int age; public Person(String name, int age) { System.out.println("若出现该提示,则说明构造函数被执行了"); this.name = name; this.age = age; }
@Override public String toString() { return "Person{" + "name='" + name + '\'' + ", age=" + age + '}'; } }
|
② 创建一个ObjectOutputStream输出流;并调用ObjectOutputStream对象的writeObject输出可序列化对象到本地文件中。
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
| 文件名:WriteObject.java
package serialize_test;
import java.io.FileOutputStream; import java.io.ObjectOutputStream;
public class WriteObject { public static void main(String[] args) throws Exception { try ( ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("object.txt"))) { Person person = new Person("shiyan", 1); oos.writeObject(person); oos.flush(); oos.close(); } catch (Exception e) { e.printStackTrace(); }finally{ System.out.println("文件已被序列化写入本地文件中"); } } }
|
③ 控制台输出结果:
1 2
| 反序列化,你调用我了吗? 文件已被序列化写入本地文件中
|
此时文件已被成功写入本地文件中,我们打开看一下文件内容。
2、反序列化读取java对象内容
① 创建一个ObjectInputStream输入流,然后调用ObjectInputStream对象的readObject()得到序列化的对象。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| package serialize_test;
import java.io.FileInputStream; import java.io.ObjectInputStream;
public class WriteObject { public static void main(String[] args) throws Exception { ObjectInputStream ois = new ObjectInputStream(new FileInputStream("object.txt")); Person brady = (Person) ois.readObject(); System.out.println(brady); } }
|
② 控制台输出结果:
1
| Person{name='shiyan', age=1}
|
注:从这里可以看出来在反序列化的时候并不会被执行构造函数的代码。
0x05 自定义序列化过程
该章节一共有三个部分,分别是:常规自定义、另一种自定义、强制自定义。
1. 常规自定义
java提供了可选的自定义序列化。可以进行控制序列化的方式,或者对序列化数据进行其它操作等。
1 2 3 4
| private void writeObject(java.io.ObjectOutputStream out) throws IOException; private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException; private void readObjectNoData() throws ObjectStreamException;
|
通过重写writeObject与readObject方法,可以自己选择哪些属性需要序列化, 哪些属性不需要。如果writeObject使用某种规则序列化,则相应的readObject需要相反的规则反序列化,以便能正确反序列化出对象。
1、创建一个可被序列化的类,并且重写其中的writeObject与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
| 文件名:OverrideClass.java
package serialize_test;
import java.io.IOException; import java.io.Serializable;
public class OverrideClass implements Serializable { public String name;
public OverrideClass(){ }
public String getName() { return name; }
public void setName(String name) { this.name = name; } private void readObject(java.io.ObjectInputStream in) throws IOException,ClassNotFoundException{ in.defaultReadObject(); Runtime.getRuntime().exec("calc"); System.out.println("重写readObject函数"); } private void writeObject(java.io.ObjectOutputStream out) throws IOException{ out.defaultWriteObject(); System.out.println("重写writeObject函数"); } }
|
2、对该类进行序列化写入文件,并再反序列化读取内容。
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
| 文件名:ReaderClass.java
package serialize_test;
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream;
public class ReaderClass {
public static void main(String[] args) throws Exception { ObjectOutputStream Ar1 = new ObjectOutputStream(new FileOutputStream("shiyan.txt")); OverrideClass Ar2 = new OverrideClass(); Ar2.setName("shiyan"); Ar1.writeObject(Ar2); Ar1.flush(); Ar1.close(); System.out.println("---------分割线-------------"); ObjectInputStream Br1 = new ObjectInputStream(new FileInputStream("shiyan.txt")); OverrideClass Br2 = (OverrideClass) Br1.readObject(); System.out.println(Br2.getName()); }
}
|
3、控制台输出结果:
1 2 3 4
| 重写writeObject函数 ---------分割线------------- 重写readObject函数 shiyan
|
4、效果图:
2. 另一种自定义
除了上述常规的自定义还有一种自定义方法:
1 2
| ANY-ACCESS-MODIFIER Object writeReplace() throws ObjectStreamException; ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException;
|
writeReplace:在序列化时,会先调用此方法,再调用writeObject方法。此方法可将任意对象代替目标序列化对象。
readResolve:反序列化时替换反序列化出的对象,反序列化出来的对象被立即丢弃。此方法在readeObject后调用。
注意:readResolve与writeReplace的访问修饰符可以是private、protected、public,如果父类重写了这两个方法,子类都需要根据自身需求重写,这显然不是一个好的设计。通常建议对于final修饰的类重写readResolve方法没有问题;否则,重写readResolve使用private修饰。
1、创建一个可被序列化的类,并且重写其中的readResolve与writeReplace方法。
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
| 文件名:OverrideClass1.java
package serialize_test;
import java.io.IOException; import java.io.ObjectStreamException; import java.io.Serializable;
public class OverrideClass1 implements Serializable { public String name;
public OverrideClass1(){ }
public String getName() { return name; }
public void setName(String name) { this.name = name; } private Object readResolve() throws ObjectStreamException,IOException{ Runtime.getRuntime().exec("calc"); System.out.println("重写readResolve函数"); OverrideClass1 Dr1 = new OverrideClass1(); String Dr2 = "读取的本地序列化对象,已经被替换为自定义的Dr1了"; Dr1.setName(Dr2); return Dr1; } private Object writeReplace() throws ObjectStreamException{ OverrideClass1 Cr1 = new OverrideClass1(); Cr1.name = "原始将被写入的对象已经变成自定义的Cr1了"; System.out.println("重写writeReplace函数"); return Cr1; } }
|
2、对该类进行序列化写入文件,并再反序列化读取内容;这里需要注意的是因为readResolve与writeReplace方法的是相冲突的,在实际运行时我们要注释一掉1个,才能测试出具体的变化。
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
| 文件名:ReaderClass1.java
package serialize_test;
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream;
public class ReaderClass1 {
public static void main(String[] args) throws Exception { ObjectOutputStream Ar1 = new ObjectOutputStream(new FileOutputStream("shiyan1.txt")); OverrideClass1 Ar2 = new OverrideClass1(); Ar2.setName("shiyan"); Ar1.writeObject(Ar2); Ar1.flush(); Ar1.close(); System.out.println("---------分割线-------------"); ObjectInputStream Br1 = new ObjectInputStream(new FileInputStream("shiyan1.txt")); OverrideClass1 Br2 = (OverrideClass1) Br1.readObject(); System.out.println(Br2.getName()); }
}
|
3、控制台输出结果:
注释掉 writeReplace() 的运行结果:
1 2 3
| ---------分割线------------- 重写readResolve函数 读取的本地序列化对象,已经被替换为自定义的Dr1了
|
效果图:
注释掉 readResolve() 的运行结果:
1 2 3
| 重写writeReplace函数 ---------分割线------------- 原始将被写入的对象已经变成自定义的Cr1了
|
3. 强制自定义
通过实现Externalizable接口,必须实现writeExternal、readExternal方法。
1 2 3 4 5 6
| 文件名(接口文档):Externalizable.class
public interface Externalizable extends java.io.Serializable { void writeExternal(ObjectOutput out) throws IOException; void readExternal(ObjectInput in) throws IOException, ClassNotFoundException; }
|
注意:Externalizable接口不同于Serializable接口,实现此接口必须实现接口中的两个方法实现自定义序列化,这是强制性的;特别之处是必须提供pulic的无参构造器,因为在反序列化的时候需要反射创建对象。其次使用Externalizable时,必须按照写入时的确切顺序读取所有字段状态。否则会产生异常。例如,如果更改继承该Externalizable的Demo类中的name和age属性的读取顺序,则将抛出java.io.EOFException。而Serializable接口没有这个要求。
1、创建一个可被序列化的类,并且重写其中的readExternal与writeExternal方法。
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
| 文件名:OverrideClass2.java
package serialize_test;
import java.io.Externalizable; import java.io.IOException; import java.io.ObjectInput; import java.io.ObjectOutput;
public class OverrideClass2 implements Externalizable { public String name;
public OverrideClass2(){ }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public void writeExternal(ObjectOutput out) throws IOException{ out.writeObject(name); System.out.println("----执行 writeExternal 方法------"); } public void readExternal(ObjectInput in) throws IOException, ClassNotFoundException{ this.name=(String)in.readObject(); System.out.println("----执行 readExternal 方法------"); } }
|
2、对该类进行序列化写入文件,并再反序列化读取内容。
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
| 文件名:ReaderClass2.java
package serialize_test;
import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream;
public class ReaderClass2 {
public static void main(String[] args) throws Exception { ObjectOutputStream Ar1 = new ObjectOutputStream(new FileOutputStream("shiyan2.txt")); OverrideClass2 Ar2 = new OverrideClass2(); Ar2.setName("shiyan"); Ar1.writeObject(Ar2); Ar1.flush(); Ar1.close(); System.out.println("---------分割线-------------"); ObjectInputStream Br1 = new ObjectInputStream(new FileInputStream("shiyan2.txt")); OverrideClass2 Br2 = (OverrideClass2) Br1.readObject(); System.out.println(Br2.getName()); }
}
|
3、控制台输出结果:
1 2 3 4
| ----执行 writeExternal 方法------ ---------分割线------------- ----执行 readExternal 方法------ shiyan
|
0x06 什么是 serialVersionUID
serialVersionUID适用于java序列化机制。简单来说,JAVA序列化的机制是通过判断类的serialVersionUID来验证的版本一致的。在进行反序列化时,JVM会把传来的字节流中的serialVersionUID于本地相应实体类的serialVersionUID进行比较。如果相同说明是一致的,可以进行反序列化,否则会出现反序列化版本一致的异常,即是InvalidCastException。
具体的序列化过程:序列化操作的时候系统会把当前类的serialVersionUID写入到序列化文件中,当反序列化时系统会去检测文件中的serialVersionUID,判断它是否与当前类的serialVersionUID一致,如果一致就说明序列化类的版本与当前类版本是一样的,可以反序列化成功,否则失败。
serialVersionUID有两种显示的生成方式 {
一是默认的1L,比如:private static final long serialVersionUID = 1L;
二是根据类名、接口名、成员方法及属性等来生成一个64位的哈希字段,比如:
private static final long serialVersionUID = xxxxL;
}
0x07 反序列攻击时序图
序列化的机制到这里已经了解的差不多了,下面贴一张廖新喜大佬在某次安全大会上的反序列攻击时序图,做为一个漏洞攻击了解。
0x08 参考文章
[1] https://www.cnblogs.com/9dragon/p/10901448.html
[2] https://www.liaoxuefeng.com/wiki/1252599548343744/1298366845681698
[3] http://xxlegend.com/2018/06/20/%E5%85%88%E7%9F%A5%E8%AE%AE%E9%A2%98%20Java%E5%8F%8D%E5%BA%8F%E5%88%97%E5%8C%96%E5%AE%9E%E6%88%98%20%E8%A7%A3%E8%AF%BB/
[4] http://code2sec.com/javafan-xu-lie-hua-lou-dong-xue-xi-shi-jian-yi-cong-serializbalejie-kou-kai-shi-xian-dan-ge-ji-suan-qi.html