應用安全:JAVA反序列化漏洞之殤
概述
- 序列化是讓Java對象脫離Java運行環境的一種手段,可以有效的實現多平臺之間的通信、對象持久化存儲。
- Java 序列化是指把 Java 對象轉換為字節序列的過程便于保存在內存、文件、數據庫中,ObjectOutputStream類的 writeObject() 方法可以實現序列化。反序列化是指把字節序列恢復為 Java 對象的過程,ObjectInputStream 類的 readObject() 方法用于反序列化。
漏洞成因
-
序列化和反序列化本身并不存在問題。但當輸入的反序列化的數據可被用戶控制,那么攻擊者即可通過構造惡意輸入,讓反序列化產生非預期的對象,在此過程中執行構造的任意代碼。
-
漏洞代碼示例如下:
-
...... //讀取輸入流,并轉換對象 InputStream in=request.getInputStream(); ObjectInputStream ois = new ObjectInputStream(in); //恢復對象 ois.readObject(); ois.close();
-
這里特別要注意的是非預期的對象,正因為此java標準庫及大量第三方公共類庫成為反序列化漏洞利用的關鍵。安全研究人員已經發現大量利用反序列化漏洞執行任意代碼的方法,最讓大家熟悉的是Gabriel Lawrence和Chris Frohoff在《Marshalling Pickles how deserializing objects can ruin your day》中提出的利用Apache Commons Collection實現任意代碼執行。此后安全研究人員也陸續爆出XML、Json、Yaml等反序列化的相關漏洞。
-
除了commons-collections 3.1可以用來利用java反序列化漏洞,還有更多第三方庫同樣可以用來利用反序列化漏洞并執行任意代碼,部分如下:
-
commons-fileupload 1.3.1
-
commons-io 2.4
-
commons-collections 3.1
-
commons-logging 1.2
-
commons-beanutils 1.9.2
-
org.slf4j:slf4j-api 1.7.21
-
com.mchange:mchange-commons-java 0.2.11
-
org.apache.commons:commons-collections 4.0
-
com.mchange:c3p0 0.9.5.2
-
org.beanshell:bsh 2.0b5
-
org.codehaus.groovy:groovy 2.3.9
-
……
Java反序列化詳解
序列化數據結構
-
通過查看序列化后的數據,可以看到反序列化數據開頭包含兩字節的魔術數字,這兩個字節始終為十六進制的0xAC ED。接下來是兩字節的版本號0x00 05的數據。此外還包含了類名、成員變量的類型和個數等。
-
這里以類SerialObject示例來詳細進行介紹Java對象序列化后的數據結構:
-
public class SerialObject implements Serializable{private static final long serialVersionUID = 5754104541168322017L;private int id;public String name;public SerialObject(int id,String name){this.id=id;this.name=name;}... }
-
序列化SerialObject實例后以二進制格式查看:
-
00000000: aced 0005 7372 0024 636f 6d2e 7878 7878 ....sr.$com.xxxx 00000010: 7878 2e73 6563 2e77 6562 2e68 6f6d 652e xx.sec.web.home. 00000020: 5365 7269 616c 4f62 6a65 6374 4fda af97 SerialObjectO... 00000030: f8cc c5e1 0200 0249 0002 6964 4c00 046e .......I..idL..n 00000040: 616d 6574 0012 4c6a 6176 612f 6c61 6e67 amet..Ljava/lang 00000050: 2f53 7472 696e 673b 7870 0000 07e1 7400 /String;xp....t. 00000060: 0563 7279 696e 0a .cryin.
-
序列化的數據流以魔術數字和版本號開頭,這個值是在調用ObjectOutputStream序列化時,由writeStreamHeader方法寫入:
-
protected void writeStreamHeader() throws IOException {bout.writeShort(STREAM_MAGIC);//STREAM_MAGIC (2 bytes) 0xACEDbout.writeShort(STREAM_VERSION);//STREAM_VERSION (2 bytes) 5}
-
序列化后的SerialObject對象詳細結構:
-
STREAM_MAGIC (2 bytes) 0xACED STREAM_VERSION (2 bytes) 0x0005TC_OBJECT (1 byte) 0x73TC_CLASSDESC (1 byte) 0x72classNamelength (2 bytes) 0x24 = 36text (36 bytes) com.xxxxxx.sec.web.home.SerialObjectserialVersionUID (8 bytes) 0x4FDAAF97F8CCC5E1 = 5754104541168322017classDescInfoclassDescFlags (1 byte) 0x02 = SC_SERIALIZABLEfieldscount (2 bytes) 2field[0]primitiveDescprim_typecode (1 byte) I = integerfieldNamelength (2 bytes) 2text (2 bytes) idfield[1]objectDescobj_typecode (1 byte) L = objectfieldNamelength (2 bytes) 4text (4 bytes) nameclassName1TC_STRING (1 byte) 0x74length (2 bytes) 0x12 = 18text (18 bytes) Ljava/lang/String;classAnnotationTC_ENDBLOCKDATA (1 byte) 0x78superClassDescTC_NULL (1 byte) 0x70classdata[]classdata[0] (4 bytes) 0xe107 = id = 2017classdata[1]TC_STRING (1 byte) 0x74length (2 bytes) 5text (10 bytes) cryin
反序列化過程詳解
- Java程序中類ObjectInputStream的readObject方法被用來將數據流反序列化為對象,如果流中的對象是class,則它的ObjectStreamClass描述符會被讀取,并返回相應的class對象,ObjectStreamClass包含了類的名稱及serialVersionUID。
- 如果類描述符是動態代理類,則調用resolveProxyClass方法來獲取本地類。如果不是動態代理類則調用resolveClass方法來獲取本地類。如果無法解析該類,則拋出ClassNotFoundException異常。
- 如果反序列化對象不是String、array、enum類型,ObjectStreamClass包含的類會在本地被檢索,如果這個本地類沒有實現java.io.Serializable或者externalizable接口,則拋出InvalidClassException異常。因為只有實現了Serializable和Externalizable接口的類的對象才能被序列化。
反序列化漏洞檢測方案
代碼審計
-
反序列化操作一般在導入模版文件、網絡通信、數據傳輸、日志格式化存儲、對象數據落磁盤或DB存儲等業務場景,在代碼審計時可重點關注一些反序列化操作函數并判斷輸入是否可控,如下:
-
ObjectInputStream.readObject ObjectInputStream.readUnshared XMLDecoder.readObject Yaml.load XStream.fromXML ObjectMapper.readValue JSON.parseObject ...
-
同時也要關注第三jar包是否提供了一些公共的反序列化操作接口,如果沒有相應的安全校驗如白名單校驗方案,且輸入可控的話就也可能存在安全問題。
進階審計
-
對于直接獲取用戶輸入進行反序列化操作這種點比較好審計并發現,目前反序列化漏洞已經被談起太多次了,所以有經驗的開發都會在代碼中有相應的修復。但并不是所有修復都無懈可擊。比如采用黑名單校驗的修復方式,對于這種修復可在工程代碼中嘗試挖掘新的可以利用的’gadget‘。
-
代碼中有使用到反序列化操作,那自身項目工程中肯定存在可以被反序列化的類,包括Java自身、第三方庫有大量這樣的類,可被反序列化的類有一個特點,就是該類必定實現了Serializable接口,Serializable 接口是啟用其序列化功能的接口,實現 java.io.Serializable 接口的類才是可序列化的。一個典型的示例如下:
-
public class SerialObject implements Serializable{private static final long serialVersionUID = 5754104541168322017L;private int id;public String name;public SerialObject(int id,String name){this.id=id;this.name=name;}public void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{//執行默認的readObject()方法in.defaultReadObject();} }
-
所以在代碼審計時對這些類也可進行特別關注,分析并確認是否有可能被發序列化漏洞利用執行任意代碼。發現新的可利用的類即可突破使用黑名單進行校驗的一些應用。
白盒檢測
- 大型企業的應用很多,每個都人工去審計不現實,往往都有相應的自動化靜態代碼審計工具,這里以ObjectInputStream.readObject()為例,其它反序列化接口的檢測原理也相似。在自動化檢測時,可通過實現解析java源代碼,檢測readObject()方法調用時判斷其對象是否為java.io.ObjectOutputStream。如果此時ObjectInputStream對象的初始化參數來自外部請求輸入參數則基本可以確定存在反序列化漏洞了。這是只需確認是否存在相應的安全修復即可。 可參考lgtm.com對于Deserialization of user-controlled data檢測方式的實現。
黑盒檢測
-
調用ysoserial并依次生成各個第三方庫的利用payload(也可以先分析依賴第三方包量,調用最多的幾個庫的paylaod即可),該payload構造為訪問特定url鏈接的payload,根據http訪問請求記錄判斷反序列化漏洞是否利用成功。如:
-
java -jar ysoserial.jar CommonsCollections1 'curl " + URL + " '
-
也可通過DNS解析記錄確定漏洞是否存在。現成的輪子很多,推薦NickstaDB寫的SerialBrute,還有一個針對RMI的測試工具BaRMIe,也很不錯~。.
RASP檢測
-
Java程序中類ObjectInputStream的readObject方法被用來將數據流反序列化為對象,如果流中的對象是class,則它的ObjectStreamClass描述符會被讀取,并返回相應的class對象,ObjectStreamClass包含了類的名稱及serialVersionUID。
-
類的名稱及serialVersionUID的ObjectStreamClass描述符在序列化對象流的前面位置,且在readObject反序列化時首先會調用resolveClass讀取反序列化的類名,所以RASP檢測反序列化漏洞時可通過重寫ObjectInputStream對象的resolveClass方法獲取反序列化的類即可實現對反序列化類的黑名單校驗。
-
百度的開源RASP產品就是使用的這種方法,具體可參考其DeserializationHook.java的實現:
-
@Overrideprotected MethodVisitor hookMethod(int access, String name, String desc,String signature, String[] exceptions, MethodVisitor mv) {if ("resolveClass".equals(name) && "(Ljava/io/ObjectStreamClass;)Ljava/lang/Class;".equals(desc)) {return new AdviceAdapter(Opcodes.ASM5, mv, access, name, desc) {@Overrideprotected void onMethodEnter() {loadArg(0);invokeStatic(Type.getType(HookHandler.class),new Method("checkDeserializationClass", "(Ljava/io/ObjectStreamClass;)V"));}};}return mv;}
-
其中檢測覆蓋的反序列化類黑名單如下:
-
plugin.register('deserialization', function (params, context) {var deserializationInvalidClazz = ['org.apache.commons.collections.functors.InvokerTransformer','org.apache.commons.collections.functors.InstantiateTransformer','org.apache.commons.collections4.functors.InvokerTransformer','org.apache.commons.collections4.functors.InstantiateTransformer','org.codehaus.groovy.runtime.ConvertedClosure','org.codehaus.groovy.runtime.MethodClosure','org.springframework.beans.factory.ObjectFactory','xalan.internal.xsltc.trax.TemplatesImpl']var clazz = params.clazzfor (var index in deserializationInvalidClazz) {if (clazz === deserializationInvalidClazz[index]) {return {action: 'block',message: '嘗試反序列化攻擊',confidence: 100}}}return clean })
攻擊檢測
-
通過查看反序列化后的數據,可以看到反序列化數據開頭包含兩字節的魔術數字,這兩個字節始終為十六進制的0xAC ED。接下來是兩字節的版本號。我只見到過版本號為5(0x00 05)的數據。考慮到zip、base64各種編碼,在攻擊檢測時可針對該特征進行匹配請求post中是否包含反序列化數據,判斷是否為反序列化漏洞攻擊。
-
xxxdeMacBook-Pro:demo xxx$ xxd objectexp 00000000: aced 0005 7372 0032 7375 6e2e 7265 666c ....sr.2sun.refl00000010: 6563 742e 616e 6e6f 7461 7469 6f6e 2e41 ect.annotation.A00000020: 6e6e 6f74 6174 696f 6e49 6e76 6f63 6174 nnotationInvocat00000030: 696f 6e48 616e 646c 6572 55ca f50f 15cb ionHandlerU.....
-
但僅從特征匹配只能確定有攻擊嘗試請求,還不能確定就存在反序列化漏洞,還要結合請求響應、返回內容等綜合判斷是否確實存在漏洞。
Java反序列化漏洞修復方案
通過Hook resolveClass來校驗反序列化的類
-
通過上面序列化數據結構可以了解到包含了類的名稱及serialVersionUID的ObjectStreamClass描述符在序列化對象流的前面位置,且在readObject反序列化時首先會調用resolveClass讀取反序列化的類名,所以這里通過重寫ObjectInputStream對象的resolveClass方法即可實現對反序列化類的校驗。這個方法最早是由IBM的研究人員Pierre Ernst在2013年提出《Look-ahead Java deserialization》,具體實現代碼示例如下:
-
public class AntObjectInputStream extends ObjectInputStream{public AntObjectInputStream(InputStream inputStream)throws IOException {super(inputStream);}/*** 只允許反序列化SerialObject class*/@Overrideprotected Class<?> resolveClass(ObjectStreamClass desc) throws IOException,ClassNotFoundException {if (!desc.getName().equals(SerialObject.class.getName())) {throw new InvalidClassException("Unauthorized deserialization attempt",desc.getName());}return super.resolveClass(desc);} }
-
通過此方法,可靈活的設置允許反序列化類的白名單,也可設置不允許反序列化類的黑名單。但反序列化漏洞利用方法一直在不斷的被發現,黑名單需要一直更新維護,且未公開的利用方法無法覆蓋。
-
SerialKiller 是由Luca Carettoni利用上面介紹的方法實現的反序列化類白/黑名單校驗的jar包。具體使用方法可參考其代碼倉庫。
-
contrast-rO0是一個輕量級的agent程序,通過通過重寫ObjectInputStream來防御反序列化漏洞攻擊。使用其中的SafeObjectInputStream類來實現反序列化類白/黑名單控制,示例代碼如下:
-
SafeObjectInputStream in = new SafeObjectInputStream(inputStream, true); in.addToWhitelist(SerialObject.class);in.readObject();
使用ValidatingObjectInputStream來校驗反序列化的類
-
使用Apache Commons IO Serialization包中的ValidatingObjectInputStream類的accept方法來實現反序列化類白/黑名單控制,具體可參考ValidatingObjectInputStream介紹;示例代碼如下:
-
private static Object deserialize(byte[] buffer) throws IOException, ClassNotFoundException , ConfigurationException {Object obj;ByteArrayInputStream bais = new ByteArrayInputStream(buffer);// Use ValidatingObjectInputStream instead of InputStreamValidatingObjectInputStream ois = new ValidatingObjectInputStream(bais); //只允許反序列化SerialObject classois.accept(SerialObject.class);obj = ois.readObject();return obj; }
使用ObjectInputFilter來校驗反序列化的類
-
Java 9包含了支持序列化數據過濾的新特性,開發人員也可以繼承java.io.ObjectInputFilter類重寫checkInput方法實現自定義的過濾器,,并使用ObjectInputStream對象的setObjectInputFilter設置過濾器來實現反序列化類白/黑名單控制。示例代碼如下:
-
import java.util.List; import java.util.Optional; import java.util.function.Function; import java.io.ObjectInputFilter; class BikeFilter implements ObjectInputFilter {private long maxStreamBytes = 78; // Maximum allowed bytes in the stream.private long maxDepth = 1; // Maximum depth of the graph allowed.private long maxReferences = 1; // Maximum number of references in a graph.@Overridepublic Status checkInput(FilterInfo filterInfo) {if (filterInfo.references() < 0 || filterInfo.depth() < 0 || filterInfo.streamBytes() < 0 || filterInfo.references() > maxReferences || filterInfo.depth() > maxDepth|| filterInfo.streamBytes() > maxStreamBytes) {return Status.REJECTED;}Class<?> clazz = filterInfo.serialClass();if (clazz != null) {if (SerialObject.class == filterInfo.serialClass()) {return Status.ALLOWED;}else {return Status.REJECTED;}}return Status.UNDECIDED;} // end checkInput } // end class BikeFilter
-
上述示例代碼,僅允許反序列化SerialObject類對象,上述示例及更多關于ObjectInputFilter的均參考自NCC Group Whitepaper由Robert C. Seacord寫的《Combating Java Deserialization Vulnerabilities with Look-Ahead Object Input Streams (LAOIS)》
黑名單校驗修復
- 在反序列化時設置類的黑名單來防御反序列化漏洞利用及攻擊,這個做法在源代碼修復的時候并不是推薦的方法,因為你不能保證能覆蓋所有可能的類,而且有新的利用payload出來時也需要隨之更新黑名單。
- 但有某些場景下可能又不得不選擇黑名單方案。寫代碼的時候總會把一些經常用到的方法封裝到公共類,這樣其它工程中用到只需要導入jar包即可,此前已經見到很多提供反序列化操作的公共接口,使用第三方庫反序列化接口就不好用白名單的方式來修復了。這個時候作為第三方庫也不知道誰會調用接口,會反序列化什么類,所以這個時候可以使用黑名單的方式來禁止一些已知危險的類被反序列化,部分的黑名單類如下:
- org.apache.commons.collections.functors.InvokerTransformer
- org.apache.commons.collections.functors.InstantiateTransformer
- org.apache.commons.collections4.functors.InvokerTransformer
- org.apache.commons.collections4.functors.InstantiateTransformer
- org.codehaus.groovy.runtime.ConvertedClosure
- org.codehaus.groovy.runtime.MethodClosure
- org.springframework.beans.factory.ObjectFactory
- com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
- org.apache.commons.fileupload
- org.apache.commons.beanutils
- …
安全編碼建議
- 更新commons-collections、commons-io等第三方庫版本;
- 業務需要使用反序列化時,盡量避免反序列化數據可被用戶控制,如無法避免建議盡量使用白名單校驗的修復方式;