本文是IBM developerWorks中的一篇文章,介紹了不使用加密與簽章技術,如何防止對不可信數據輸入的解序列化。(2013.01.18最後更新)
Java序列化允許開發者將Java對象保存為二進制格式,以便將該對象持久化到一個文件中或將其在網絡中進行傳遞。遠程方法調用(RMI)使用序列化作為客戶端與服務器端之間的通信媒介。當服務從客戶端接收二進制數據,以及將輸入的數據進行解序列化去構造Java實例時,就會產生多種安全問題。本文關注其中一種問題:駭客可能序列化另一個類的實例,並將其傳給服務程序。那麼服務程序就會解序列化該惡意對象,並很可能將該對象強制轉換為服務所期望得到的合法類型,而這將導致異常的發生。然而,該異常對於確保數據安全性則顯得太晚了。本文解釋了為什麼要以及怎樣去實現一種安全的序列化。
脆弱的類
你的服務程序不可能反序列化任意類的對象。為什麼不能呢?簡單的回答是:因為在服務器端的類路徑中可能存有被駭客利用的脆弱類。這些類所包含的代碼為駭客造就了拒絕服務(DOS)的條件,或者--在極端情況下--會允許駭客注入任意代碼。
你可能會相信存在這種攻擊的可能性,但考慮到一個典型的服務器端程序的類路徑中存在太多的類,不僅包含你自己的代碼,還包括Java核心類庫,第三方的類庫,以及其它的中間件或框架中的類庫。另外,在應用程序的生命周期中,類路徑可能會被改變,或者為了應對底層運行環境的變化,應用程序的類路徑也可能被修改。當試圖去利用這樣的弱點時,通過傳送多個序列化對象,駭客能夠將這些操作組合到一塊。
我應該強調一下,僅當滿足如下條件時,服務才會解序列化一個惡意對象:
1. 惡意對象的類存在於服務器端的類路徑中。駭客不可能隨便地傳遞任意類的序列化對象,因為應用服務可能無法加載這個類。
2. 惡意對象的類要麼是可序列化的,要麼是可外部化的。(即,服務器端的這個類要實現java.io.Serializable或java.io.Externalizable)
另外,通過從序列化流中直接復制數據,在不調用構造器的情況下,解序列化操作就能產生對象樹,所以駭客不可能執行序列化對象類的構造器中的Java代碼。
但駭客還有其它途徑在服務器端去執行代碼。無論JVM在何時去解序列化一個對象,都將實現如下三個方法中的一個,都將調用並執行該方法中的代碼:
1. 方法readObject(),當標准的序列化機制不適用時,開發者一般就會用到該方法。例如,當需要對transient成員變量進行賦值時。
2. 方法readResolve(),一般用於序列化單例對象。
3. 方法readExternal(),用於外部化對象。
所以,如果在你的類路徑中存在著使用上述方法的類,你就必須意識到駭客可能會在遠程調用這些方法。此類攻擊在過往曾被用於破壞Applet安全沙箱;同樣地,相同的攻擊技術也可用於服務器端應用。
繼續讀下去,將會看到如何才能只允許應用服務對其期望的類的對象進行解序列化。
Java序列化二進制格式
一個對象被序列化之後,二進制數據將包含有元數據(指與數據的結構相關的信息,例如類的名稱,成員的數量,以及成員的類型),及對象數據本身。我將以一個簡單的Bicycle類作為例子,如清單1所示,該類包含三個成員變量(id,name和nbrWheels)以及與之對應的set與get方法。
清單1. Bicycle類
package com.ibm.ba.scg.LookAheadDeserializer; public class Bicycle implements java.io.Serializable { private static final long serialVersionUID = 5754104541168320730L; private int id; private String name; private int nbrWheels; public Bicycle(int id, String name, int nbrWheels) { this.id = id; this.name = name; this.nbrWheels = nbrWheels; } public String getName() { return name; } public void setName(String name) { this.name = name; } public void setId(int id) { this.id = id; } public int getId() { return id; } public int getNbrWheels() { return nbrWheels; } public void setNbrWheels(int nbrWheels) { this.nbrWheels = nbrWheels; } }
在一個清單1所示類的實例被序列化之後,其數據流如清單2所示:
清單2. Bicycle類的序列化流
000000: AC ED 00 05 73 72 00 2C 63 6F 6D 2E 69 62 6D 2E |········com.ibm.| 000016: 62 61 2E 73 63 67 2E 4C 6F 6F 6B 41 68 65 61 64 |ba.scg.LookAhead| 000032: 44 65 73 65 72 69 61 6C 69 7A 65 72 2E 42 69 63 |Deserializer.Bic| 000048: 79 63 6C 65 4F DA AF 97 F8 CC C0 DA 02 00 03 49 |ycle···········I| 000064: 00 02 69 64 49 00 09 6E 62 72 57 68 65 65 6C 73 |··idI··nbrWheels| 000080: 4C 00 04 6E 61 6D 65 74 00 12 4C 6A 61 76 61 2F |L··name···Ljava/| 000096: 6C 61 6E 67 2F 53 74 72 69 6E 67 3B 78 70 00 00 |lang/String;····| 000112: 00 00 00 00 00 01 74 00 08 55 6E 69 63 79 63 6C |·········Unicycl| 000128: 65 |e|
對上述數據應用標准的對象序列化流協議,你將看到如清單3所示的序列化對象:
清單3. 被序列化的Bicycle對象的細節
STREAM_MAGIC (2 bytes) 0xACED STREAM_VERSION (2 bytes) 5 newObject TC_OBJECT (1 byte) 0x73 newClassDesc TC_CLASSDESC (1 byte) 0x72 className length (2 bytes) 0x2C = 44 text (59 bytes) com.ibm.ba.scg.LookAheadDeserializer.Bicycle serialVersionUID (8 bytes) 0x4FDAAF97F8CCC0DA = 5754104541168320730 classDescInfo classDescFlags (1 byte) 0x02 = SC_SERIALIZABLE fields count (2 bytes) 3 field[0] primitiveDesc prim_typecode (1 byte) I = integer fieldName length (2 bytes) 2 text (2 bytes) id field[1] primitiveDesc prim_typecode (1 byte) I = integer fieldName length (2 bytes) 9 text (9 bytes) nbrWheels field[2] objectDesc obj_typecode (1 byte) L = object fieldName length (2 bytes) 4 text (4 bytes) name className1 TC_STRING (1 byte) 0x74 length (2 bytes) 0x12 = 18 text (18 bytes) Ljava/lang/String; classAnnotation TC_ENDBLOCKDATA (1 byte) 0x78 superClassDesc TC_NULL (1 byte) 0x70 classdata[] classdata[0] (4 bytes) 0 = id classdata[1] (4 bytes) 1 = nbrWheels classdata[2] TC_STRING (1 byte) 0x74 length (2 bytes) 8 text (8 bytes) Unicycle
從清單3中你可以看到該序列化對象的類型為com.ibm.ba.scg.LookAheadDeserializer.Bicycle,它的ID為0,只有一個輪子,即它是一個獨輪車。
重點是這個二進制格式包含一種文件頭,這就允許你對輸入進行校驗。
類校驗
如你在清單3中所看到的,在讀取該二進制流時,在對象本身出現之前,首先會看到該序列化對象的類型描述。這種結構就允許實現自己的算法去讀取類型描述,並依靠類的名稱去決定是否繼續讀取該序列化流。幸運地是,通過使用Java提供的一個常用於定制類加載的"鉤子",你能很容易地實現該功能--即,覆蓋resolveClass()方法。這個"鉤子"方法非常適合用於提供定制的校驗功能,無論序列化流何時包含了不被期望的類,你都可以用這個方法去拋出一個異常。你需要繼承類java.io.ObjectInputStream,並覆蓋其中的resolveClass()方法。清單4中的代碼就利用該項技術確保只有Bicycle類的實例才可被解序列化。
清單4. 定制校驗"鉤子"程序
package com.ibm.ba.scg.LookAheadDeserializer; import java.io.IOException; import java.io.InputStream; import java.io.InvalidClassException; import java.io.ObjectInputStream; import java.io.ObjectStreamClass; import com.ibm.ba.scg.LookAheadDeserializer.Bicycle; public class LookAheadObjectInputStream extends ObjectInputStream { public LookAheadObjectInputStream(InputStream inputStream) throws IOException { super(inputStream); } /** * Only deserialize instances of our expected Bicycle class */ @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); } }
查看本欄目
通過對com.ibm.ba.scg.LookAheadDeserializer類的實例調用readObject()方法,就可以防止對不被期望的對象進行解序列化操作。
作為一個示例應用程序,清單5序列化了兩個對象--一個是期望的類(com.ibm.ba.scg.LookAheadDeserializer.Bicycle)的實例,另一個是不被期望的類(java.io.File)的實例--然後使用清單4中的定制校驗"鉤子"程序去嘗試它們進行解序列化。
清單5. 使用定制的"鉤子"程序
package com.ibm.ba.scg.LookAheadDeserializer; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import com.ibm.ba.scg.LookAheadDeserializer.Bicycle; public class LookAheadDeserializer { private static byte[] serialize(Object obj) throws IOException { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(obj); byte[] buffer = baos.toByteArray(); oos.close(); baos.close(); return buffer; } private static Object deserialize(byte[] buffer) throws IOException, ClassNotFoundException { ByteArrayInputStream bais = new ByteArrayInputStream(buffer); // We use LookAheadObjectInputStream instead of InputStream ObjectInputStream ois = new LookAheadObjectInputStream(bais); Object obj = ois.readObject(); ois.close(); bais.close(); return obj; } public static void main(String[] args) { try { // Serialize a Bicycle instance byte[] serializedBicycle = serialize(new Bicycle(0, "Unicycle", 1)); // Serialize a File instance byte[] serializedFile = serialize(new File("Pierre Ernst")); // Deserialize the Bicycle instance (legitimate use case) Bicycle bicycle0 = (Bicycle) deserialize(serializedBicycle); System.out.println(bicycle0.getName() + " has been deserialized."); // Deserialize the File instance (error case) Bicycle bicycle1 = (Bicycle) deserialize(serializedFile); } catch (Exception ex) { ex.printStackTrace(System.err); } } }
當運行該應用程序時,在試圖去java.io.File的對象進行解序列化之前,JVM就拋出異常,如圖1所示:
圖1. 應用程序輸出
結論
本文向你展示了在序列化流中發現不被期望的類之後,若不使用加密,簽章,或簡單的成員變量校驗等手段,如何能盡快地停止Java解序列化操作。
需要記住的是,整棵對象樹(根對象,及其所有的成員對象)是在解序列化過程中進行組建的。在更為復雜的情況下,你可能必須允許更多的類可被解序列化。