
Java反序列化是java安全的基礎,想要學好java反序列化,就不能只看看相關文章,要自己動手實踐,看看java反序列化到底是怎么回事。
JSON和XML是通用數據交互格式,通常用于不同語言、不同環境下數據的交互,比如前端的JavaScript通過JSON和后端服務通信、微信服務器通過XML和公眾號服務器通信。但這兩個數據格式都有一個共同的問題:不支持復雜的數據類型。大多數處理方法中,JSON和XML支持的數據類型就是基本數據類型,整型、浮點型、字符串、布爾等,如果開發者希望在傳輸數據的時候直接傳輸一個對象,那么就不得不想辦法擴展基礎的JSON(XML)語法。
快速入門
Java Serialization(序列化):將java對象以一連串的字節保存在磁盤文件中的過程,也可以說是保存java對象狀態的過程。序列化可以將數據永久保存在磁盤上(通常保存在文件中)。
下面我們就手敲代碼,自己實現一個序列化程序!
public class main { private static class innerClass implements Serializable { String name; String test; int years; public innerClass(){} public innerClass(String name, String test, int years) { this.name = name; this.test = test; this.years = years; } @Override public String toString() { return "innerClass{" + "name='" + name + '\'' + ", test='" + test + '\'' + ", years=" + years + '}'; } } public static void main(String[] args) throws Exception { innerClass ic = new innerClass();//創建對象 ic.name="123"; ic.test="test"; ic.years=123546; File f = new File("java_security/1.txt");// 模塊名/文件名 if(f.exists()) { System.out.println("文件存在"); }else{ //否則創建新文件 f.createNewFile(); } try{ FileOutputStream fos=new FileOutputStream(f); ObjectOutputStream oos=new ObjectOutputStream(fos); oos.writeObject(ic);//將ic對象序列化寫入文件 oos.flush(); oos.close(); fos.close(); }catch (Exception e) { System.out.println(e); } }}
注意點:
1、序列化對象需要實現Serializable接口2、序列化需要使用ObjectOutputStream對象創建對象輸出流3、ObjectOutputStream對象序列化所用方法writeObject()4、ObjectOutputStream對象需要文件輸出流作為輸出目標5、FileOutputStream對象需要一個文件對象
因此,我們整個實現過程為:創建需要序列化的對象、創建文件對象、創建文件輸出流對象、創建對象輸出流對象、序列化。
運行程序,我們得到1.txt。

可以看到,在java_security模塊下生成了1.txt文件,里面包含著innerClass對象(即我剛剛序列化的對象)的序列化字節碼。
這些字節碼都是我們人為不可看的,很不利于我們在對于java反序列化或者java安全方面的研究,有什么辦法能解決這個問題呢?
SerializationDumper
我們可以使用SerializationDumper來將序列化字節碼轉化為方便閱讀的形式,下面我們就一起來裝一下SerializationDumper吧。
git clone https://github.com/NickstaDB/SerializationDumper.git
進入安裝路徑,執行build.bat 文件。
E:\web-Tools\SerializationDumper> build.bat
然后就可以在該目錄中使用SerializationDumper.jar了,接下來我們就試試使用SerializationDumper。
將SerializationDumper拖入項目。

E:\IntelliJ IDEA 2018.2.7\project\java_security>java -jar SerializationDumper.jarUsage: SerializationDumper <hex-ascii-data> SerializationDumper -f <file-containing-hex-ascii> SerializationDumper -r <file-containing-raw-data> Rebuild a dumped stream: SerializationDumper -b <input-file> <output-file>
按照上述使用方法 使用 -r 處理raw-data文件。
E:\IntelliJ IDEA 2018.2.7\project\java_security>java -jar SerializationDumper.jar -r 1.txt > 2.txt STREAM_MAGIC - 0xac edSTREAM_VERSION - 0x00 05Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 15 - 0x00 0f Value - main$innerClass - 0x6d61696e24696e6e6572436c617373 serialVersionUID - 0xca 3e 75 e0 69 b7 50 c5 newHandle 0x00 7e 00 00 classDescFlags - 0x02 - SC_SERIALIZABLE fieldCount - 3 - 0x00 03 Fields 0: Int - I - 0x49 fieldName Length - 5 - 0x00 05 Value - years - 0x7965617273 1: Object - L - 0x4c fieldName Length - 4 - 0x00 04 Value - name - 0x6e616d65 className1 TC_STRING - 0x74 newHandle 0x00 7e 00 01 Length - 18 - 0x00 12 Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b 2: Object - L - 0x4c fieldName Length - 4 - 0x00 04 Value - test - 0x74657374 className1 TC_REFERENCE - 0x71 Handle - 8257537 - 0x00 7e 00 01 classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 02 classdata main$innerClass values years (int)123546 - 0x00 01 e2 9a name (object) TC_STRING - 0x74 newHandle 0x00 7e 00 03 Length - 3 - 0x00 03 Value - 123 - 0x313233 test (object) TC_STRING - 0x74 newHandle 0x00 7e 00 04 Length - 4 - 0x00 04 Value - test - 0x74657374

這里就可以很清楚的看到,跟我們之前設定的屬性是相符合的。
反序列化
try{ FileInputStream fis=new FileInputStream("java_security/1.txt"); ObjectInputStream ois = new ObjectInputStream(fis); innerClass ic2=(innerClass)ois.readObject(); System.out.println(ic2); ois.close(); fis.close();}catch(Exception e) { System.out.println(e);}
與序列化的代碼片相反,反序列化將文件內的字節流重新反序列化為對象。反序列化流程如上,便不再贅述。

###
復寫readObject和writeObject
經過上面簡單的案例,大家應該能了解到序列化與反序列化的大體步驟,接下來就開始了解readObject和writeObject的復寫。
進階的一些小trick
我們看到類實現的Serializable 接口,它是沒有任何內容的,相當于一個標識符。

那么我們該怎么復寫readObject和writeObject呢。分析源碼:
public final void writeObject(Object obj) throws IOException { if (enableOverride) { writeObjectOverride(obj); return; } try { writeObject0(obj, false); } catch (IOException ex) { if (depth == 0) { writeFatalException(ex); } throw ex; }}
首先從writeObject方法進了writeObject0。
if (obj instanceof String) { writeString((String) obj, unshared); } else if (cl.isArray()) { writeArray(obj, desc, unshared); } else if (obj instanceof Enum) { writeEnum((Enum<?>) obj, desc, unshared); } else if (obj instanceof Serializable) { writeOrdinaryObject(obj, desc, unshared); } else { if (extendedDebugInfo) { throw new NotSerializableException( cl.getName() + "\n" + debugInfoStack.toString()); } else { throw new NotSerializableException(cl.getName()); } }

跟蹤語句,我們找到了這樣一句,若obj或其子類實現了Serializable,則進入這個判斷語句,即進入writeOrdinaryObject方法。
instanceof 是java的保留關鍵字。他的作用就是測試左邊的對象是不是右邊類的實例,是的話就返回true,不是的話返回false。
private void writeOrdinaryObject(Object obj, ObjectStreamClass desc, boolean unshared){ ...... if (desc.isExternalizable() && !desc.isProxy()) { writeExternalData((Externalizable) obj); } else { writeSerialData(obj, desc); } ......}
這里按按實現了Externalizable接口或Serializable接口分別執行writeExternalData和writeSerialData方法,我們這里進入writeSerialData方法。
private void writeSerialData(Object obj, ObjectStreamClass desc) throws IOException{ ObjectStreamClass.ClassDataSlot[] slots = desc.getClassDataLayout(); for (int i = 0; i < slots.length; i++) { ObjectStreamClass slotDesc = slots[i].desc; if (slotDesc.hasWriteObjectMethod()) { ... } } ......}
進入hasWriteObjectMethod方法。
boolean hasWriteObjectMethod() { requireInitialized(); return (writeObjectMethod != null);}
康康writeObjectMethod變量。
/** class-defined writeObject method, or null if none */private Method writeObjectMethod;
這里可以看到了,如果類中定義了 writeObject 。查找該變量康康。

好!這里可以看到,利用了java反射得到對象的writeObject方法,這里就說判斷序列化對象中是否含有writeObject方法。
回到writeSerialData方法。
if (slotDesc.hasWriteObjectMethod()) {//如果目標類重寫了writeObject方法 PutFieldImpl oldPut = curPut; curPut = null; SerialCallbackContext oldContext = curContext; if (extendedDebugInfo) { debugInfoStack.push( "custom writeObject data (class \"" + slotDesc.getName() + "\")"); } try { curContext = new SerialCallbackContext(obj, slotDesc); bout.setBlockDataMode(true); //利用反射執行類中的writeObject方法 slotDesc.invokeWriteObject(obj, this); bout.setBlockDataMode(false); bout.writeByte(TC_ENDBLOCKDATA); } finally { curContext.setUsed(); curContext = oldContext; if (extendedDebugInfo) { debugInfoStack.pop(); } } curPut = oldPut;} else { //沒重新,執行默認反序列化方法 defaultWriteFields(obj, slotDesc);}
看到這里,我們基本上就可以得出結論了:
要重寫readObject和writeObject方法,只需要在需要序列化和反序列化中的類中寫相應的方法。簡單的說,以readObject方法為例,在ObjectInputStream對象調用readObject時,經過一系列調用,檢測你需要序列化對象中是否含有readObject,如果有則通過java反射特性,得到需要序列化對象的readObject方法,否則使用默認的readObject方法!
接下來,我們來改寫一下inner類,加入readObject和writeObject方法。
private static class innerClass implements Serializable { String name; String test; int years; public innerClass(){} public innerClass(String name, String test, int years) { this.name = name; this.test = test; this.years = years; } @Override public String toString() { return "innerClass{" + "name='" + name + '\'' + ", test='" + test + '\'' + ", years=" + years + '}'; } private void readObject(ObjectInputStream is) throws IOException, ClassNotFoundException { // 自定義反序列化實現 System.out.println("readObject execute"); is.defaultReadObject(); String message = (String) is.readObject(); System.out.println(message); } private void writeObject(ObjectOutputStream is) throws IOException, ClassNotFoundException { // 自定義序列化實現 System.out.println("writebject execute"); is.defaultWriteObject(); is.writeObject("This is a object"); }}

運行結果無疑是成功了。
再康康序列化出來的結構。
STREAM_MAGIC - 0xac edSTREAM_VERSION - 0x00 05Contents TC_OBJECT - 0x73 TC_CLASSDESC - 0x72 className Length - 15 - 0x00 0f Value - main$innerClass - 0x6d61696e24696e6e6572436c617373 serialVersionUID - 0xca 3e 75 e0 69 b7 50 c5 newHandle 0x00 7e 00 00 classDescFlags - 0x03 - SC_WRITE_METHOD | SC_SERIALIZABLE fieldCount - 3 - 0x00 03 Fields 0: Int - I - 0x49 fieldName Length - 5 - 0x00 05 Value - years - 0x7965617273 1: Object - L - 0x4c fieldName Length - 4 - 0x00 04 Value - name - 0x6e616d65 className1 TC_STRING - 0x74 newHandle 0x00 7e 00 01 Length - 18 - 0x00 12 Value - Ljava/lang/String; - 0x4c6a6176612f6c616e672f537472696e673b 2: Object - L - 0x4c fieldName Length - 4 - 0x00 04 Value - test - 0x74657374 className1 TC_REFERENCE - 0x71 Handle - 8257537 - 0x00 7e 00 01 classAnnotations TC_ENDBLOCKDATA - 0x78 superClassDesc TC_NULL - 0x70 newHandle 0x00 7e 00 02 classdata main$innerClass values years (int)123546 - 0x00 01 e2 9a name (object) TC_STRING - 0x74 newHandle 0x00 7e 00 03 Length - 3 - 0x00 03 Value - 123 - 0x313233 test (object) TC_STRING - 0x74 newHandle 0x00 7e 00 04 Length - 4 - 0x00 04 Value - test - 0x74657374 objectAnnotation TC_STRING - 0x74 newHandle 0x00 7e 00 05 Length - 16 - 0x00 10 Value - This is a object - 0x546869732069732061206f626a656374 TC_ENDBLOCKDATA - 0x78
最后多出來一節。
objectAnnotation TC_STRING - 0x74 newHandle 0x00 7e 00 05 Length - 16 - 0x00 10 Value - This is a object - 0x546869732069732061206f626a656374 TC_ENDBLOCKDATA - 0x78
這就意味著我們可以在序列化時向字節碼中寫入一些這個對象的屬性以外的東西。這個特性就讓Java的開發變得非常靈活。
總結
本文初步介紹了java反序列化步驟,自己動手實實在在的實現了基本的序列化與反序列化。利用SerializationDumper看到了序列化字節碼的基本結構。對readObject與writeObject方法進行源碼跟進,理解了為什么Serializable是空接口的情況下,我們可以在需要反序列化的類內部直接重寫readObject與writeObject方法,實質是ObjectOutputStream對象經過調用鏈,利用java反射,獲取了類中的方法,從而執行在類中的readObject與writeObject方法。
java反序列化的學習還在繼續,下一章我將和大家一起學習java中一些簡單的反序列化漏洞。
安全俠
Andrew
上官雨寶
ManageEngine卓豪
尚思卓越
Andrew
安全俠
X0_0X
一顆小胡椒
cayman
007bug
一顆小胡椒