Java反序列化基础

Java反序列化基础

Lv1

反射机制

获取Class对象

  1. forName方法
    用法:Class.forName(String className)

  2. 类名.class

    用法:className.class

  3. getClass方法
    用法:Object.getClass()

    其中Object应为上下文中已经实例化的类的对象。

  4. classLoader.loadClass方法

    用法:classLoader.loadClass(className)

    此处需要注意的是获取数组类型的Class对象需要使用Java类型的描述符方式,例如:

    1
    2
    Class<?> doubleArray = Class.forName("[D");						//相当于double[].class
    Class<?> cStringArray = Class.forName("[[Ljava.lang.String;"); // 相当于String[][].class

补充-获取内部类时需要将.替换为$,如com.security.Test1$Test2

创建类实例

获取类的构造函数

在Java的任何一个类都必须有一个或多个构造方法,如果代码中没有创建构造方法,那么在类编译的时候会自动创建一个无参数的构造方法。

  1. Class<?>.getConstructor

    用法:Contstuctor c = Class<?>.getConstructor(参数列表类型)

    获取类中Public属性的指定构造方法。例如:

    1
    2
    3
    Class clazz = Class.forName("java.lang.ProcessBuilder");
    // 指定获取参数列表为String[].class的构造方法
    clazz.getConstructor(String[].class);
  2. Class<?>.getDeclaredConstructor

    用法:Contstuctor c = Class<?>.getDeclaredConstructor(参数列表类型)

    获取类中“声明”的指定构造方法,包括私有构造方法

    需要注意的是,当调用私有构造方法的时候需要使用setAccessible来修改其作用域,例如:

    1
    2
    3
    4
    5
    Class clazz = Class.forName("java.lang.Runtime");
    // 获取Runtime的私有无参构造方法
    Constructor c = clazz.getDeclaredConstructor();
    // 由于是私有构造方法,需要使用setAccessible修改其作用域
    c.setAccessible(true);
  3. Class<?>.getConstructors

    用法:Contstuctor[] c = Class<?>.getDeclaredConstructors()

    获取类中所有的公有构造函数,返回一个构造函数列表。

  4. Class<?>.getDeclaredConstructors

    用法:Contstuctor[] c = Class<?>.getDeclaredConstructors()

    获取类中所有的构造函数,返回一个构造函数列表。

创建类的实例化对象

  1. newInstance方法
    用法:Class<?>.newInstance(),返回Object类型,即返回Class<?>的一个对象
    实质上这个方法的作用是调用这个类的无参构造函数,当这个类没有无参构造方法或者无参构造方法为私有属性的时候,无法直接通过newInstance来获得这个类的对象,这种情况下就需要先获取到指定的构造函数后再调用newInstance方法来获取实例化对象。例如以下三个例子:

    • 无参构造方法
    1
    2
    Class clazz = Class.forName("com.security.example");
    Object obj = clazz.newInstance()
    • 指定参数列表类型构造方法
    1
    2
    3
    4
    5
    Class clazz = Class.forName("java.lang.ProcessBuilder");
    // 获取ProcessBuilder类中参数列表类型为List的构造方法
    Constructor c = clazz.getConstructor(List.class);
    // 调用ProcessBuilder类中参数列表类型为List的构造方法获取ProcessBuilder类实例
    Object obj = c.newInstance(Arrays.asList("calc.exe"));
    • 私有无参构造函数
    1
    2
    3
    4
    5
    6
    7
    Class clazz = Class.forName("java.lang.Runtime");
    // 获取Runtime的私有无参构造方法
    Constructor c = clazz.getDeclaredConstructor();
    // 由于是私有构造方法,需要使用setAccessible修改其作用域
    c.setAccessible(true);
    // 调用Runtime类私有无参构造方法获取Runtime类实例
    Object obj = c.newInstance()

获取/调用类方法

获取类中函数

  1. getMethod方法
    用法:Method m = Class<?>.getMethod(String name, class<?>...parameterType)
    第一个参数代表方法名,第二个参数表示该方法参数类型(可缺省),不过Java中支持类的重载,一个同名函数往往会由于参数列表的不同而具有不同的效果,因而无法仅通过一个函数名来确定一个函数,因此第二个参数也会用来获取指定的一个方法。此方法仅能获取到Public属性或从父类继承的方法。

  2. getDeclaredMethod方法

    用法:Method m = Class<?>.getDeclaredMethod(String name, class<?>...parameterType)

    该方法与getDeclaredConstructor方法类似,可以获取到私有属性的方法,在使用invoke调用私有方法前同样需要使用setAccessible方法修改其作用域。

  3. getMethods方法

    用法:Method[] m = Class<?>.getMethods()

    获得该类所有公有方法,返回一个Method[]类型的方法列表。

  4. getDeclaredMethods方法

    用法:Method[] m = Class<?>.getDeclaredMethods()

    获得该类所有方法,返回一个Method[]类型的方法列表。。

调用类中函数

  1. invoke方法
    用法:Method.invoke(Object obj, Object ... args)

    这个方法一般要与getMethod结合使用,一般是先用前者获取到一个Method类,再用invoke调用。

    当调用的是一个普通方法时,第一个参数代表类的一个对象

    当调用的是一个静态方法时,第一个参数代表,第二个参数代表方法的参数

    可以这么理解:正常执行一个方法时,我们通过A.method(B,C,D,...)来执行;在反射中,我们则是通过method.invoke(A,B,C,D,...)去执行,普通方法的A需要时该类的一个对象,而静态方法的A则是类。

    例如:

    • 普通方法(以ProcessBuilder.start为例):

      1
      2
      3
      4
      5
      6
      7
      8
      9
      Class clazz = Class.forName("java.lang.ProcessBuilder");
      // 获取ProcessBuilder类中参数列表类型为List的构造方法
      Constructor c = clazz.getConstructor(List.class);
      // 获取ProcessBuilder类实例
      Object obj = c.newInstance(Arrays.asList("calc.exe"));
      // 获取ProcessBuilder类start方法
      Method myStart = clazz.getMethod("start");
      // 调用ProcessBuilder类中的start方法
      myStart.invoke(obj);
    • 静态方法(以Runtime.getRuntime为例):

      1
      2
      3
      4
      5
      Class clazz = Class.forName("java.lang.Runtime");
      // 获取Runtime类getRuntime方法
      Method myGetRuntime = clazz.getMethod("getRuntime");
      // 调用Runtime类getRuntime方法
      myGetRuntime.invoke(clazz);

获取/设置成员变量

获取成员变量

  1. getField方法

    用法:Field field = clazz.getField("变量名");

    获取当前类指定的公有或从父类继承的成员变量。

  2. getDeclaredField方法

    用法:Field field = clazz.getDeclaredField("变量名");

    获取当前类指定的成员变量。

  3. getFields方法

    用法:Field[] fields = clazz.getFields();

    获取当前类所有公有或从父类继承的成员变量。

  4. getDeclaredFields方法

    用法:Field[] fields = clazz.getDeclaredFields();

    获取当前类所有的成员变量。

获取成员变量的值

  1. get方法

    用法:Object obj = field.get(类实例对象);

    获取成员变量值。

设置成员变量的值

  1. set方法

    用法:field.set(类实例对象, 修改后的值);

    修改成员变量值。

    • 如果想要修改非公有属性的成员变量值,则需要在修改前使用: field.setAccessible(true)的方式修改为访问成员变量访问权限。

    • 如果需要修改被final关键字修饰的成员变量,那么需要先修改方法:

      1
      2
      3
      4
      5
      6
      7
      8
      // 反射获取Field类的modifiers
      Field modifiers = field.getClass().getDeclaredField("modifiers");
      // 设置modifiers修改权限
      modifiers.setAccessible(true);
      // 修改成员变量的Field对象的modifiers值
      modifiers.setInt(field, field.getModifiers() & ~Modifier.FINAL);
      // 修改成员变量值
      field.set(类实例对象, 修改后的值);

例子:Runtime类

Runtime类是我们在Java安全中较为常见用于实现RCE的一个类,那么如何通过这个类来执行命令呢?刚才提到过java.lang.Runtime类的无参构造方法是私有的,我们无法通过newInstance来获取Runtime类的对象。
之所以要将类的某个构造方法设置为私有的,这是由于单例模式的开发模式,通俗地来解释就是在某个类的构造函数只需要实例化调用一次时会如此设计,后续如果需要调用这个类的对象则需要通过调用这个类的某个特定静态方法来获得,这样的设计可以避免某个类的构造函数被多次重复的调用,最典型的例子就是创建数据库连接。
那么Runtime所提供的静态方法就是getRuntime,通过调用getRuntime我们可以获取到Runtime类的一个对象。

例如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package org.example;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;

public class Main {
public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, NoSuchMethodException, InvocationTargetException {
// 等价于 Runtime myRuntime = Runtime.getRuntime();
Object myRuntime = Class.forName("java.lang.Runtime").getMethod("getRuntime").invoke();
// 等价于 myRuntime.exec("calc.exe");
// Runtime的exec方法有6个重载,这里的getMethod方法的第二个参数指定了获取参数为String的exec方法
Method exec = Class.forName("java.lang.Runtime").getMethod("exec", String.class);
exec.invoke(myRuntime,"calc.exe");

// 也可以使用getDeclaredConstructor方法来获取到Runtime类的私有构造方法
Constructor runtimeConstructor = Class.forName("java.lang.Runtime").getDeclaredConstructor();
// 修改私有方法权限
runtimeConstructor.setAccessible(true);
// 创建Runtime类实例
Object myRuntime2 = runtimeConstructor.newInstance();
// 获取exec方法
Method exec2 = Class.forName("java.lang.Runtime").getMethod("exec", String.class);
exec2.invoke(myRuntime2,"calc.exe");
}
}

序列化和反序列化

序列化:使用ObjectOutputStream类的writeObject函数

1
public final void writeObject(Object x) throws IOException

反序列化:使用ObjectInputStream类的readObject函数

1
public final Object readObject() throws IOException, ClassNotFoundException

支持序列化的对象必须满足:

  1. 实现了java.io.Serializable接口
  2. 当前对象的所有类属性可序列化,如果有一个属性不想或不能被序列化,则需要指定transient,使得该属性将不会被序列化

DOME:

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
package org.example;
import java.io.*;
class Employee implements Serializable{
private String name;
private String address;
// number成员声明了transient属性无法反序列化
private transient int number;

public Employee(String name, String address, int number){
this.name = name;
this.address = address;
this.number = number;
}

public void info(){
System.out.println("I am " + name);
System.out.println("Number is " + number);
}

// Override readObject
private void readObject(ObjectInputStream in) throws Exception {
// 调用默认的readObject方法,使其能正常反序列化
in.defaultReadObject();
// 重载的readObject方法多了一行输出
System.out.println("Employee call readObject Function");
}
}
public class Main {
public static void main(String[] args) {
Employee e = new Employee("Reyan Ali","Phokka Kuan, Ambehta Peer",123);
try {
// Serialize
// 创建一个文件输出流 -> 用于写入文件
FileOutputStream fileOut = new FileOutputStream("./employee.ser");
// 创建一个对象输出流 并且输出定向到文件中
ObjectOutputStream out = new ObjectOutputStream(fileOut);
// writeObject 序列化
out.writeObject(e);
out.close();
fileOut.close();
System.out.println("Serialized");

// Deserialize
// 创建文件输入流 -> 用于读取文件
FileInputStream fileIn = new FileInputStream("./employee.ser");
// 创建一个对象输入流
ObjectInputStream in = new ObjectInputStream(fileIn);
// readObject 反序列化
Employee obj = (Employee) in.readObject();
in.close();
fileIn.close();
obj.info();
System.out.println("Deserialized");

} catch(IOException i) {
i.printStackTrace();
} catch (ClassNotFoundException ex) {
throw new RuntimeException(ex);
}
}
}

运行结果:

image-20230130204141460

Number成员由于声明了transient属性没有反序列化,所以这里输出的值为0

反序列化触发点

除了常见的ObjectInputStream.readObject可以触发反序列化操作外,还有以下几种触发方式:

1
2
3
4
5
6
7
ObjectInputStream.readObject		// 流转化为Object
ObjectInputStream.readUnshared // 流转化为Object
XMLDecoder.readObject // 读取xml转化为Object -> XML反序列化
Yaml.load // yaml字符串转Object-> yaml反序列化
XStream.fromXML // XStream用于Java Object与xml相互转化
ObjectMapper.readValue // jackson中的api -> jackson反序列化漏洞
JSON.parseObject // fastjson中的api -> fastjson反序列化漏洞

readUnshared方法读取对象,不允许后续的readObjectreadUnshared调用引用这次调用反序列化得到的对象,而readObject读取的对象可以。

反序列化过程分析

步骤

反序列化的过程主要分为两步:

  1. 读取序列化字节流,根据序列化规格提取对应的类
  2. 利用反射实例化获得对象

流程

跟进ObjectInputStream.readObject方法:

image-20230130211852595

跟进readObject方法:

image-20230130212010365

主要的反序列化代码是调用的readObject0方法:

1
Object obj = readObject0(type, false);

readObject0里有一个switch判断:

image-20230130212421942

这里的115所指向的是TC_OBJECT,代表反序列化的对象是一个Object

image-20230130213053495

跟进readOrdinaryObject

image-20230130214019300

这里的readClassDesc(false)作用是从序列化流中提取出相关的类信息:

image-20230130214508184

跟进这里的readNonProxyDesc()方法:

image-20230130214604919

这里的关键点在于resolveClass()方法,这个方法实现了利用反射机制获取到类的Class对象:

image-20230130214701690

注意这里利用反射中的getNameforName`最终获取到了类的Class对象:

image-20230130214137398

此处整一个反射就是先通过Class.forName获取到当前描述器所指代的类的Class对象,后续会在initNonProxyinitProxy函数中复制该Class对象的相关信息(包括相关函数),最后在2044行处ObjectStreamClass.newInstance实例化该对象:

image-20230130213134883

获得对象之后会在2213行的readSerialData()函数将序列化流中的相关数据填充进实例化后的对象中或调用当前类描述器的readObject函数:

image-20230131090855974

readSerialData()具体实现如下:

image-20230131091855636

这里的hasReadObjectMethod用于判断该类是否有自己的readObject方法。

然后可以跟进invokeReadObject方法,更深入的看到是如何设置对象的字段值的,这里忽略掉中间调用,最终是在defaultReadFields中的setObjFieldValues方法实现的:

image-20230131092026261

小结

Java程序中类ObjectInputStreamreadObject方法被用来将数据流反序列化为对象,如果流中的对象是class,则它的ObjectStreamClass描述符会被读取,并返回相应的class对象,ObjectStreamClass包含了类的名称及serialVersionUID

如果类描述符是动态代理类,则调用resolveProxyClass方法来获取本地类。如果不是动态代理类则调用resolveClass方法来获取本地类。如果无法解析该类,则抛出ClassNotFoundException异常。

如果反序列化对象不是String、array、enum类型,ObjectStreamClass包含的类会在本地被检索,如果这个本地类没有实现java.io.Serializable或者externalizable接口,则抛出InvalidClassException异常。因为只有实现了SerializableExternalizable接口的类的对象才能被序列化。

前面分析中提到最后会调用resolveClass获取类的Class对象,这是反序列化过程中一个重要的地方,也是必经之路,所以有研究人员提出通过重载ObjectInputStreamresolveClass来限制可以被反序列化的类。

参考链接

  • 标题: Java反序列化基础
  • 作者:
  • 创建于 : 2023-03-11 19:38:54
  • 更新于 : 2025-08-02 01:45:55
  • 链接: https://yesec.github.io/2023/03/11/Java反序列化基础/
  • 版权声明: 本文章采用 CC BY-NC-SA 4.0 进行许可。
评论