
Java反序列化基础

反射机制
获取Class对象
forName
方法
用法:Class.forName(String className)
类名.class
用法:
className.class
getClass
方法
用法:Object.getClass()
其中
Object
应为上下文中已经实例化的类的对象。classLoader.loadClass
方法用法:
classLoader.loadClass(className)
此处需要注意的是获取数组类型的Class对象需要使用Java类型的描述符方式,例如:
1
2Class<?> doubleArray = Class.forName("[D"); //相当于double[].class
Class<?> cStringArray = Class.forName("[[Ljava.lang.String;"); // 相当于String[][].class
补充-获取内部类时需要将.
替换为$
,如com.security.Test1$Test2
创建类实例
获取类的构造函数
在Java的任何一个类都必须有一个或多个构造方法,如果代码中没有创建构造方法,那么在类编译的时候会自动创建一个无参数的构造方法。
Class<?>.getConstructor
用法:
Contstuctor c = Class<?>.getConstructor(参数列表类型)
获取类中
Public
属性的指定构造方法。例如:1
2
3Class clazz = Class.forName("java.lang.ProcessBuilder");
// 指定获取参数列表为String[].class的构造方法
clazz.getConstructor(String[].class);Class<?>.getDeclaredConstructor
用法:
Contstuctor c = Class<?>.getDeclaredConstructor(参数列表类型)
获取类中“声明”的指定构造方法,包括私有构造方法。
需要注意的是,当调用私有构造方法的时候需要使用
setAccessible
来修改其作用域,例如:1
2
3
4
5Class clazz = Class.forName("java.lang.Runtime");
// 获取Runtime的私有无参构造方法
Constructor c = clazz.getDeclaredConstructor();
// 由于是私有构造方法,需要使用setAccessible修改其作用域
c.setAccessible(true);Class<?>.getConstructors
用法:
Contstuctor[] c = Class<?>.getDeclaredConstructors()
获取类中所有的公有构造函数,返回一个构造函数列表。
Class<?>.getDeclaredConstructors
用法:
Contstuctor[] c = Class<?>.getDeclaredConstructors()
获取类中所有的构造函数,返回一个构造函数列表。
创建类的实例化对象
newInstance
方法
用法:Class<?>.newInstance()
,返回Object
类型,即返回Class<?>
的一个对象
实质上这个方法的作用是调用这个类的无参构造函数,当这个类没有无参构造方法或者无参构造方法为私有属性的时候,无法直接通过newInstance
来获得这个类的对象,这种情况下就需要先获取到指定的构造函数后再调用newInstance
方法来获取实例化对象。例如以下三个例子:- 无参构造方法
1
2Class clazz = Class.forName("com.security.example");
Object obj = clazz.newInstance()- 指定参数列表类型构造方法
1
2
3
4
5Class 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
7Class clazz = Class.forName("java.lang.Runtime");
// 获取Runtime的私有无参构造方法
Constructor c = clazz.getDeclaredConstructor();
// 由于是私有构造方法,需要使用setAccessible修改其作用域
c.setAccessible(true);
// 调用Runtime类私有无参构造方法获取Runtime类实例
Object obj = c.newInstance()
获取/调用类方法
获取类中函数
getMethod
方法
用法:Method m = Class<?>.getMethod(String name, class<?>...parameterType)
第一个参数代表方法名,第二个参数表示该方法参数类型(可缺省),不过Java中支持类的重载,一个同名函数往往会由于参数列表的不同而具有不同的效果,因而无法仅通过一个函数名来确定一个函数,因此第二个参数也会用来获取指定的一个方法。此方法仅能获取到Public属性或从父类继承的方法。getDeclaredMethod
方法用法:
Method m = Class<?>.getDeclaredMethod(String name, class<?>...parameterType)
该方法与
getDeclaredConstructor
方法类似,可以获取到私有属性的方法,在使用invoke
调用私有方法前同样需要使用setAccessible
方法修改其作用域。getMethods
方法用法:
Method[] m = Class<?>.getMethods()
获得该类所有公有方法,返回一个
Method[]
类型的方法列表。getDeclaredMethods
方法用法:
Method[] m = Class<?>.getDeclaredMethods()
获得该类所有方法,返回一个
Method[]
类型的方法列表。。
调用类中函数
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
9Class 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
5Class clazz = Class.forName("java.lang.Runtime");
// 获取Runtime类getRuntime方法
Method myGetRuntime = clazz.getMethod("getRuntime");
// 调用Runtime类getRuntime方法
myGetRuntime.invoke(clazz);
获取/设置成员变量
获取成员变量
getField
方法用法:
Field field = clazz.getField("变量名");
获取当前类指定的公有或从父类继承的成员变量。
getDeclaredField
方法用法:
Field field = clazz.getDeclaredField("变量名");
获取当前类指定的成员变量。
getFields
方法用法:
Field[] fields = clazz.getFields();
获取当前类所有的公有或从父类继承的成员变量。
getDeclaredFields
方法用法:
Field[] fields = clazz.getDeclaredFields();
获取当前类所有的成员变量。
获取成员变量的值
get
方法用法:
Object obj = field.get(类实例对象);
获取成员变量值。
设置成员变量的值
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 | package org.example; |
序列化和反序列化
序列化:使用ObjectOutputStream
类的writeObject
函数
1 | public final void writeObject(Object x) throws IOException |
反序列化:使用ObjectInputStream
类的readObject
函数
1 | public final Object readObject() throws IOException, ClassNotFoundException |
支持序列化的对象必须满足:
- 实现了
java.io.Serializable
接口 - 当前对象的所有类属性可序列化,如果有一个属性不想或不能被序列化,则需要指定
transient
,使得该属性将不会被序列化
DOME:
1 | package org.example; |
运行结果:
Number
成员由于声明了transient
属性没有反序列化,所以这里输出的值为0
反序列化触发点
除了常见的ObjectInputStream.readObject
可以触发反序列化操作外,还有以下几种触发方式:
1 | ObjectInputStream.readObject // 流转化为Object |
readUnshared
方法读取对象,不允许后续的readObject
和readUnshared
调用引用这次调用反序列化得到的对象,而readObject
读取的对象可以。
反序列化过程分析
步骤
反序列化的过程主要分为两步:
- 读取序列化字节流,根据序列化规格提取对应的类
- 利用反射实例化获得对象
流程
跟进ObjectInputStream.readObject
方法:
跟进readObject方法:
主要的反序列化代码是调用的readObject0
方法:
1 | Object obj = readObject0(type, false); |
在readObject0
里有一个switch
判断:
这里的115
所指向的是TC_OBJECT
,代表反序列化的对象是一个Object
:
跟进readOrdinaryObject
:
这里的readClassDesc(false)
作用是从序列化流中提取出相关的类信息:
跟进这里的readNonProxyDesc()
方法:
这里的关键点在于resolveClass()
方法,这个方法实现了利用反射机制获取到类的Class对象:
注意这里利用反射中的getName和
forName`最终获取到了类的Class对象:
此处整一个反射就是先通过Class.forName
获取到当前描述器所指代的类的Class对象,后续会在initNonProxy
或initProxy
函数中复制该Class对象的相关信息(包括相关函数),最后在2044行处ObjectStreamClass.newInstance
实例化该对象:
获得对象之后会在2213行的readSerialData()
函数将序列化流中的相关数据填充进实例化后的对象中或调用当前类描述器的readObject函数:
readSerialData()
具体实现如下:
这里的hasReadObjectMethod
用于判断该类是否有自己的readObject
方法。
然后可以跟进invokeReadObject
方法,更深入的看到是如何设置对象的字段值的,这里忽略掉中间调用,最终是在defaultReadFields
中的setObjFieldValues
方法实现的:
小结
Java程序中类ObjectInputStream
的readObject
方法被用来将数据流反序列化为对象,如果流中的对象是class
,则它的ObjectStreamClass
描述符会被读取,并返回相应的class对象,ObjectStreamClass
包含了类的名称及serialVersionUID
。
如果类描述符是动态代理类,则调用resolveProxyClass
方法来获取本地类。如果不是动态代理类则调用resolveClass
方法来获取本地类。如果无法解析该类,则抛出ClassNotFoundException
异常。
如果反序列化对象不是String、array、enum类型,ObjectStreamClass
包含的类会在本地被检索,如果这个本地类没有实现java.io.Serializable
或者externalizable
接口,则抛出InvalidClassException
异常。因为只有实现了Serializable
或Externalizable
接口的类的对象才能被序列化。
前面分析中提到最后会调用resolveClass
获取类的Class对象,这是反序列化过程中一个重要的地方,也是必经之路,所以有研究人员提出通过重载ObjectInputStream
的resolveClass
来限制可以被反序列化的类。
参考链接
- 标题: 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 进行许可。