Java反序列化漏洞之殤

ref:https://xz.aliyun.com/t/2043php

小結:html

3.2.2版本以前的Apache-CommonsCollections存在該漏洞(不僅該包)
1.漏洞觸發場景
在java編寫的web應用與web服務器間java一般會發送大量的序列化對象例如如下場景:
  1)HTTP請求中的參數,cookies以及Parameters。
  2)RMI協議,被普遍使用的RMI協議徹底基於序列化
  4)JMX 一樣用於處理序列化對象
  5)自定義協議 用來接收與發送原始的java對象java

2. 漏洞挖掘
  (1)肯定反序列化輸入點
    首先應找出readObject方法調用,在找到以後進行下一步的注入操做。通常能夠經過如下方法進行查找:
     1)源碼審計:尋找能夠利用的「靶點」,即肯定調用反序列化函數readObject的調用地點。
     2)對該應用進行網絡行爲抓包,尋找序列化數據,如wireshark,tcpdump等。注: java序列化的數據通常會以標記(ac ed 00 05)開頭,base64編碼後的特徵爲rO0AB。
  (2)再考察應用的Class Path中是否包含Apache Commons Collections庫,這一點須要考慮?ios

           若是在頂層容器中例如tomcat/jetty等包含commcollectios3.1,而在業務中引用commcollections3.2,則仍可能觸發漏洞。git

 

關於反序列化漏洞分析及利用研究的文章很多,但鮮有檢測及修復方面的介紹,本文旨站在應用安全的角度,從安全編碼、代碼審計、漏洞檢測及修復方案對反序列化漏洞進行詳細分享。github

概述

序列化是讓Java對象脫離Java運行環境的一種手段,能夠有效的實現多平臺之間的通訊、對象持久化存儲。web

Java 序列化是指把 Java 對象轉換爲字節序列的過程便於保存在內存、文件、數據庫中,ObjectOutputStream類的 writeObject() 方法能夠實現序列化。反序列化是指把字節序列恢復爲 Java 對象的過程,ObjectInputStream 類的 readObject() 方法用於反序列化。spring

漏洞成因

序列化和反序列化自己並不存在問題。但當輸入的反序列化的數據可被用戶控制,那麼攻擊者便可經過構造惡意輸入,讓反序列化產生非預期的對象,在此過程當中執行構造的任意代碼。shell

漏洞代碼示例以下:數據庫

......
//讀取輸入流,並轉換對象
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) 0xACED
     bout.writeShort(STREAM_VERSION);//STREAM_VERSION (2 bytes) 5
    }

序列化後的SerialObject對象詳細結構:

STREAM_MAGIC (2 bytes) 0xACED 
STREAM_VERSION (2 bytes) 0x0005
    TC_OBJECT (1 byte) 0x73
        TC_CLASSDESC (1 byte) 0x72
        className
            length (2 bytes) 0x24 = 36
            text (36 bytes) com.xxxxxx.sec.web.home.SerialObject
        serialVersionUID (8 bytes) 0x4FDAAF97F8CCC5E1 = 5754104541168322017
        classDescInfo
            classDescFlags (1 byte) 0x02 = SC_SERIALIZABLE
            fields
                count (2 bytes) 2
                field[0]
                    primitiveDesc
                        prim_typecode (1 byte) I = integer
                        fieldName
                            length (2 bytes) 2
                            text (2 bytes) id
                field[1]
                    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) 0xe107 = id = 2017
        classdata[1]
            TC_STRING (1 byte) 0x74
            length (2 bytes) 5
            text (8 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
...

同時也要關注存在漏洞的第三方庫及版本是否安全。

進階審計

對於直接獲取用戶輸入進行反序列化操做這種點比較好審計並發現,目前反序列化漏洞已經被談起太屢次了,因此有經驗的開發都會在代碼中有相應的修復。但並非全部修復都無懈可擊。好比採用黑名單校驗的修復方式,對於這種修復可在工程代碼中嘗試挖掘新的能夠利用的’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的實現:

/**
 * @name Deserialization of user-controlled data
 * @description Deserializing user-controlled data may allow attackers to
 *              execute arbitrary code.
 * @kind problem
 * @problem.severity error
 * @precision high
 * @id java/unsafe-deserialization
 * @tags security
 *       external/cwe/cwe-502
 */
import java
import semmle.code.java.security.DataFlow
import semmle.code.java.frameworks.Kryo
import semmle.code.java.frameworks.XStream
import semmle.code.java.frameworks.SnakeYaml

class ObjectInputStreamReadObjectMethod extends Method {
  ObjectInputStreamReadObjectMethod() {
    this.getDeclaringType().getASourceSupertype*().hasQualifiedName("java.io", "ObjectInputStream") and
    (this.hasName("readObject") or this.hasName("readUnshared"))
  }
}

class XMLDecoderReadObjectMethod extends Method {
  XMLDecoderReadObjectMethod() {
    this.getDeclaringType().hasQualifiedName("java.beans", "XMLDecoder") and
    this.hasName("readObject")
  }
}

class SafeXStream extends FlowSource {
  SafeXStream() {
    any(XStreamEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() = this
  }
}

class SafeKryo extends FlowSource {
  SafeKryo() {
    any(KryoEnableWhiteListing ma).getQualifier().(VarAccess).getVariable().getAnAccess() = this
  }
}

predicate unsafeDeserialization(MethodAccess ma, Expr sink) {
  exists(Method m | m = ma.getMethod() |
    m instanceof ObjectInputStreamReadObjectMethod and
    sink = ma.getQualifier()
    or
    m instanceof XMLDecoderReadObjectMethod and
    sink = ma.getQualifier()
    or
    m instanceof XStreamReadObjectMethod and
    sink = ma.getAnArgument() and
    not exists(SafeXStream sxs | sxs.flowsTo(ma.getQualifier()))
    or
    m instanceof KryoReadObjectMethod and
    sink = ma.getAnArgument() and
    not exists(SafeKryo sk | sk.flowsTo(ma.getQualifier()))
    or
    ma instanceof UnsafeSnakeYamlParse and
    sink = ma.getArgument(0)
  )
}

class UnsafeDeserializationSink extends Expr {
  UnsafeDeserializationSink() {
    unsafeDeserialization(_, this)
  }
  MethodAccess getMethodAccess() { unsafeDeserialization(result, this) }
}

from UnsafeDeserializationSink sink, RemoteUserInput source
where source.flowsTo(sink)
select sink.getMethodAccess(), "Unsafe deserialization of $@.", source, "user input"

黑盒檢測

調用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的實現:

@Override
    protected 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) {
                @Override
                protected 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.clazz
    for (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.refl
    00000010: 6563 742e 616e 6e6f 7461 7469 6f6e 2e41  ect.annotation.A
    00000020: 6e6e 6f74 6174 696f 6e49 6e76 6f63 6174  nnotationInvocat
    00000030: 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
     */
    @Override
    protected 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 InputStream
    ValidatingObjectInputStream ois = new   ValidatingObjectInputStream(bais); 

    //只容許反序列化SerialObject class
    ois.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.
    @Override
    public 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等第三方庫版本;
  • 業務須要使用反序列化時,儘可能避免反序列化數據可被用戶控制,如沒法避免建議儘可能使用白名單校驗的修復方式;

總結

關於反序列化漏洞分析及利用研究的文章很多,但鮮有檢測及修復方面的介紹,本文旨站在應用安全的角度,從安全編碼、代碼審計、漏洞檢測及修復方案對反序列化漏洞進行詳細分享。但願對從事應用安全的朋友有所幫助。文中如有問題之處歡迎指出交流。

參考

相關文章
相關標籤/搜索