Java 序列化將物件轉為二進位流,支援 RMI 等遠端通訊。但當服務端對不可信輸入執行反序列化時,駭客可構造惡意物件流,利用類別路徑中既有的「脆弱類別」觸發拒絕服務甚至任意程式碼執行。本文示範如何在不引入加密、簽章或欄位校驗的前提下,透過「先驗類別驗證」在物件構建前攔截非預期類別。
服務端類別路徑包含海量類別(核心庫、第三方框架、中介軟體)。駭客利用條件:
Serializable 或 Externalizable。反序列化不呼叫建構子,但會自動調用以下任一方法執行程式碼:
readObject():自訂反序列化邏輯(如處理 transient 欄位)。readResolve():單例模式還原。readExternal():Externalizable 介面方法。歷史證實此類攻擊可突破 Applet 沙箱,同樣威脅伺服端應用。
序列化流開頭包含 類別描述元資料(類別名、serialVersionUID、欄位定義),早於實際物件資料。這使我們可在讀取物件資料前先檢視類別名稱,決定是否繼續。
STREAM_MAGIC (0xACED) STREAM_VERSION (5) TC_OBJECT (0x73) TC_CLASSDESC (0x72) className: "com.ibm.ba.scg.LookAheadDeserializer.Bicycle" serialVersionUID: 0x4FDAAF97F8CCC0DA fields: id(int), nbrWheels(int), name(String) classdata: id=0, nbrWheels=1, name="Unicycle"
關鍵在於:類別名稱出現在物件資料之前,提供天然的驗證窗口。
Java 提供「鉤子方法」resolveClass(ObjectStreamClass desc),專為自訂類別載入設計,完美適配驗證需求。步驟:
ObjectInputStream。resolveClass(),比對 desc.getName() 與白名單。InvalidClassException,阻斷後續反序列化。public class LookAheadObjectInputStream extends ObjectInputStream {
public LookAheadObjectInputStream(InputStream in) throws IOException { super(in); }
@Override
protected Class<?> resolveClass(ObjectStreamClass desc)
throws IOException, ClassNotFoundException {
if (!desc.getName().equals(Bicycle.class.getName())) {
throw new InvalidClassException("Unauthorized deserialization attempt", desc.getName());
}
return super.resolveClass(desc);
}
}
// 序列化合法 Bicycle 物件
byte[] good = serialize(new Bicycle(0, "Unicycle", 1));
// 序列化惡意 java.io.File 物件
byte[] bad = serialize(new File("Pierre Ernst"));
// 反序列化合法物件 → 成功
Bicycle b0 = (Bicycle) deserialize(good); // 輸出 "Unicycle has been deserialized."
// 反序列化惡意物件 → 於 resolveClass 階段拋出異常,物件未構建
Bicycle b1 = (Bicycle) deserialize(bad); // InvalidClassException
圖 1. 應用程式輸出:惡意 File 物件在 resolveClass 階段被攔截
「先驗類別驗證」在物件圖構建前即可攔截非預期類別,屬於最早期的防禦層。實務上:
ObjectInputFilter(Java 9+)實現更細粒度過濾。