Java序列化和反序列化

Java序列化和反序列化

版权申明:本文为原创文章,转载请注明原文出处

原文链接:http://example.com/post/c3110923.html

Java序列化和反序列化

Java序列化和反序列化入门

参考视频:Java反序列化漏洞专题-基础篇(21/09/05更新类加载部分)_哔哩哔哩_bilibili

初始序列化和反序列化

Java序列化是指把Java对象转换为字节序列的过程

Java反序列化是指把字节序列恢复为Java对象的过程

序列化分为两大部分:序列化和反序列化。序列化是这个过程的第一部分,将数据分解成字节流,以便存储在文件中或在网络上传输。反序列化就是打开字节流并重构对象。对象序列化不仅要将基本数据类型转换成字节表示,有时还要恢复数据。恢复数据要求有恢复数据的对象实例。

序列化函数是写文件,将序列化后的内容写到文件ser.bin

1
2
3
4
public static void serialize(Object obj) throws IOException {
ObjectOutputStream oos = new ObjectOutputStream(Files.newOutputStream(Paths.get("ser.bin")));
oos.writeObject(obj);
}

反序列化函数是读文件,读取文件ser.bin文件中的内容

1
2
3
4
public static Object unserialize(String Filename) throws IOException, ClassNotFoundException {
ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get(Filename)));
return ois.readObject();
}
举例说明

整体流程是首先定义了一个 Person 类,然后通过 serialize 方法将 Person 对象序列化到文件中,然后再通过 unserialize 方法将文件中的数据反序列化为 Person 对象。

先构造一个 Person

image-20240326153856780

先进行序列化生成 ser.bin 文件

1
2
3
4
5
public static void main(String[] args) throws Exception{
Person person = new Person("Alice", 30, "1234 Main St", "555-1234");
System.out.println(person);
serialize(person);
}

ser.bin 文件

image-20240326154024774

然后进行反序列化输出 ser.bin 文件

1
2
3
4
public static void main(String[] args) throws Exception {
Person person = (Person)unserialize("ser.bin");
System.out.println(person);
}

输出内容如下,说明是成功读取了文件流

image-20240326154116092

关键说明
  • implement

    在person类中实现了一个接口,关键字implements用于声明一个类实现(implement)一个接口。

    implements Serializable 表示 Person 类实现了 Serializable 接口。

    Serializable 接口是一个标记接口,它没有任何方法。实现了 Serializable 接口的类表示这些类的对象可以被序列化,即可以将对象转换为字节流以便在网络上传输或者将其保存到文件中。在Java中,对象的序列化和反序列化是一种常见的机制,用于持久化对象或在网络中传输对象。通过实现 Serializable 接口,Java虚拟机(JVM)可以对这些对象执行序列化和反序列化操作。

    image-20240326154534496

    当去掉这里的 implements Serializable 再次执行序列化的语句就会报错,接口的函数是空的,说明不需要实现什么,但是需要标注代表该类是可以被序列化的

  • 不参与序列化的标记(transient

    在Java中,transient 关键字用于标记类的成员变量,表示这些变量不参与对象的序列化过程。当一个对象被序列化时,其 transient 修饰的成员变量的值不会被保存到序列化数据中,而在反序列化时,这些成员变量会被初始化为其默认值。

    这种行为的设计是出于安全和灵活性的考虑。有时候,某些对象的状态可能包含敏感信息或者不需要被序列化和持久化,使用 transient 关键字可以排除这些成员变量不被序列化。例如,密码字段、临时计算结果等通常都会被标记为 transient

    对name标记transient

    image-20240326155614913

    序列化和反序列化函数执行中都不会报错,但是反序列化执行的结果name的值为null

    image-20240326155840350

安全问题

当有序列化的对象传递到本台主机时,只要服务器端反序列化数据,客户端传递类的readObject中代码会自动执行,给予攻击者在服务器上运行代码的能力

这个安全问题涉及到 Java 序列化的一个特性,即序列化数据中可以包含用于对象恢复的自定义逻辑。在 Java 中,当一个类实现了 java.io.Serializable 接口并包含 private void readObject(ObjectInputStream in) 和/或 private void writeObject(ObjectOutputStream out) 方法时,这些方法可以被用来自定义序列化和反序列化的过程。

攻击者可以利用这一特性来在服务器上执行恶意代码,原理如下:

  1. 攻击者构造一个带有恶意逻辑的特制的序列化数据。
  2. 攻击者将这个序列化数据发送给一个服务器,服务器上的代码对这个数据进行反序列化。
  3. 在服务器端,当序列化数据被反序列化时,readObject 方法会被调用,而攻击者可以在这个方法中嵌入恶意代码,从而在服务器上执行这些代码。

这个问题的核心在于,Java 的序列化机制允许在对象恢复时执行特定的代码,而这个特性可以被攻击者利用来在服务器端执行任意代码,从而造成严重的安全风险。

可利用条件
  • 共同条件:继承Serializable

    这个条件是序列化攻击的前提条件之一。Java 中的序列化和反序列化机制是建立在类实现 Serializable 接口的基础上的。因此,为了利用序列化漏洞,对象必须实现 Serializable 接口,以便进行序列化和反序列化操作。

  • 入口类 source(重写readObject参数类型广泛,最好jdk自带)(例如:Map类

    source 类是指在反序列化过程中执行的入口点,通常是一个包含 readObject 方法的类。攻击者可以在该方法中执行恶意代码。为了增加攻击的成功率,攻击者通常会寻找一些 JDK 自带的类,因为这些类在很多情况下会被信任并且有更广泛的使用场景。

  • 调用链 gadget chain

    调用链指的是一系列的对象和方法调用,这些调用会在反序列化时被触发执行。攻击者会构造一个恶意的对象图,其中包含一些有漏洞的类,这些类会在反序列化时触发恶意操作。这些类和方法的组合构成了所谓的 gadget chain。攻击者的目标是利用这个 gadget chain 来实现攻击。

  • 执行类 sink(rce ssrf写文件等等)

    执行类指的是被攻击者利用 gadget chain 中的漏洞触发执行的类或方法。这些类或方法通常会包含一些危险的操作,比如执行远程代码(RCE)、发起服务器端请求伪造(SSRF)、写文件等等。攻击者的目标就是利用 gadget chain 最终达到执行这些危险操作的目的。

可能的形式
  • 入口类的 readObject 直接调用危险方法

    攻击者可以在序列化数据中包含一些触发恶意操作的指令,而这些指令会在 readObject 方法中被执行。例如,攻击者可能会在 readObject 中调用 Runtime.exec() 方法来执行系统命令,或者执行其他敏感的操作,从而实现对服务器的攻击。

    比如直接在 Person 类中重写 readObject() 方法

    1
    2
    3
    4
    private void readObject(ObjectInputStream ois) throws IOException,ClassNotFoundException{
    ois.defaultReadObject();
    Runtime.getRuntime().exec("calc");
    }

    再次序列化然后反序列化,成功弹出了计算器

    image-20240326162006965

  • 入口类的参数中包含可控类,该类有危险方法, readObject 时调用

    攻击者可以通过控制序列化数据中的某些类的内容,使得这些类在被反序列化时调用其中的危险方法。例如,攻击者可能构造一个特定的类,其中包含一个 finalize() 方法,在该方法中执行恶意代码。当这个类被反序列化时,finalize() 方法会被自动执行,从而触发攻击。

  • 入口类的参数中包含可控类,该类又调用其他有危险方法的类, readObject 时调用

    攻击者可能构造一个复杂的对象图,其中包含多个类,这些类之间存在调用关系。攻击者可以通过控制序列化数据中的这些类的内容,使得反序列化过程中执行危险方法。例如,攻击者可能控制一个类,该类在反序列化时调用其他类的危险方法,从而间接实现对服务器的攻击。

  • 构造函数/静态代码块等类加载时隐式执行

    在 Java 中,类的构造函数和静态代码块在类加载时会被隐式执行。攻击者可以构造恶意类,其中包含有恶意代码的构造函数或静态代码块。当这些类被反序列化时,其构造函数或静态代码块会被执行,从而触发攻击。这种情况下,攻击者无需调用 readObject 方法,而是利用类加载时的隐式执行来触发攻击。

Java反射和URLDNS链

Reflection定义

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

  • 正射:定义类,调用方法

    1
    2
    Student student = new Student();
    student.getId("1");
  • 反射:反射就是在运行时才知道要操作的类是什么,并且可以在运行时获取类的完整构造,并调用对应的方法。(把java类中的各种成分映射成一个个的Java对象)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    // 通过类的全限定名获取Class对象
    Class clazz = Class.forName("reflection.Student");

    // 获取指定方法对象,这里是获取名为getId,参数类型为String的方法
    Method method = clazz.getMethod("getId", String.class);

    // 获取无参构造函数对象
    Constructor constructor = clazz.getConstructor();

    // 通过构造函数创建类的实例
    Object object = constructor.newInstance();

    // 调用实例的指定方法,这里是调用getId方法,并传递参数1
    method.invoke(object, "1");
Reflection作用
  1. 让Java具有动态性:反射使Java可以在运行时加载和操作类,即使这些类在编译时是未知的。这在插件系统、依赖注入等场景非常有用。

1
2
3
4
5
6
7
// 假设类名是通过配置文件或其他方式动态获取的
String className = "com.example.MyClass";
Class<?> clazz = Class.forName(className);

// 输出类名
System.out.println("Class loaded: " + clazz.getName());

解释

  • 通过类的全限定名动态加载类。
  • 可以根据配置文件或其他输入来决定加载哪个类,使程序更具动态性。
  1. 修改已有对象的属性:反射可以用来访问和修改对象的私有属性。(下图实现了修改person对象的name值)

image-20240601152713619image-20240601152516036

解释

  • 获取私有属性的Field对象。
  • 将属性设置为可访问(即使是私有的)。
  • 修改属性的值。
  1. 动态生成对象:通过反射可以动态创建对象实例。

image-20240601152819556image-20240601152844874

解释

  • 通过Class对象获取构造函数。
  • 使用构造函数动态创建对象实例。
  1. 动态调用方法:通过反射可以在运行时调用对象的方法。

image-20240601153144217image-20240601153215161

解释

  • 通过Class对象获取方法对象。
  • 使用方法对象调用方法,并传递参数。
  1. 操作内部类和私有方法:反射可以用来操作内部类和私有方法。

image-20240601153559550image-20240601153626409

解释

  • 获取内部类和私有方法的Class对象和Method对象。
  • 动态创建内部类实例,并调用私有方法。
举例说明
  1. 结合参数创建新对象:直接使用 newInstance() 是无法传参的,但是通过获取构造函数对象函数 getConstructor() 函数可以实现传参(思路:从具体实例对象获取类对象,再获取创建函数对象,再创建实例对象)

image-20240601155001867

  1. 获取类里面属性getFields 函数(输出public属性)和 getDeclaredFields 函数(输出public+private属性)区别,这两个函数都是获取属性对象

    image-20240601155800809image-20240601155941697

    输出

    image-20240601160022885image-20240601160034312

  2. 根据属性名获取变量参数getField 函数(public属性)和 getDeclaredField 函数(public+private属性),这两个函数都是获取属性对象,改变值的时候需要指定要改变的实例对象,其中对于私有对象,反射的权限是很大的,所以只需要设置 setAccessible 函数为true

    image-20240601160644555image-20240601160503017

  3. 调用类里面的方法:获取该方法调用的对象,同样分为 getMethods 函数和 getDeclaredMethods 函数,区别和获取属性一致

    getMethod 函数和 getDeclaredMethod 函数获取单个方法对象,获取函数方法的时候,需要指明该函数需要传参的变量的类型,该函数方法在使用的时候( invoke)时,要指明执行该方法的实例对象

    image-20240601161405697image-20240601161415478

URLDNS链
原理

HashMap 类中存在 readObject() 犯法,在该方法中有一句 putVal(hash(key),***)

image-20240601201044511

再进入此处的 hash 方法,发现调用了 key 变量的 hashCode() 方法

image-20240601201344002

此处将 key 设置为URL类,URL类中有 hashCode() 方法,且初始化为-1(也代表只有第一次调用该函数时,该对象的hashCode变量值为-1)

image-20240601201416411image-20240601201433945

JDK动态代理

  1. 静态代理:创建一个对象,实现调用另一个对象的函数,同时可以独立记录日志等操作

    示例如下,代码输出为: show method in IUserImpl show method in UserProxy

    image-20240602140015032image-20240602140031652image-20240602140046252

    缺点:接口实现了多少方法,代理里就需要添加多少方法(繁琐)

  2. 动态代理:创建动态代理对象,重写 InvocationHandler 类的 invoke 调用函数方法

    示例如下,代码输出为 method: show is invoked show method in IUserImpl

    每一个动态代理类的调用处理程序都必须实现InvocationHandler接口,并且每个代理类的实例都关联到了实现该接口的动态代理类调用处理程序中,当我们通过动态代理对象调用一个方法时候,这个方法的调用就会被转发到实现InvocationHandler接口类的invoke方法来调用

    image-20240602141852317image-20240602141911736

类的动态加载

类加载流程:加载--验证--准备--解析--初始化(静态代码块被调用)--实例化(使用,构造代码块,无/有参构造函数)--卸载

示例

Person类如下:

image-20240602143013092

  • (使用)当输入代码 new Person() 输出为 静态代码块被调用了 构造代码块被调用了 无参构造方法被调用了

  • (使用)当输入代码 new Person("test", 18, "test", "test"); 输出为 静态代码块被调用了 构造代码块被调用了 有参构造方法被调用了

  • (使用)当输入代码 Person.staticMethod(); 输出为 静态代码块被调用了 静态方法被调用了

  • (初始化)当输入代码 Person.id = 1; 输出为 静态代码块被调用了

  • (类加载)当输入代码 Class clazz = Person.class; 无输出

  • (默认进行初始化)当输入代码 Class clazz = Class.forName("com.example.java_deserialization.Person"); 输出为 静态代码块被调用了

  • (不进行初始化)当输入代码

    1
    2
    3
    4
    5
    6
    7
    ClassLoader cl=ClassLoader.getSystemClassLoader();
    Class.forName("com.example.java_deserialization.Person",false,cl);

    //或者是

    ClassLoader cl=ClassLoader.getSystemClassLoader();
    Class c= cl.loadClass("com.example.java_deserialization.Person");

    无输出

  • 当输入代码

    1
    2
    3
    4
    5
    6
    7
    8
    9
    ClassLoader cl=ClassLoader.getSystemClassLoader();
    Class c=Class.forName("com.example.java_deserialization.Person",false,cl);
    c.newInstance();

    //或者是

    ClassLoader cl=ClassLoader.getSystemClassLoader();
    Class c= cl.loadClass("com.example.java_deserialization.Person");
    c.newInstance();

    输出为 静态代码块被调用了 构造代码块被调用了 无参构造方法被调用了

结论

动态类加载方法: Class.forName 可以选择是否初始化

  1. ClassLoader cl = ClassLoader.getSystemClassLoader();
    • ClassLoader 是 Java 类加载器的基类,负责动态加载 Java 类到 JVM 中。
    • getSystemClassLoader()ClassLoader 类的静态方法,用来获取系统类加载器。系统类加载器通常是负责加载应用程序类路径(即 CLASSPATH 环境变量指向的路径)下的类。
  2. Class c = cl.loadClass("com.example.java_deserialization.Person");
    • loadClass(String name)ClassLoader 的一个方法,用于加载指定类的字节码并返回 Class 对象。
    • 这里 name 参数是类的全限定名(即包含包名的完整类名),比如 com.example.java_deserialization.Person
    • 调用 cl.loadClass("com.example.java_deserialization.Person") 会让类加载器加载 Person 类并返回对应的 Class 对象。如果类已经被加载,这个方法会直接返回现有的 Class 对象。
  3. c.newInstance();
    • newInstance()Class 类的方法,用于创建这个 Class 对象所表示的类的一个新实例。这个方法要求 Class 对象必须具有一个无参的构造方法。
    • c.newInstance() 相当于调用这个类的无参构造函数,并返回新创建的对象的引用。
详细机理解释
  1. ClassLoader 工作原理

    Java 的类加载器机制遵循双亲委派模型(parent delegation model),主要包括以下几个步骤:

    • 检查缓存:检查类加载器的缓存,是否已经加载过该类,如果加载过直接返回类的 Class 对象。
    • 委派给父类加载器:如果没有加载过,当前类加载器会先委派父类加载器去加载该类。每个类加载器都有一个父加载器,除了根加载器(Bootstrap ClassLoader)。
    • 自己加载:如果父加载器也没有加载过这个类,那么当前类加载器才会尝试自己去加载。

    系统类加载器 (System ClassLoader) 是 JVM 提供的默认类加载器,通常是应用程序类加载器(Application ClassLoader),它负责加载 CLASSPATH 下的类。

  2. Class 加载过程

    类加载过程可以分为以下几个阶段:

    • 加载(Loading):查找并导入类的二进制数据。

    • 链接(Linking)

      • 验证(Verification):确保导入的类文件的字节码符合 JVM 的规范。
      • 准备(Preparation):为类的静态变量分配内存,并将其初始化为默认值。
      • 解析(Resolution):将类、接口、字段、方法的符号引用替换为直接引用。
    • 初始化(Initialization):执行类构造器 <clinit>() 方法,初始化静态变量和静态代码块。

  3. Reflection 工作原理

    反射机制允许在运行时操作类和对象。Class 类提供了一些方法来获取类的信息以及创建类的实例:

    • newInstance() 方法会调用类的无参构造方法创建一个新的对象实例。如果类没有无参构造方法或者构造方法不可访问(例如是 private 的),调用 newInstance() 会抛出异常。

底层的原理,实现加载任意的类

ClassLoader->SecureClassLoader->URLClassLoader->APPClassLoader

loadClass->findClass(重写的方法)->defineClass(从字节码加载)

可利用
  1. 利用URLClassLoader的加载类方法(传参类型为URL)加载类(file/http/jar)

    • file:

      1
      2
      3
      URLClassLoader urlClassLoader = new URLClassLoader(new java.net.URL[]{new java.net.URL("file:///D:\\java workforse\\java_deserialization\\")});
      Class c = urlClassLoader.loadClass("com.example.java_deserialization.Hello");
      c.newInstance();
    • http:先启动在class目录下使用 python -m http.server 8888 启动http服务,再使用http协议传递文件

      1
      2
      3
      URLClassLoader urlClassLoader = new URLClassLoader(new java.net.URL[]{new java.net.URL("http://localhost:8888/")});
      Class c = urlClassLoader.loadClass("com.example.java_deserialization.Hello");
      c.newInstance();
  2. 利用defineClass,该类是Protect,只能用反射调用,字节码加载任意类,私有方法

    1
    2
    3
    4
    5
    Method defineClassMethod = ClassLoader.class.getDeclaredMethod("defineClass", String.class, byte[].class, int.class, int.class);
    defineClassMethod.setAccessible(true);
    byte[] code= Files.readAllBytes(Paths.get("D:\\java workforse\\java_deserialization\\Hello.class"));
    Class c =(Class) defineClassMethod.invoke(cl, "com.example.java_deserialization.Hello", code,0,code.length);
    c.newInstance();
  3. 利用Unsafe类,实际利用的是Unsafe.defineClass,也是字节码加载任意类,public类,类不能直接生成,里面可以直接上传

    1
    2
    3
    4
    5
    6
    7
    8
    9
    byte[] code= Files.readAllBytes(Paths.get("D:\\java workforse\\java_deserialization\\Hello.class"));
    // Class c =(Class) defineClassMethod.invoke(cl, "com.example.java_deserialization.Hello", code,0,code.length);
    // c.newInstance();
    Class unsafe= Unsafe.class;
    Field f=unsafe.getDeclaredField("theUnsafe");
    f.setAccessible(true);
    Unsafe u=(Unsafe)f.get(null);
    Class c = (Class) u.defineClass("com.example.java_deserialization.Hello", code, 0, code.length, cl, null);
    c.newInstance();

Java序列化和反序列化

http://example.com/post/c3110923.html

Author

yyyyyyxnp

Posted on

2024-03-26

Updated on

2024-09-29

Licensed under

You need to set install_url to use ShareThis. Please set it in _config.yml.
You forgot to set the business or currency_code for Paypal. Please set it in _config.yml.

Comments

You forgot to set the shortname for Disqus. Please set it in _config.yml.