前言 · 攻击Java Web应用-[Java Web安全]
Java序列化 - 二进制格式详解_java序列化二进制特征-CSDN博客
1 类加载机制
1.1 概述
Java程序在运行钱需要先编译成.class文件,这个文件的内容被称为字节码,然后调用Java.lang.ClassLoader加载字节码,每一个类都会在方法区保存一份它的元数据,在堆中创建一个与之对应的Class对象。
类的生命周期,经历7个阶段,分别是加载、验证、准备、解析、初始化、使用、卸载。除了使用和卸载两个过程,前面的5个阶段 加载、验证、准备、解析、初始化 的执行过程,就是类的加载过程。验证、准备、解析合成为连接过程。
1.2 类加载过程
1.2.1 加载
加载是类加载过程的第一个阶段,加载过程由类加载器完成,可自定义类加载器。在加载阶段,JVM需要完成以下三件事情:
- 通过一个类的全限定名去找到其对应的.class文件;
- 将这个.class文件内的二进制数据读取出来,转化成方法区的运行时数据结构;
- 在Java堆中生成一个代表这个类的java.lang.Class对象,作为对方法区中这些数据的访问入口
1.2.2 校验
Class文件中的内容是字节码,这些内容可以由任何途径产出,验证阶段的目的是保证文件内容里的字节流符合Java虚拟机规范,且这些内容信息运行后不会危害虚拟机自身的安全。
验证阶段会完成以下校验:
- 文件格式验证:验证字节流是否符合Class文件格式的规范。例如:是否以0xCAFEBABE开头、主次版本号是否在当前虚拟机的处理范围之内、常量池中的常量是否有不被支持的类型 等等;
- 元数据验证:对字节码描述的元数据信息进行语义分析,要符合Java语言规范。例如:是否继承了不允许被继承的类(例如final修饰过的)、类中的字段、方法是否和父类产生矛盾 ...... 等等;
- 字节码验证:对类的方法体进行校验分析,确保这些方法在运行时是合法的、符合逻辑的;
- 符号引用验证:发生在解析阶段,符号引用转为直接引用的时候,例如:确保符号引用的全限定名能找到对应的类、符号引用中的类、字段、方法允许被当前类所访问等。
验证阶段不是必须的,虽然这个阶段非常重要。Java虚拟机允许程序员主动取消这个阶段,用来缩短类加载的时间,可以根据自身需求,使用 -Xverify:none参数来关闭大部分的类验证措施。
1.2.3 准备
准备阶段给类的静态字段(static关键字修饰)分配内存,并设置为默认值(0或null)。
- public static int value1=114514:会给value1静态变量分配内存,并赋值为默认值0;
- public static final int value2 = 114514:会给value2常量分配内存,并赋值为114514;
1.2.4 解析
这个阶段,虚拟机会把这个Class文件中,常量池内的符号引用转换为直接引用。主要解析的是 类或接口、字段、类方法、接口方法、方法类型、方法句柄等符号引用。我们可以把解析阶段中,符号引用转换为直接引用的过程,理解为当前加载的这个类,和它所引用的类,正式进行“连接“的过程。
- 符号引用:Java代码在编译期间,是不知道最终引用的类型,具体指向内存中哪个位置的,这时候会用一个符号引用,来表示具体引用的目标是"谁"。例如使用符号名称(如变量名、函数名、类名)来引用某个实体,而不是直接引用该实体的内存地址或位置。
- 直接引用:直接引用就是可以直接或间接指向目标内存位置的指针或句柄。
1.2.5 初始化
初始化的过程,就是执行类构造器 <clinit>()方法的过程。
- static修饰的变量赋值:准备阶段分配内存,初始化阶段赋值;
- static修饰的代码块:执行该代码块的内容,一般反序列化漏洞可以将恶意代码写入static代码块中。
类构造器方法<clinit> 和 类构造函数<init> 的区别:
- 前者是类构造器方法,每个类在执行完整的加载机制的时候,才会执行;后者是实例构造器,每次new一个类实例的时候都会执行;
- <clinit>() 方法由编译器自动生成,但不是必须生成的,只有这个类存在static修饰的变量,或者类中存在静态代码块但时候,才会自动生成<clinit>()方法。
1.3 类加载器
类加载器ClassLoader就是完成类加载全过程中的加载。Java中有自带的类加载器,开发者也可自定义类加载器。
在JVM中,类加载器最顶层的是Bootstrap ClassLoader(启动类加载器)、Extension ClassLoader(拓展类加载器)、App ClassLoader(系统类加载器)。
-
Bootstrap ClassLoader:负责加载<JAVA_HOME>\lib 目录,或者被 -Xbootclasspath 参数制定的路径,例如 jre/lib/rt.jar 里所有的class文件。由C++实现,不是ClassLoader子类。日常开发没办法获取该加载器,会返回null。
-
Extension ClassLoader:负责加载Java平台中扩展功能的一些jar包,包括<JAVA_HOME>\lib\ext 目录中 或 java.ext.dirs 指定目录下的jar包。由Java代码实现在sun.misc.Launcher中,继承于URLClassLoader。
-
App ClassLoade:我们自己开发的应用程序,就是由它进行加载的,负责加载ClassPath路径下所有jar包。由Java代码实现在sun.misc.Launcher中,继承于URLClassLoader。
下面将利用系统类加载器加载自己写的类。首先,定义一个带有恶意代码的被加载类LoadedClass.java:
package com.example.thymeleafproject.ReflectionAndClassLoader;
import java.io.IOException;
public class LoadedClass {
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
然后创建一个测试类classloadertest.java,直接运行代码发现并没有执行static里面的代码。这是因为loadClass只是完成加载的部分,并没有初始化,static是在初始化时被执行的。
package com.example.thymeleafproject.ReflectionAndClassLoader;
import sun.misc.Launcher.*;
public class classloadertest {
public static void main(String[] args) throws ClassNotFoundException, InstantiationException, IllegalAccessException {
ClassLoader appClassLoader = ClassLoader.getSystemClassLoader();
// System.out.println(appClassLoader);
// // 使用系统类加载器加载指定类
Class<?> myClass = appClassLoader.loadClass("com.example.thymeleafproject.ReflectionAndClassLoader.LoadedClass");
//
// // 创建实例
// Object obj = myClass.newInstance();
}
}
真正起到加载作用的是,类加载器公共父类ClassLoader的protected Class<?> loadClass(String name, boolean resolve):
在其中起作用的父类URLClassLoader的findClass和defineClass。findClass找到该类的位置和字节码,defineClass将其转为Class对象。
在进行类加载的时候ClassLoader中相关的函数:
- loadClass():用于加载指定类
- findClass():用于查找指定的Java类
- defineClass():定义指定的类,将其转为Class对象,一般来说findClass中会调用defineClass
- findLoadedClass():查看加载缓存
- reslveClass():解析和链接指定类
双亲委派模型:如下所引之图。通过内置的parent变量关联,并非继承。
2 反射机制
Java反射(Reflection)是Java非常重要的动态特性,通过使用反射我们不仅可以获取到任何类的成员方法(Methods)、成员变量(Fields)、构造方法(Constructors)等信息,还可以动态创建Java类实例、调用任意的类方法、修改任意的类成员变量值等。反射所提供的灵活性,是Java框架的基础之一,但也打破的Java的访问控制权限,存在安全隐患。
在实验前先定义两个类:reflectTest和reflectedClass。reflectTest是main入口类,reflectedClass类是待实验测试的类,其代码如下:
package com.example.thymeleafproject.ReflectionAndClassLoader;
import java.io.IOException;
public class reflectedClass {
static{
try {
Runtime.getRuntime().exec("calc");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
private int age;
private String name;
public reflectedClass() {
}
public reflectedClass(int age, String name) {
this.age = age;
this.name = name;
}
@Override
public String toString() {
return "reflectedClass{" +
"age=" + age +
", name='" + name + '\'' +
'}';
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
2.1 获取Class对象
Java反射操作的是java.lang.Class对象,所以需要先获得Class对象,然后才是考虑反射操作,一般获取Class对象的方法有:
- ;classLoader.loadClass("类全限定名"):进行类的加载,但没有初始化
- Class.forName("类全限定名"):即加载,默认会初始化;
- 类名.class:是一种编译时的静态绑定,如果是未知类在被加载前、没办法通过这种方式获取。
2.2 反射创建类实例——Constructor
通过反射创建类实例需要先获取构造方法,然后newInstance()。在Java的任何一个类都必须有一个或多个构造方法,如果代码中没有创建构造方法那么在类编译的时候会自动创建一个无参数的构造方法。通过反射来获取的构造方法存放在Constructor类中
package com.example.thymeleafproject.ReflectionAndClassLoader;
import java.lang.reflect.Constructor;
public class reflectTest {
public static void main(String[] args) throws Exception {
// 使用Class.forName: 弹窗,加载并初始化
Class<?> aClass = Class.forName("com.example.thymeleafproject.ReflectionAndClassLoader.reflectedClass");
// System.out.println(aClass);
/* 一、 getConstructor(Class<?> ... parameterType) */
// 1.1 获取public访问权限的无参构造对象
Constructor con1 = aClass.getConstructor(new Class[]{});
System.out.println("getConstructor(new Class[]{}): --- " + con1);
Object o1 = con1.newInstance();
System.out.println("无参构造获取对象:" + o1);
// 1.2 获取public访问权限的有参构造对象
Constructor con2 = aClass.getConstructor(new Class[]{int.class, String.class});
System.out.println("aClass.getConstructor(new Class[]{Integer.class, String.class} : ---- " + con2);
Object o2 = con2.newInstance(new Object[]{25,"tom"});
System.out.println("有参构造获取对象:" + o2);
/* 二、 getConstructors() */
// 2. 获取public访问权限的所有构造方法类
Constructor<?>[] con3 = aClass.getConstructors();
System.out.println("");
System.out.println("aClass.getConstructors(): --- ");
for( Constructor con : con3){
System.out.println(con);
}
System.out.println("");
/* 三、 getDeclaredConstrutor(Class<?> ... parameterType) */
// 获取某个构造方法,既可以是public的,也能是private的,但对于private的需要设置权限setAccessible(true);
Constructor con4 = aClass.getDeclaredConstructor();
System.out.println("getDeclaredConstrutor(Class<?> ... parameterType) : --- " + con4);
con4.setAccessible(true);
Object o4 = con4.newInstance();
System.out.println(o4);
/* 四、 getDeclaredConstrutors() */
// 获取全部构造方法,既可以是public的,也能是private的,但对于private构造方法的需要设置权限setAccessible(true);
Constructor<?>[] con5 = aClass.getDeclaredConstructors();
System.out.println("");
System.out.println("aClass.getDeclaredConstructors(): --- ");
for( Constructor con : con5){
con.setAccessible(true);
System.out.println(con);
}
System.out.println("");
}
}
2.3 反射调用类方法
获取类成员方法以及调用方法
// 获取某个方法
Method method = clazz.getDeclaredMethod("方法名");
Method method = clazz.getDeclaredMethod("方法名", 参数类型如String.class,多个参数用","号隔开);
//获取全部方法
Method[] methods = clazz.getDeclaredMethods()
// 一般方法调用
method.invoke(方法实例对象, 方法参数值,多个参数值用","隔开);
// 静态方法调用
method.invoke(null, 方法参数值,多个参数值用","隔开);
2.4 反射调用成员变量
// 获取类所有成员变量
Field fields = clazz.getDeclaredFields();
// 获取类具体成员变量
Field field = clazz.getDeclaredField("变量名");
// 获取变量值
Object obj = field.get(类实例对象);
// 修改变量值
field.set(类实例对象, 修改后的值);
// private变量需要设置访问权限
field.setAccessible(true)
// final关键词修饰成员常量步骤如下
// 1. 反射获取Field类的modifiers(类修饰符)
Field modifiers = field.getClass().getDeclaredField("modifiers");
// 2. 设置modifiers修改权限
modifiers.setAccessible(true);
// 3. 修改成员变量的Field对象的modifiers值
modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
// 4. 修改成员变量值
field.set(类实例对象, 修改后的值);
2.5 Runtime反射弹窗
Class runtimeClass1 = Class.forName("java.lang.Runtime");
// 获取构造方法
Constructor constructor = runtimeClass1.getDeclaredConstructor();
constructor.setAccessible(true);
// 创建Runtime类示例,等价于 Runtime rt = new Runtime();
Object runtimeInstance = constructor.newInstance();
// 获取Runtime的exec(String cmd)方法
Method runtimeMethod = runtimeClass1.getMethod("exec", String.class);
// 调用exec方法,等价于 rt.exec(calc);
Process process = (Process) runtimeMethod.invoke(runtimeInstance, "calc");
3 序列化与反序列化
3.1 概述
Java 序列化是指把 Java 对象转换为字节序列的过程ObjectOutputStream类的writeObject()方法可以实现序列化。反序列化是指把字节序列恢复为 Java 对象的过程,ObjectInputStream 类的 readObject() 方法用于反序列化。
序列化机制可以将Java对象转换为数据流用来保存在磁盘上或者通过网络传输。这使得对象可以脱离程序独立存在。
3.2 案例
3.2.1 创建类继承接口
创建一个Person类实现Serializable接口。
一个对象只有实现Serializable或者Externalnalizable接口才能序列化。两个接口的内容如下。Externalizable接口定义了writeExternal和readExternal两个方法,分别用于手动指定对象的序列化和反序列化过程。
public interface Serializable {
}
public interface Externalizable extends java.io.Serializable {
void writeExternal(ObjectOutput out) throws IOException;
void readExternal(ObjectInput in) throws IOException, ClassNotFoundException;
}
3.2.2 序列化和反序列化测试
序列化的字符串是以AC ED 00 05开头的,以便JVM识别。
3.3 序列化二进制文件分析
以十六进制的格式打开序列化后的二进制文件(ser.bin),里面的描述数字都可以在ObjectStreamConstants接口中找到。
AC ED 00 05:AC ED是魔法数,00 05是序列化格式的版本号。可在ObjectStreamConstants类中查看。
73 72:73代表接下读取的数据的一个对象,对应ObjectStreamConstants接口中的final static byte TC_OBJECT = (byte)0x73; 72代表对该对象的描述(final static byte TC_OBJECT = (byte)0x73;)。
00 39:序列化类的全限定名为的长度,com.example.thymeleafproject.serialandUnserialTest.Person的长度十六进制就是0x0039,然后接下来的0x39个单元的内容来记录类的全限定名(地址从00000008-00000040)。
7C 4B E1 86 A0 19 98 98:这八个字节单元是用来描述类的serialVersionUID,其在每个类中都是独一无二的,由于Person没有定义该字段,JVM会自动生成该字段并赋值。
02:该字节长度的标志信息代表了 序列化中标识类版本 ; 该数值也是可以在ObjectStreamConstants接口中找到. (final static byte SC_SERIALIZABLE = 0x02;)。
00 02:代表序列化类的字段个数。
49 00 03 61 67 65:49对应ASCII码是 I ,对应int类型字段;00 03代表字段的名称长度;61 67 65是字段的名字age。
4c 00 04 6e 61 6d 65 74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b:4C对应ASCII码是 L,对应Java中对象类型,String是对象;00 04代表该字段名称长度;6E 61 6D 65为改字段变量名name;74 00 12 4c 6a 61 76 61 2f 6c 61 6e 67 2f 53 74 72 69 6e 67 3b则是用来描述该对象类型的全限定名及其长度,但凡涉及到类,都需要描述其全限定名及长度:74是表示String类型,00 12表示后面的长度,其余则对应L/Java/Lang/String.
78 70:78 70是结束标志,如果是超类就没有70,70对应TC_NULL。后面就是描述具体的字段值。
00 00 00 19:int长度占四个字节,其值对应为25。
74 00 03 74 6F 6D:74表示String类型,00 03表示字段值的长度,74 6F 6D为字段值tom。
## ObjectStreamConstants接口中,与二进制流格式相关的常量
final static short STREAM_MAGIC = (short)0xaced;
final static short STREAM_VERSION = 5;
final static byte TC_NULL = (byte)0x70;
final static byte TC_REFERENCE = (byte)0x71;
final static byte TC_CLASSDESC = (byte)0x72;
final static byte TC_OBJECT = (byte)0x73;
final static byte TC_STRING = (byte)0x74;
final static byte TC_ARRAY = (byte)0x75;
final static byte TC_CLASS = (byte)0x76;
final static byte TC_BLOCKDATA = (byte)0x77;
final static byte TC_ENDBLOCKDATA = (byte)0x78;
final static byte TC_RESET = (byte)0x79;
final static byte TC_BLOCKDATALONG = (byte)0x7A;
final static byte TC_EXCEPTION = (byte)0x7B;
final static byte TC_LONGSTRING = (byte) 0x7C;
final static byte TC_PROXYCLASSDESC = (byte) 0x7D;
final static byte TC_ENUM = (byte) 0x7E;
final static int baseWireHandle = 0x7E0000;
## 描述字段类型的编码
ASCII Java类型 十六进制
B byte 42
C char 43
D double 44
F float 46
I int 49
J long 4A
L object 4C
S short 53
Z boolean 5A
[ array 5B
3.4 反序列化流程分析
要了解反序列化的具体流程,需要先弄清楚ObjectInputStream类的初始化流程,然后才是readObject的流程。在1.2的案例中,输入filename参数是"ser.bin"。开始分析(启动!!)。
3.4.1 ObjectInputStream ois = new ObjectInputStream(new FileInputStream("ser.bin"));
主要是创建一些数据读取类、反序列化对象管理类、校验类,以及做校验:魔术数和版本信息是否匹配、类是否允许序列化等。
verifySubclass():来验证子类是否符合要求。该方法的实现通常是空的,它的目的是给子类提供一个钩子(hook),以便在子类中进行自定义的子类验证。子类可以通过重写 verifySubclass() 方法,来添加额外的验证逻辑,以确保反序列化的类符合特定的安全要求。例如,可以验证反序列化的类必须继承特定的父类、实现特定的接口、或满足其他自定义的限制条件()。
bin = new BlockDataInputStream(in);:BlockDataInputStream 是 Java IO 包中的一个类,用于读取块数据格式的输入流。块数据其实就是把数据先放入一个buf里面,然后统一读取,而不是逐个读取,这能减少和磁盘交互的次数,提高效率。in是一开始传入的FileInputSteam,输一个输入流对象,用来和底层系统交互,起到交互作用的是FileDescriptor类。
handles = new HandleTable(10);:用于存储和管理反序列化对象的句柄(handle)以及与之相关的信息。
vlist = new ValidationList();:用于存储验证对象的列表。enableOverride = false;:表示不允许重写。bin.setBlockDataMode(true);:启动块数据模式。
readStreamHeader();:用于检查和验证序列化流的头部信息(验证魔术数和版本,AC ED 00 05)。
3.4.2 Object obj = ois.readObject();
这里的重点其实是:是如何从ois.readObject进入到person.readObject中的。跟进ois.readObject()。
跟进readObject0()主要是bin这个变量起作用,代码有点长,把它分为三块。首先第一块,由于创建ObjectInputStream时bin.setBlockDataMode(true)将bin.blkmode变量设置为true,这里进去if的逻辑,但这里都是抛异常,然后又将bin.blkmode变量设为flase。这里看解析是修复了4360508问题(俺也不懂是啥)。
至于第二块,TC_RESET(ObjectStreamConstants的常量,对应0x79,或121,可见1.3会指示对象流重置读取状态,并从下一个字节开始解析新的对象数据。在1.3对序列化流格式进行分析可知,在这个案例中并没有设置TC_RESET,而是TC_OBJECT(0x73,115)。所以这里也会直接跳过。
第三块代码如下:会进入case TC_OBJECT: return checkResolve(readOrdinaryObject(unshared));中。unshared是传入的参数,固定为false。而readOrdinaryObject(unshared)会真正的执行反序列化流程,并且进入person的readObject90中。
depth++;
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);
}
跟进readOrdinaryObject(boolean unshared)。这部分结合1.3一起食用更好。这里由于类之间调用比较频繁,如果要写就要写很多(又长又臭)。
- ObjectStreamClass desc = readClassDesc(false);
readClassDesc其实就是去读取序列化流的ClassDesc信息,这部分是从(73 - 78 70),在这个过程会将类加载序列化的类,创建相应的class对象、filed对象和Method对象(readObject方法),但没有初始化filed的值。
- readSerialData(obj, desc);——1801行
在前面几行中,会实例化一个序列化的对象obj,在本文中就是Person类。readSerialData(obj, desc);就是给obj赋值,并进入obj的readObject方法。跟进readSerialData():SerialCallbackContext 类是一个辅助类,用于管理序列化回调。它提供了一种机制,允许序列化过程中的回调方法能够访问对象的私有成员和方法(如readObject);
跟进invokeReadObject():反射调用readObjectMethod.invoke()
最终来到自定义的readObject方法:
跟进ois.defaultReadObject();:这里主要是给(Person)obj对象的变量赋值
然后就回到person的readObject方法中,执行Runtime.getRuntime().exec("calc");
3.5 其他知识点
3.5.1 transient关键字
关键字transient用于修饰类的成员变量,表示该变量在对象序列化过程中应该被忽略,不会被保存到持久化存储介质(如文件或数据库)中。
3.5.2 serialVersionUID
serialVersionUID是一个64位的long类型数字,反序列化时,会读取数据流中的serialVersionUID并与当前类的serialVersionUID进行比较。如果两者不一致,就会抛出InvalidClassException,表示版本不匹配,无法进行反序列化操作。
在Person中定义serialVersionUID变量,并用1.5.1的ser.bin反序列化,会报错。
3.5.3 反序列化中的多类问题
3.5.3.1 父子类
重新创建或修改PersonFather.java、Person.java、test.java。
package com.example.thymeleafproject.serialandUnserialTest;
import java.io.Serializable;
public class PersonFather implements Serializable {
protected String testExtend;
public PersonFather() {
}
public PersonFather(String testExtend) {
this.testExtend = testExtend;
}
public String getTestExtend() {
return testExtend;
}
public void setTestExtend(String testExtend) {
this.testExtend = testExtend;
}
}
package com.example.thymeleafproject.serialandUnserialTest;
import java.io.Externalizable;
import java.io.Serializable;
public class Person extends PersonFather implements Serializable {
private String name;
private int age;
// private static final long serialVersionUID = -6849794470754660000L;
public Person() {
}
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
", testExtend='" + testExtend + '\'' +
'}';
}
}
package com.example.thymeleafproject.serialandUnserialTest;
import java.io.*;
public class test {
public static void main(String[] args) throws Exception {
Person tom = new Person("tom", 25);
tom.testExtend = "ok";
System.out.println("序列化前的对象:" + tom);
serialize(tom);
Person person = (Person) unserilize("ser.bin");
System.out.println("反序列化后的对象:" + person);
// binToHex("ser.bin");
// System.out.println("-6849794470754667710L".length());
}
public static void serialize(Object obj) throws IOException{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));
oos.writeObject(obj);
}
public static Object unserilize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(filename));
Object obj = ois.readObject();
return obj;
}
public static void binToHex(String filename){
try (FileInputStream inputStream = new FileInputStream(filename)) {
int byteRead;
while ((byteRead = inputStream.read()) != -1) {
System.out.printf("%02X ", byteRead); // 以十六进制格式打印每个字节
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
下面开始对比试验测试,有几个实验变量:父类是否实现Serializable等接口、父类是否有无参构造函数、父类变量是否用transient关键词修饰,以及子类没有不实现,父类实现接口。
(1)父类实现Serializable等接口、父类有无参构造函数、父类变量不用transient关键词修饰。
(2)父类实现Serializable等接口、父类有无参构造函数、父类变量用transient关键词修饰。
(3)父类实现Serializable等接口、父类没有无参构造函数、父类变量不用transient关键词修饰。
(4)父类实现Serializable等接口、父类没有无参构造函数、父类变量用transient关键词修饰。
(5)父类不实现Serializable等接口、父类有无参构造函数、父类变量不用transient关键词修饰。
(6)父类不实现Serializable等接口、父类有无参构造函数、父类变量用transient关键词修饰。
(7)父类不实现Serializable等接口、父类没有无参构造函数、父类变量不用transient关键词修饰。
(8)父类不实现Serializable等接口、父类没有无参构造函数、父类变量用transient关键词修饰。
(9)子类没有不实现,父类实现接口
总结:
- 父类必须要有无参构造函数,如果一个类不定义构造函数时,JVM会自动给该类创建无参构造函数;
- 如果父类不实现Seriliazbale等接口,父类的字段属性不能被序列化;
- 子类不实现Seriliazbale等接口,但父类实现接口,子类也可序列化和反序列化,因为接口被继承了。
3.5.3.2 类成员字段
新建一个PersonInnerClass.java类,作为Person的内部类。
package com.example.thymeleafproject.serialandUnserialTest;
import java.io.Serializable;
public class PersonInnerClass implements Serializable {
public int number;
public double salary;
public PersonInnerClass() {
}
public PersonInnerClass(int number, double salary) {
this.number = number;
this.salary = salary;
}
public int getNumber() {
return number;
}
public void setNumber(int number) {
this.number = number;
}
public double getSalary() {
return salary;
}
public void setSalary(double salary) {
this.salary = salary;
}
@Override
public String toString() {
return "PersonInnerClass{" +
"number=" + number +
", salary=" + salary +
'}';
}
}
发现只有当内部类也实现Serilizable接口时才能实现序列化和反序列化,否则会报错。
3.5.4 反序列化漏洞
Java广义上的反序列化漏洞很多,本文章只是针对readObject而言。当一个类中自定义readObject了,将该类反序列化的时候,就可以执行类的readObject方法,走其中的逻辑,如果里面有参数可控,并直接或间接调用危险方法,那么就可能存在Java反序列化漏洞。
在这种情况下,readObject方法就是我们的Source点,执行点就是sink点。Source到sink之间的链条称为Gadget。一般针对反序列化漏洞的形式有以下几种:
- 直接会调用危险函数,例如本次实验例子。
- 通过反射实现RCE0(例如CC1等)
- 通过类加载实现RCE(BCEL等)
3.5.5 注意事项总结
- 静态变量不会被序列化,否则会导致覆盖原本的类变量;
- 如果一个对象的成员变量是一个对象,那么这个对象同样需要继承Serilizable等接口,其数据成员才会被保存还原,以递归的方式;
- 父类必须要有无参构造函数,如果一个类不定义构造函数时,JVM会自动给该类创建无参构造函数;
- 如果父类不实现Seriliazbale等接口,父类的字段属性不能被序列化;
- 子类不实现Seriliazbale等接口,但父类实现接口,子类也可序列化和反序列化,因为接口被挤成了;
- 如果可序列化类未显式声明 serialVersionUID,则序列化运行时将基于该类的各个方面计算该类的默认 serialVersionUID 。