Java序列化、反序列化、反序列化漏洞

救苦救难韩天尊發表於2024-09-25

目錄
  • 1 序列化和反序列化
    • 1.1 概念
    • 1.2 序列化可以做什麼?
  • 3 實現方式
    • 3.1 Java 原生方式
    • 3.2 第三方方式
  • 4 反序列化漏洞

1 序列化和反序列化

1.1 概念

Java 中序列化的意思是將執行時的物件轉成可網路傳輸或者儲存的位元組流的過程。而反序列化正相反,是把位元組流恢復成物件的過程。

1.2 序列化可以做什麼?

  1. 持久化儲存:將物件狀態儲存到儲存裝置(如硬碟)中,以便於後續讀取使用。
  2. 網路傳輸:將物件轉換成位元組流,透過網路傳送給另一個 JVM 例項,接收方再將位元組流轉回物件。
  3. 深度複製:透過序列化與反序列化可以實現物件的深複製,即建立一個新的物件,並且新物件的資料與原物件相同,但是它們在記憶體中的地址不同。

3 實現方式

3.1 Java 原生方式

step1:實現 Serializable 介面

要使一個類的物件能夠被序列化,只需要讓這個類實現 Serializable 介面即可。Serializable 是一個標記介面,它沒有定義任何方法。例如:

public class Person implements Serializable {
    // 可選,用於版本控制
    private static final long serialVersionUID = 1L; 
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
        "name='" + name + ''' +
        ", age=" + age +
        '}';
    }

}

step2:使用 ObjectOutputStream#writeObject() 方法序列化。例如:

public static void main(String[] args) throws IOException {
    ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D:\test.bin"));
    oos.writeObject(new Person("zhangsan",18));
}

step3:使用 ObjectInputStream#readObject() 方法反序列化。例如:

public static void main(String[] args) throws IOException, ClassNotFoundException {
    ObjectInputStream ois = new ObjectInputStream(Files.newInputStream(Paths.get("D:\test.bin")));
    Person p = (Person) ois.readObject();
    System.out.println(p);
}
// 列印 Person{name='zhangsan', age=18}

3.2 第三方方式

使用 Java 原生序列化方式序列化的物件只能被 Java 讀取(反序列化),所以可以考慮先把物件轉成一種通用的格式——如 JSON 字串,然後把 JSON 字串轉成位元組流進行網路傳輸,從而實現跨平臺或者跨語言。

這個時候就可以使用市面上開源的序列化工具了,比如 JSON、Xml、hessian等。

4 反序列化漏洞

反序列化是把資料流轉成物件,那麼萬一資料流被人惡意加工過呢?

拿 Java 原生的反序列化舉例,反序列化需要呼叫 ObjectInputStream#readObject() 方法,但是如果資料流的物件自己重寫了 readObject(),那 Java 便會呼叫自己的這個 readObject() 方法,這就給了攻擊者可乘之機,他們就能在自己的 readObject() 方法裡寫攻擊程式碼。

我們改造一下上面例子裡的 Person 類:

@Data
public class Person implements Serializable {
    // 可選,用於版本控制
    private static final long serialVersionUID = 1L;
    private String name;
    private int age;

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
        "name='" + name + ''' +
        ", age=" + age +
        '}';
    }

    // 重寫了 readObject 方法,反序列化時便會呼叫此處
    private void readObject(ObjectInputStream ois) throws IOException, ClassNotFoundException {
        ois.defaultReadObject(); //這一步是先讓反序列化讀取的時候按照預設的方法執行
        Runtime.getRuntime().exec("calc"); //這一步是攻擊程式碼,作用是開啟windows系統計算器
    }
}

此時再去執行反序列化便會開啟系統的計算器,如果把開啟計算器改成別的攻擊程式碼,攻擊者便能實現對系統的攻擊。

為什麼 Java 會允許我們重寫 readObject,並讓服務端呼叫我們的 readObject 呢?其實這麼做的原因是為了方便定製化某些類的序列化方法,比如 HashMap 類就重寫了 readObject 方法,原因是由於 HashMap 內部使用了一些特定的資料結構(如陣列和連結串列/紅黑樹),直接反序列化可能無法正確地恢復這些內部結構。因此,readObject 方法會負責根據序列化的資料正確地重建這些內部結構。

相關文章