年前開始研究Java反序列化,一直沒有研究Java反序列化文件的格式。最近閑來無事,研究一下java反序列化文件的格式。扯這么多的原因主要是要達成兩個目標
- 嘗試使用python讀取java反序列化文件,並轉換為Json格式以方便閱讀(其實沒多少用,還是一樣難懂)。
- json定義反序列化,隨意編輯反序列化流,通過json生成序列化文件。縮小某些payload工具的大小,生成一個exp需要N多依賴的jar包,這不講伍德
首先我們先大致講解一下java序列化以及相關東西。
- java序列化功能只傳遞對象的屬性,不傳遞對象的方法,靜態變量等等。你可以把java序列化功能類比Json傳輸。
- java存在八種基本類型,分別是字節,整型數字,浮點數,布爾值。至於對象,其實是這八種基本類型的復雜組合。無論多么復雜的對象,其實都可以被解析為這八種基本類型。當然這八種基本類型在其他語言中也存在,為我們使用python解析反序列化文件提供了理論基礎。
1 基礎類型的讀取
序列化中也有基礎類型。我們需要定義幾個函數用來讀取基礎類型。
例如
- 一個Byte一般作為指令,標記后面的數據流是什么
- int為4個Byte,也就是32位byte
- Short為2個Byte,是16位
- 以此類推,按照c語言的數據類型長度定義,千萬別亂寫
- 字符串類型,在這里我們只討論ascii編碼的情況,暫時不討論Utf編碼的情況。在序列化中,一個字符串類型,首先讀取兩個Byte,也就是short類型,作為字符串的長度。隨后按照字符串的長度,讀取Byte作為字符串。
2 序列化中控制指令
前面我們提到,讀取一個Byte作為控制指令,控制指令的作用是標記后面的數據類型,序列化中控制指令如下
TC_NULL = b'\x70'
標記后面的數據為空,對應java就是NullTC_REFERENCE = b'\x71
java序列化協議是一個格式十分緊湊的協議,是不會出現兩個一摸一樣的對象,類等。如果第二次出現,則會通過reference去指向之前的那個內容。你可以把這個類比為指針。TC_CLASSDESC = b'\x72'
這個是處理並返回類描述符。。與下面的class的區別在於,這個返回的是描述類的一個對象,主要包括類的名稱,suid等各種屬性。TC_CLASS = b'\x76'
在java序列化中,類的傳輸通過名稱,suid等屬性,對端通過名稱查找classpath中該類。而這個TC_CLASS將會根據上面的類描述符,通過Class.forName去查找這個類。TC_OBJECT = b'\x73'
標記后面的數據為Object對象TC_STRING = b'\x74'
標記后面的數據字符串。與基本類型中字符串的區別在於,這里面讀取的字符串將會被緩存,如果出現第二個一模一樣的字符串,則通過reference的方式,直接讀取緩存中的字符串TC_ARRAY = b'\x75'
標記后面的數據為數組類型TC_BLOCKDATA = b'\x77'
在對象的WriteObject方法中,我們可以自定義的寫入數據,除了非Object數據,其他所有數據將會被寫在一起,也就是BlockData。當然,只有readObject方法中,合適的讀取順序才可以成功還原blockdata。TC_ENDBLOCKDATA = b'\x78'
在readObject中,表明數據已經讀取完畢TC_EXCEPTION = b'\x7B'
表明后面需要讀取一個exception類型的對象TC_PROXYCLASSDESC = b'\x7D'
讀取一個動態代理的對象
3 還原反序列化流
有了上面的知識作為基礎,下面我們嘗試還原反序列化流中的內容。當然我們並沒有按照某些特定的順序去講解。
3.1 還原類的描述符(ClassDesc
類的描述符為序列化協議中的基石,它表明了后面的數據類型以及讀取方法。類的描述符,一共有以下幾個字段
- name 字符串類型,類的名稱
- suid long類型,類的suid,為了防止兼容性而設置的一個值。同一類的不同版本可能suid不一直,這樣防止不兼容的情況發生
- flag 表明類是否為反序列化,是否存在writeObject方法,也就是額外寫入數據等等
- field 類中包含的數據類型列表,
- 父類,父類也是類的描述符
- 類的額外信息
field
這里只包含類的數據類型的名稱與類型,不包括值。一定注意
讀取父類
由於java不支持多繼承,所以在這里只有兩種情況,繼承自一個父類和沒有父類這兩種情況。在這里我們只需要遞歸讀取,直到控制指令為TC_NULL,即父類為空,作為結束遞歸的條件。
類的描述符,記得要緩存。計算handle的時候,后面的值可能會引用該類的描述符
下面用一圖總結類的描述符的的協議結構
| utf 類名|4Byte suid|1Byte flags|2Byte 字段數量|字段詳細內容|父類|類的額外數據|
3.2 還原數組(TC_ARRAY
我們知道,java的數組中的內容只能為同一類型。所以在處理數組信息的時候,首先讀取類描述符,表明數組中的內容的數據類型。然后讀取數組的長度。最后按照數組長度以及數組類型,去讀取並還原數組中的數據。
|ClassDesc|Int length|數組數據|
數組也會被緩存,並被計算為handle
3.3 還原對象
還原對象的數據其實特別簡單。首先讀取類的描述符,然后緊接着按照類描述符的字段讀取數據即可。所以在這里,一個byte出錯,將會導致后面的數據全部讀取錯誤。
在這里需要注意幾點
- 首先讀取父類中字段的值,然后再讀子類的字段值,在這里我們使用棧數據結構去解決讀取
- 如果父類包含額外信息(例如writeObject寫入),則首先讀取父類包含的額外信息,再去讀取子類的額外信息
- 如果對象繼承自EXTERNALIZABLE接口,則無法單純通過流中數據還原對象中的值。因為java將不會負責字段值的讀取寫入,這一切都由開發人員決定哪些字段被保存以及保存的方法。這也就是weblogic中反序列化觸發XXE的漏洞原理,出現問題的類基本都繼承自EXTERNALIZABLE接口,且通過xml定義被保存的對象
- 如果對象繼承自Serializable接口,且存在writeObject方法。當writeObject方法中沒有通過ObjectOutputStream.defaultWriteObject將類的默認字段寫入到序列化流中,也無法還原對象的值。原因在於,反序列化中讀取類的值按照類的字段順序去讀取,如果沒有調用defaultWriteObject寫入,則相當於順序不可知,也是無法單純通過流中數據去還原對象
當然,對象中字段的值有可能還是對象,需要遞歸讀取,直到讀取所有的字段為基礎數據類型。在這里建議設置遞歸的最大深度,防止出現爆棧的異常。
4. 總結
目前完成各種復雜對象的讀取,例如x友的exp讀取,weblogic 反序列化Exp的讀取,並轉換為json。不過代碼還未寫完,地址暫時為https://github.com/potats0/javaSerializationDump/blob/main/main.py
截圖如下,讀取x友exp
x友exp轉json的結果
https://gist.github.com/potats0/108fe530350d14b11aba79736d6a3f0c#file-ncexp-json
下篇文章將談一下如何將json轉為序列化文件以及相關數據結構的設計。