Java安全之反序列化(1)
序列化與反序列化
概述
Java序列化是指把Java對象轉換為位元組序列的過程;這串字符可能被儲存/發送到任何需要的位置,在適當的時候,再將它轉回原本的 Java 對象,而Java反序列化是指把位元組序列恢復為Java對象的過程。
為什麼需要序列化與反序列化
當兩個進程進行遠程通信時,可以相互發送各種類型的數據,包括文本、圖片、音頻、視頻等, 而這些數據都會以二進制序列的形式在網絡上傳送。那麼當兩個Java進程進行通信時,能否實現進程間的對象傳送呢?答案是可以的。如何做到呢?這就需要Java序列化與反序列化了。換句話說,一方面,發送方需要把這個Java對象轉換為位元組序列,然後在網絡上傳送;另一方面,接收方需要從位元組序列中恢復出Java對象
Java 提供了兩個類 java.io.ObjectOutputStream
和 java.io.ObjectInputStream
來實現序列化和反序列化的功能,其中 ObjectInputStream 用於恢復那些已經被序列化的對象,ObjectOutputStream 將 Java 對象的原始數據類型和圖形寫入 OutputStream。
在 Java 的類中,必須要實現 java.io.Serializable
或 java.io.Externalizable
接口才可以使用,而實際上 Externalizable 也是實現了 Serializable 接口
ObjectOutputStream
ObjectOutputStream 繼承的父類或實現的接口如下:
- 父類
OutputStream
:所有位元組輸出流的頂級父類,用來接收輸出的位元組並發送到某些接收器(sink)。 - 接口
ObjectOutput
:ObjectOutput 擴展了 DataOutput 接口,DataOutput 接口提供了將數據從任何 Java 基本類型轉換為位元組序列並寫入二進制流的功能,ObjectOutput 在 DataOutput 接口基礎上提供了writeObject
方法,也就是類(Object)的寫入。 - 接口 ObjectStreamConstants:定義了一些在對象序列化時寫入的常量。常見的一些的比如
STREAM_MAGIC
、STREAM_VERSION
等。
通過這個類的父類及父接口,我們大概可以理解這個類提供的功能:能將 Java 中的類、數組、基本數據類型等對象轉換為可輸出的位元組,也就是反序列化。接下來看一下這個類中幾個關鍵方法
writeObject
這是 ObjectOutputStream 對象的核心方法之一,用來將一個對象寫入輸出流中,任何對象,包括字符串和數組,都是用 writeObject
寫入到流中的。
之前說過,序列化的過程,就是將一個對象當前的狀態描述為位元組序列的過程,也就是 Object -> OutputStream 的過程,這個過程由 writeObject
實現。writeObject
方法負責為指定的類編寫其對象的狀態,以便在後面可以使用與之對應 readObject
方法來恢復它
writeUnshared
用於將非共享對象寫入 ObjectOutputStream,並將給定的對象作為刷新對象寫入流中。
使用 writeUnshared
方法會使用 BlockDataOutputStream 的新實例進行序列化操作,不會使用原來 OutputStream 的引用對象。
writeObject0
writeObject
和 writeUnshared
實際上調用 writeObject0
方法,也就是說 writeObject0
是上面兩個方法的基礎實現。具體的實現流程將會在後面再進行詳細研究。
writeObjectOverride
如果 ObjectOutputStream 中的 enableOverride 屬性為 true,writeObject
方法將會調用 writeObjectOverride
,這個方法是由 ObjectOutputStream 的子類實現的。
在由完全重新實現 ObjectOutputStream 的子類完成序列化功能時,將會調用實現類的 writeObjectOverride
方法進行處理。
ObjectInputStream
ObjectInputStream 繼承的父類或實現的接口如下:
- 父類
InputStream
:所有位元組輸入流的頂級父類。 - 接口
ObjectInput
:ObjectInput 擴展了 DataInput 接口,DataInput 接口提供了從二進制流讀取位元組並將其重新轉換為 Java 基礎類型的功能,ObjectInput 額外提供了readObject
方法用來讀取類。 - 接口
ObjectStreamConstants
:同上。
ObjectInputStream 實現了反序列化功能,看一下其中的關鍵方法。
readObject
從 ObjectInputStream 讀取一個對象,將會讀取對象的類、類的簽名、類的非 transient 和非 static 字段的值,以及其所有父類類型。
我們可以使用 writeObject
和 readObject
方法為一個類重寫默認的反序列化執行方,所以其中 readObject
方法會 「傳遞性」 的執行,也就是說,在反序列化過程中,會調用反序列化類的 readObject
方法,以完整的重新生成這個類的對象。
readUnshared
從 ObjectInputStream 讀取一個非共享對象。 此方法與 readObject
類似,不同點在於readUnshared
不允許後續的 readObject
和 readUnshared
調用引用這次調用反序列化得到的對象。
readObject0
readObject
和 readUnshared
實際上調用 readObject0
方法,readObject0
是上面兩個方法的基礎實現。
readObjectOverride
由 ObjectInputStream 子類調用,與 writeObjectOverride 一致。
通過上面對 ObjectOutputStream 和 ObjectInputStream 的了解,兩個類的實現幾乎是一種對稱的、雙生的方式進行
反序列化漏洞
一個類想要實現序列化和反序列化,必須要實現 java.io.Serializable
或 java.io.Externalizable
接口。
Serializable 接口是一個標記接口,標記了這個類可以被序列化和反序列化,而 Externalizable 接口在 Serializable 接口基礎上,又提供了 writeExternal
和 readExternal
方法,用來序列化和反序列化一些外部元素。
其中,如果被序列化的類重寫了 writeObject 和 readObject 方法,Java 將會委託使用這兩個方法來進行序列化和反序列化的操作。
正是因為這個特性,導致反序列化漏洞的出現:在反序列化一個類時,如果其重寫了 readObject
方法,程序將會調用它,如果這個方法中存在一些惡意的調用,則會對應用程序造成危害。
在這裡我們利用寫一個簡單的測試程序,如下代碼創建了 Person 類,實現了 Serializable 接口,並重寫了 readObject 方法,在方法中使用 Runtime 執行命令彈出計算器
public class Person implements Serializable {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException {
Runtime.getRuntime().exec("calc.exe");
}
}
然後我們將這個類序列化並寫在文件中,隨後對其進行反序列化,就觸發了命令執行
public class SerializableTest {
public static void main(String[] args) throws IOException, ClassNotFoundException {
Person person = new Person("gk0d", 24);
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.txt"));
oos.writeObject(person);
oos.close();
FileInputStream fis = new FileInputStream("test.txt");
ObjectInputStream ois = new ObjectInputStream(fis);
ois.readObject();
ois.close();
}
}
那為什麼我們重寫了readObject就會執行呢?來看一下 java.io.ObjectInputStream#readObject()
方法的具體實現代碼。
readObject
方法實際調用 readObject0
方法反序列化字符串
readObject0
方法以位元組的方式去讀,如果讀到 0x73
,則代表這是一個對象的序列化數據,將會調用 readOrdinaryObject
方法進行處理
readOrdinaryObject
方法會調用 readClassDesc
方法讀取類描述符,並根據其中的內容判斷類是否實現了 Externalizable
接口,如果是,則調用 readExternalData
方法去執行反序列化類中的 readExternal
,如果不是,則調用 readSerialData
方法去執行類中的 readObject
方法
在 readSerialData
方法中,首先通過類描述符獲得了序列化對象的數據布局。通過布局的 hasReadObjectMethod
方法判斷對象是否有重寫 readObject
方法,如果有,則使用 invokeReadObject
方法調用對象中的 readObject
我們就了解了反序列化漏洞的觸發原因。與反序列漏洞的觸發方式相同,在序列化時,如果一個類重寫了 writeObject
方法,並且其中產生惡意調用,則將會導致漏洞,當然在實際環境中,序列化的數據來自不可信源的情況比較少見。
那接下來該如何利用呢?我們需要找到那些類重寫了 readObject
方法,並且找到相關的調用鏈,能夠觸發漏洞。