Java序列化和反序列化(一)

Java序列化和反序列化(一)

一.基础

1.什么是序列化和反序列化

序列化:对象 -> 字符串
反序列化:字符串 -> 对象

2.为什么要进行序列化和反序列化

序列化和反序列化的设计就是用来传输数据的

当两个进程通信的时候,可以通过序列化反序列化来进行传输

3.序列化的好处

(1)能够实现数据的持久化,通过序列化可以把数据永久保存在硬盘上,也可理解为通过序列化将数据保存在文件中。

(2)利用序列化实现远程通信,在网络中进行传输对象的字节序列

4.序列化与反序列化的场景

(1)想把内存中的对象保存到一个文件中或者是数据库中

(2)用套接字在网络上进行传输

(3)通过RMI传输对象的时候

5.几种创建的序列化和反序列化

XML$SOAP
JSON
Protobuf

二.代码示范

类文件:Person.java

package src;
import java.io.Serializable;
​
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 "src.Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

序列化文件:SerializationTest.java

package src;
​
​
import java.io.FileOutputStream;//文件输出流
import java.io.IOException;//用于声明可能会抛出IOException的方法。当一个方法可能会引发输入/输出异常时,可以使用throws IOException来通知调用该方法的其他部分,让它们做出相应的异常处理。
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;//将对象以二进制形式写入输出流。它可以将对象序列化成字节流,用于在网络中传输或保存到文件中。
public class SerializationTest {
    public static void serialize(Object obj) throws IOException{
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));//输出流对象
        oos.writeObject(obj);//序列化
    }
​
    public static void main(String[] args) throws Exception{
        Person person = new Person("aa",22);
        System.out.println(person);
        serialize(person);
    }
}

反序列化文件:UnserializeTest.java

package src;
​
import java.io.FileInputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
​
public class UnserializeTest {
    public static Object unserialize(String Filename) throws IOException, ClassNotFoundException{
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();
        return obj;
    }
​
    public static void main(String[] args) throws Exception{
        Person person = (Person)unserialize("ser.bin");
        System.out.println(person);//反序列化
    }
}

代码讲解

运行代码

Run SerializationTest.java

Run UnserializationTest.java

SerializationTest.java

我们将代码进行了封装,将序列化功能封装进了 serialize这个方法里面,在序列化当中,我们通过这个FileOutputStream输出流对象,将序列化的对象输出到ser.bin当中。再调用 oos 的writeObject方法,将对象进行序列化操作。

ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("ser.bin"));//输出流对象
        oos.writeObject(obj);//序列化

UnserializationTest.java

进行反序列化

ObjectInputStream ois = new ObjectInputStream(new FileInputStream(Filename));
        Object obj = ois.readObject();

Serializable 接口

(1)序列化类的属性没有实现Serializable,那么在序列化时就会报错

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

Serializable 接口是 Java 提供的序列化接口,它是一个空接口,所以其实我们不需要实现什么。

public interface Serializable {
}

Serializable 用来标识当前类可以被 ObjectOutputStream 序列化,以及被 ObjectInputStream 反序列化。如果我们此处将 Serializable 接口删除掉的话,会导致如下结果。

(2)在序列化过程中,它的父类如果没有实现序列化接口,那么将无需提供无参构造函数来重新创建对象。

(3)一个实现Serializable接口的子类也是可以被序列化的。

(4)静态成员变量是不能被序列化

序列化是针对对象属性的,而静态成员变量是属于类的。

(5)transient 标识的对象成员变量不参与序列化

这里我们可以动手实操一下,将 Person.java中的name加上transient的类型标识。加完之后再跑我们的序列化与反序列化的两个程序,修改过程与运行结果如图所示。

 

三.序列化的安全问题

1.引子

序列化和反序列化中有两个重要的方法————writeObject和readObject

这两个方法可以经过开发者重写,一般序列化的重写都是由于下面的场景诞生的。

举个例子,MyList 这个类定义了一个 arr 数组属性,初始化的数组长度为 100。在实际序列化时如果让 arr 属性参与序列化的话,那么长度为 100 的数组都会被序列化下来,但是我在数组中可能只存放 30 个数组而已,这明显是不可理的,所以这里就要自定义序列化过程啦,具体的做法是重写以下两个 private 方法:
private void writeObject(java.io.ObjectOutputStream s)throws java.io.IOException
private void readObject(java.io.ObjectInputStream s)throws java.io.IOException, ClassNotFoundException

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

所以从根本上讲,Java反序列化的漏洞与readObject有关。

2.可能存在的漏洞形式

(1)入口类的readObject直接调用危险方法

这种情况呢,在实际开发场景中并不是特别常见,我们还是跟着代码来走一遍,写一段弹计算器的代码,文件 ———— "Person.Java"

package src;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.Serializable;
​
public class Person implements Serializable {
​
    private transient String name;
    private int age;
​
    public Person(){
​
    }
    // 构造函数
    public Person(String name, int age){
        this.name = name;
        this.age = age;
    }
​
    @Override
    public String toString(){
        return "src.Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
​
    public void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException{
        ois.defaultReadObject();//调用默认机制,以恢复对象的非静态和非瞬态(非 transient 修饰)字段
        Runtime.getRuntime().exec("calc");//在操作系统上执行外部命令。
    }
}

先运行序列化程序 ———— "SerializationTest.java",再运行反序列化程序 ———— "UnserializeTest.java"

这时就会弹出计算器,也就是calc.exe

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

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

(4)构造函数/静态代码块等加载时隐式执行

四.Java反射

1.Java反射定义

对于任意一个类,都能够得到这个类的所有属性和方法;对于任意一个对象,都能够调用它的任意方法和属性;这种动态获取信息以及动态调用对象方法的功能称为java语言的反射机制。

其实在Java中定义的一个类本身也是一个对象,即java.lang.Class类的实例,这个实例称为类对象

  • 类对象表示正在运行的 Java 应用程序中的类和接口

  • 类对象没有公共构造方法,由 Java 虚拟机自动构造

  • 类对象用于提供类本身的信息,比如有几种构造方法, 有多少属性,有哪些普通方法

要得到类的方法和属性,首先就要得到该类对象

2.获取类对象

假设现在有一个User类

package reflection;
​
public class User {
    private String name;
​
    public User(String name) {
        this.name=name;
    }
    public void setName(String name) {
        this.name=name;
    }
    public String getName() {
        return name;
    }
}

要获取该类的对象一般有三种方法

class.for.name("reflection.User")
User.class
new User().getClass()

最常用的是第一种,通过一个字符串即类的全路径名就可以得到类对象,另外两种方法依赖项太强

3.利用类对象创建对象

与new直接创建对象不同,反射是先拿到类对象,然后通过类对象获取构造器对象,再通过构造器对象创建一个对象

package reflection;
import java.lang.reflect.*;
​
public class CreateObject {
    public static void main(String[] args) throws Exception{
        Class UserClass = Class.forName("reflection.User");
        Constructor constructor = UserClass.getConstructor(String.class);
        User user = (User) constructor.newInstance("iu");
        
        System.out.println(user.getName());
    }
}
方法说明
getConstructor(Class...<?> parameterTypes) 获得该类中与参数类型匹配的公有构造方法
getConstructors() 获得该类的所有公有构造方法
getDeclaredConstructor(Class...<?> parameterTypes) 获得该类中与参数类型匹配的构造方法
getDeclaredConstructors() 获得该类所有构造方法

4.通过反射调用方法

package reflection;
import java.lang.reflect.*;
​
public class CallMethod {
    public static void main(String[] args) throws Exception{
        Class UserClass = Class.forName("reflection.User");
        Constructor constructor = UserClass.getConstructor(String.class);
        User user = (User) constructor.newInstance("iu");
        
        Method method = UserClass.getDeclaredMethod("setName", String.class);
        method.invoke(user, "lizhien");
        
        System.out.println(user.getName());
    }
}
方法说明
getMethod(String name, Class...<?> parameterTypes) 获得该类某个公有的方法
getMethods() 获得该类所有公有的方法
getDeclaredMethod(String name, Class...<?> parameterTypes) 获得该类某个方法
getDeclaredMethods() 获得该类所有方法

5.通过反射访问属性

package reflection;
import java.lang.reflect.*;
​
public class AccessAttribute {
    public static void main(String[] args) throws Exception{
        Class UserClass = Class.forName("reflection.User");
        Constructor constructor = UserClass.getConstructor(String.class);
        User user = (User) constructor.newInstance("iu");
        
        Field field = UserClass.getDeclaredField("name");
        field.setAccessible(true);// name是私有属性,需要先设置可访问
        field.set(user, "lizhien");
        
        System.out.println(user.getName());
    }
}
方法说明
getField(String name) 获得某个公有的属性对象
getFields() 获得所有公有的属性对象
getDeclaredField(String name) 获得某个属性对
getDeclaredFields() 获得所有属性对象

6.利用java反射执行代码

package reflection;
​
public class Exec {
    public static void main(String[] args) throws Exception{
        Class runtimeClass = Class.forName("java.lang.Runtime");
        Object runtime = runtimeClass.getMethod("getRuntime").invoke(null);//getRuntime是静态方法,invoke时不需要传入对象
        runtimeClass.getMethod("exec", String.class).invoke(runtime, "calc.exe");
    }
}

以上代码中,利用了Java的反射机制把我们的代码意图都利用字符串的形式进行体现,使得原本应该是字符串的属性,变成了代码执行的逻辑,而这个机制也是后续的漏洞使用的前提

 
posted @ 2023-08-24 16:02  LoYoHo00  阅读(178)  评论(0编辑  收藏  举报
levels of contents