jdk解決反序列化的方法

翻譯自: https://access.redhat.com/blogs/766093/posts/3135411
翻譯: 聶心明

Java反序列化漏洞已經是過去兩年安全圈裏面最熱的流行詞了,因爲每一個使用原始java序列化的框架都會受到反序列化攻擊。一開始,還有很多不同的方法去試圖解決這個問題的。( https://github.com/kantega/notsoserialhttps://github.com/Contrast-Security-OSS/contrast-rO0https://github.com/mbechler/serianalyzer )。這篇文章着重講java反序列化漏洞和解釋oracle在最新的jdk中提供了怎樣的緩解措施。

背景

讓我們回顧java反序列化的進程。java序列化 ( https://docs.oracle.com/javase/7/docs/platform/serialization/spec/serialTOC.html ) 是Java內置的功能,這個功能可以把java對象轉換成二進制數據,也可以把二進制數據轉換成對象。通過調用serialization 把對象轉換成二進制數據,通過調用deserialization把二進制數據轉換成java對象。在企業環境中,能直接存儲和恢復對象的狀態是構建分佈式系統的關鍵因素。比如,JMS消息隊列系統 ( https://en.wikipedia.org/wiki/Java_Message_Service ) 通過序列化把流對象數據通過通信線路送到目的地。 RESTful ( https://docs.oracle.com/javaee/6/tutorial/doc/gijqy.html ) 客戶端應用可能通過序列化把 OAuth token ( https://www.oauth.com/oauth2-servers/access-tokens/ ) 對象存儲在硬盤上,以便做進一步的身份驗證。java的遠程方法調用JMI ,( https://docs.oracle.com/javase/7/docs/platform/rmi/spec/rmiTOC.html )在JVM之間直接使用序列化互相通信。除了這些還有其他使用序列化的例子。

image

檢查流

當應用代碼觸反序列化的時候, ObjectInputStream ( https://docs.oracle.com/javase/8/docs/api/java/io/ObjectInputStream.html )將對象流數據初始化爲對象。ObjectInputStream 確保恢復已序列化的對象。在這個過程中,ObjectInputStream將字節流與JVM類路徑中可用的類進行匹配。
image

所以發生了什麼問題?

在反序列化過程中,當readObject()把二進制數據轉換成對象結構的時候,它會尋找序列化流中
與對象類型相關的魔術字節,這些對象類型通常被寫入流中,或者是那些已被定義的類型(比如:enum, array, String,等)。在處理流數據時,上面提到的對象類型需要被解析,如果對象類型無法被解析,這種類型就會被解析成爲一般類型TC_OBJECT ( https://docs.oracle.com/javase/7/docs/api/java/io/ObjectStreamConstants.html#TC_OBJECT ),最終,二進制數據流中所攜帶的對象將從JVM類路徑中恢復,如果沒有找到相關的類,就會報錯。
問題出現的地方是,給readObject()提供一個字節流,此字節流可以被構造成特殊的類,這個類存在於JVM的類路徑中,並且可以被使用,這篇文章列舉了已知的利用鏈,這些利用鏈可以造成遠程命令執行。所以有大量的類 ( https://github.com/kantega/notsoserial )被認爲有rce漏洞。並且,安全研究員不斷髮現有此類漏洞的類。現在你可能會問,怎麼有這麼多類用於rce?依靠這些原始類就可以構造特定惡意的類,從而實現攻擊,這些惡意的類被序列化並且這些數據在各個點被交換,被解析,被執行。實現攻擊的技巧是JDK信任二進制數據流,payload通過有效的初始化類來反序列化。這樣構造payload就會造成毀滅性的後果。
image
當然攻擊者就可以通過輸入二進制流來達到攻擊的目的,其中的詳細信息超出了本文的範圍。要想得到更詳細的信息可以參考ysoserial ( https://github.com/frohoff/ysoserial ) 這個工具,這大概是生成payload最好的工具了吧。

怎樣緩解反序列化攻擊

輕率的說,通過實現 LookAheadObjectInputStream ( https://www.owasp.org/index.php/Deserialization_Cheat_Sheet#Guidance_on_Deserializing_Objects_Safely ) 策略就可以完全緩解反序列化漏洞。緩解的實現方法是寫一個ObjectInputStream 的子類,這個子類要重寫 resolveClass() ( https://docs.oracle.com/javase/7/docs/api/java/io/ObjectInputStream.html#resolveClass(java.io.ObjectStreamClass) ),並在這個方法中驗證一個類是否能被加載。這個方法看上去能有效地緩解反序列化漏洞,最常見的兩種實現方法是白名單和黑名單 ( https://www.schneier.com/blog/archives/2011/01/whitelisting_vs.html ) 。在白名單中,只能讓可接受的類被反序列化解析,其他的類會被阻止。黑名單則是收集已知會造成問題的類,然後把它們全部阻止。
白名單和黑名單都有自己的優點和缺點,但是我認爲基於白名單的實現方法能更好的緩解反序列化漏洞,它能有效的識別那些安全的輸入,這種做法也是安全實踐的一部分。另一個方面,基於黑名單的方式很容易讓名單變的越來越大,而且黑名單還會有覆蓋不全和被繞過的情況。

protected Class<?> resolveClass(ObjectStreamClass desc)
                throws IOException, ClassNotFoundException {
      String name = desc.getName();

      if(isBlacklisted(name) ) {
              throw new SecurityException("Deserialization is blocked for security reasons");
      }

      if(isWhitelisted(name) ) {
              throw new SecurityException("Deserialization is blocked for security reasons");
      }

      return super.resolveClass(desc);
}

jdk中新的反序列化過濾方案

儘管有一些特別的實現來緩解反序列化漏洞帶來的影響,但是關於如何解決這樣的問題,官方的規範依然很匱乏。爲了解決這個問題,Oracle 最近引進 serialization filtering ( http://openjdk.java.net/jeps/290 )來提高反序列化的安全性,它似乎結合了黑名單和白名單兩種方式。新的反序列化過濾器被集成在JDK 9之中,然鵝,這個特性已經被移植到更老的JDK之中了。
核心原理是,反序列化過濾基於 ObjectInputFilter ( https://docs.oracle.com/javase/9/docs/api/java/io/ObjectInputFilter.html )接口,這個接口提供一種配置能力,目的是在反序列化過程中驗證輸入的數據。通過ObjectInputFilter接口參數: Status.ALLOWED ( http://download.java.net/java/jdk9/docs/api/java/io/ObjectInputFilter.Status.html#ALLOWED ), Status.REJECTED ( http://download.java.net/java/jdk9/docs/api/java/io/ObjectInputFilter.Status.html#REJECTED )或者 Status.UNDECIDED ( http://download.java.net/java/jdk9/docs/api/java/io/ObjectInputFilter.Status.html#UNDECIDED ) 去檢查輸入數據的狀態。依靠反序列化腳本可以配置這些參數,比如,如果想用黑名單的形式,那麼遇到一些特殊的類就要返回Status.REJECTED,並且如果返回Status.UNDECIDED的話,就是允許反序列化。另外一方面,如果想用白名單的形式,那麼當返回Status.ALLOWED的時候,就代表匹配到了白名單裏面的類。此外,過濾器也被允許訪問一些反序列化數據中的一些其他信息,比如,在反序列化過程中類數組中數組的長度 arrayLength ( https://docs.oracle.com/javase/9/docs/api/java/io/ObjectInputFilter.FilterInfo.html#arrayLength-- ),每一個內置對象的深度 depth ( https://docs.oracle.com/javase/9/docs/api/java/io/ObjectInputFilter.FilterInfo.html#depth-- ),當前對象的引用數量 references ( https://docs.oracle.com/javase/9/docs/api/java/io/ObjectInputFilter.FilterInfo.html#references-- ),當前二進制流佔用空間的大小 streamBytes ( https://docs.oracle.com/javase/9/docs/api/java/io/ObjectInputFilter.FilterInfo.html#streamBytes-- )。這些提供了關於輸入流更多的細粒度信息,並且在每一次匹配中都會返回相應的狀態。

如何配置過濾器

jdk 9 支持三種方式配置過濾器: custom filter ( http://openjdk.java.net/jeps/290 ), 也可以使用process-wide filter ( http://openjdk.java.net/jeps/290 ) 配置全局的過濾器,built-in filters ( http://www.oracle.com/technetwork/java/javase/8u121-relnotes-3315208.html ) 專門用於RMI,現在習慣用 Distributed Garbage Collection (DGC) ( https://docs.oracle.com/javase/8/docs/platform/rmi/spec/rmi-arch4.html

基於場景的過濾器

當自己的反序列化的場景和普通場景的反序列化方式不同時,那麼自定義過濾器(custom filter)這個方案就非常合適。通常通過實現ObjectInputFilter 接口和重寫checkInput函數來創建自定義過濾器。

static class VehicleFilter implements ObjectInputFilter {
        final Class<?> clazz = Vehicle.class;
        final long arrayLength = -1L;
        final long totalObjectRefs = 1L;
        final long depth = 1l;
        final long streamBytes = 95L;

        public Status checkInput(FilterInfo filterInfo) {
            if (filterInfo.arrayLength() < this.arrayLength || filterInfo.arrayLength() > this.arrayLength
                    || filterInfo.references() < this.totalObjectRefs || filterInfo.references() > this.totalObjectRefs
                    || filterInfo.depth() < this.depth || filterInfo.depth() > this.depth || filterInfo.streamBytes() < this.streamBytes
                    || filterInfo.streamBytes() > this.streamBytes) {
                return Status.REJECTED;
            }

            if (filterInfo.serialClass() == null) {
                return Status.UNDECIDED;
            }

            if (filterInfo.serialClass() != null && filterInfo.serialClass() == this.clazz) {
                return Status.ALLOWED;
            } else {
                return Status.REJECTED;
            }
        }
    }

jdk 9 還在ObjectInputStream 類中添加兩個函數,目的是讓過濾器能set/get當前的數據流。

public class ObjectInputStream
    extends InputStream implements ObjectInput, ObjectStreamConstants {

    private ObjectInputFilter serialFilter;
    ...
    public final ObjectInputFilter getObjectInputFilter() {
        return serialFilter;
    }

    public final void setObjectInputFilter(ObjectInputFilter filter) {
        ...
        this.serialFilter = filter;
    }
    ...
} 

與jdk 9 相反,JDK 8最新的版本( 1.8.0_144 ) 似乎只允許使用ObjectInputFilter.Config.setObjectInputFilter來設置過濾器。

Process-wide (全局)過濾器

通過設置 jdk.serialFilter ( https://docs.oracle.com/javase/9/docs/api/java/io/ObjectInputFilter.Config.html ) 來配置Process-wide過濾器,這樣的配置也可以作爲系統屬性( https://docs.oracle.com/javase/tutorial/essential/environment/sysprop.html ) 或者 安全屬性 ( http://docs.oracle.com/javase/7/docs/technotes/guides/security/PolicyFiles.html )。如果系統屬性被定義,那麼它常常配置的是過濾器;否則,過濾器就要根據安全屬性(比如:jdk1.8.0_144/jre/lib/security/java.security )來配置過濾器了。
jdk.serialFilter的值作爲過濾規則,過濾器通過檢查類的名字或者限制輸入二進制流的內容來達到過濾的目的。可以用逗號和空格來分割過濾規則。數據流在被檢查之前會被過濾,過濾器會忽略配置的順序。下面是過濾器的一般配置示例

- maxdepth=value // the maximum depth of a graph
- maxrefs=value // the maximum number of the internal references
- maxbytes=value // the maximum number of bytes in the input stream
- maxarray=value // the maximum array size allowed

其他的規律也會匹配由Class.getName()返回的類名和包名。類名和包名的規則也接受星號(*),雙星號( ** ),句號 ( . ) 和斜槓 ( / )。下面是一些可能發生的場景

// this matches a specific class and rejects the rest
"jdk.serialFilter=org.example.Vehicle;!*" 

 // this matches all classes in the package and all subpackages and rejects the rest 
- "jdk.serialFilter=org.example.**;!*" 

// this matches all classes in the package and rejects the rest 
- "jdk.serialFilter=org.example.*;!*" 

 // this matches any class with the pattern as a prefix
- "jdk.serialFilter=*;

內置過濾器

jdk 9 也引進了一個內置的過濾器,配置這個過濾器主要用於RMI和Distributed Garbage Collection (DGC) 。RMI Registry 和 DGC的內置過濾器是白名單的形式,白名單包含了服務器能夠執行的類。下面是 RMIRegistryImpl 和 DGCImp的白名單類

RMIRegistryImpl

java.lang.Number
java.rmi.Remote
java.lang.reflect.Proxy
sun.rmi.server.UnicastRef
sun.rmi.server.RMIClientSocketFactory
sun.rmi.server.RMIServerSocketFactory
java.rmi.activation.ActivationID
java.rmi.server.UID

DGCImpl

java.rmi.server.ObjID
java.rmi.server.UID
java.rmi.dgc.VMID
java.rmi.dgc.Lease

除了這些類,用戶也可以用sun.rmi.registry.registryFilter和sun.rmi.transport.dgcFilter 添加自己的過濾器,系統和安全屬性的配置和上文所提到的配置是一致的。

結語

然而,java反序列化不是它自己的漏洞,使用序列化框架反序列化不信任的數據纔是問題所在。這兩點的不同非常重要,因爲後者是因爲糟糕的程序設計而引入的漏洞,而不是java本身的問題。在JEP 290 ( http://openjdk.java.net/jeps/290 )之前的反序列化框架,根本不會驗證對象的合法性。而且現在有大量的方法去緩和反序列化漏洞,在JDK本身中沒有具體的規範來處理這個缺陷。但是在新版的JEP 290中,Oracle引入了新的過濾機制,這個機制允許開發人員結合自己的應用場景來配置自己的過濾器。新的過濾機制似乎能更容易的緩解反序列化那些不被信任的輸入數據所帶來的問題。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章