作者:Kingkk
原文鏈接:https://www.kingkk.com/2020/06/%E6%B5%85%E8%B0%88%E4%B8%8BFastjson%E7%9A%84autotype%E7%BB%95%E8%BF%87/
本文為作者投稿,Seebug Paper 期待你的分享,凡經采用即有禮品相送!
投稿郵箱:paper@seebug.org
繼去年1.2.47 Fastjson被繞過之后,最近的1.2.68又出現了繞過。
正好前段時間翻了一遍Fastjson的源碼,對整體邏輯有了一些了解,就嘗試分析下autotype的校驗過程,以及這兩次繞過的思路。若有錯誤,還望指出。
autotype的校驗
為什么校驗一直被繞過
1.2.24之后,fastjson對反序列化的類型進行了校驗,主要就體現在ParserConfig.checkAutoType函數中
里面會對反序列化的類型進行黑白名單和校驗,然后獲取對應的Java類。
至于為什么沒開啟SupportAutoType屬性依然會存在反序列化的危險呢?

可以看到在解析過程中,只要key值為@type時,就會進入checkAutoType函數嘗試獲取類。
而且校驗SupportAutoType屬性的工作卻是在checkAutoType函數中完成的(跟進之后也可以看到是在函數最末端調校驗的值,并且在這之前有多處return)
那為什么要有這種設計呢?主要原因在于fastjson想讓一些基礎類(還有一些白名單中的異常類)可以不受SupportAutoType限制就可以反序列化。
例如之前別人提出的驗證是否使用fastjson的java.net.Inet6Address、java.net.URL也都是這個原理。
可以看到,即使不開啟SupportAutoType依然是可以獲取到具體的java類的。

所以,這就是為什么校驗一直被繞過,感覺主要原因就在于為了實現這個feature,而導致的一些邏輯問題。
校驗過程
checkAutoType主要有三個參數
String typeName被序列化的類名Class<?> expectClass期望類int features配置的feature值
先簡單說下expectClass這個期望類,它的主要目的是為了讓一些實現了expectClass這個接口的類可以被反序列化。
然后來看下校驗的過程,一開始就是一些非null和長度限制的判斷
之后判斷exceptClass的類型,如果非null并且不是如下類型,則設置expectClassFlag為true
簡單說的話就是不允許如下類型的exceptClass
Object.classSerializable.classCloneable.classCloseable.classEventListener.classIterable.classCollection.class

之后比較長的一個部分就是比較類的哈希值,是否在內部白名單和內部黑名單中
如果在不在內部白名單并且 開啟了SupportAutoType 或者 存在期望類時:如果在白名單中則直接加載,在黑名單中則異常退出。(講起來有點繞,直接看代碼可能好點)
String className = typeName.replace('$', '.');
Class<?> clazz;
final long BASIC = 0xcbf29ce484222325L;
final long PRIME = 0x100000001b3L;
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
if ((h1 ^ className.charAt(className.length() - 1)) * PRIME == 0x9198507b5af98f0L) {
throw new JSONException("autoType is not support. " + typeName);
}
final long h3 = (((((BASIC ^ className.charAt(0))
* PRIME)
^ className.charAt(1))
* PRIME)
^ className.charAt(2))
* PRIME;
boolean internalWhite = Arrays.binarySearch(INTERNAL_WHITELIST_HASHCODES,
TypeUtils.fnv1a_64(className)
) >= 0;
if (internalDenyHashCodes != null) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(internalDenyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
hash ^= className.charAt(i);
hash *= PRIME;
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, true);
if (clazz != null) {
return clazz;
}
}
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
之后就是嘗試從各種地方去獲取class類

首先嘗試從TypeUtils的mappings中獲取對應類

里面原本就有一些類,而且后續會被當作已獲取類的緩存使用

然后是嘗試從deserializers.findClass中獲取class類
這里面的類主要是在ParserConfig.initDeserializers()中被賦值的。
也就相當于這些特殊類也可以被無條件的反序列化

然后就是嘗試從typeMapping中獲取對應類,這其中默認的值為空,需要開發人員自行賦值。
之后就是類在白名單中時(但幾乎不大可能),嘗試自動去加載類。
最后,如果通過以上方式可以加載到類,則校驗期望類,沒有問題的話就直接返回對應的class。
所以其實到這里,依然還沒有出現SupportAutoType的校驗,但已經可以返回類了(但正常情況下返回的一般都是程序中預先設置好的一些類,還不存在動態加載)。
然后就是在沒有開啟SupportAutoType時,通過黑白名單去校驗類,黑名單拋出異常,白名單加載類并返回。

之后的部分就是通過ASM的操作,去讀取類是否有JSONType的注解(有注解的類一般都是開發自行寫的JavaBean)

之后如果 開啟了SupportAutoType 或者 有JSONType的注解 或者 存在期望類,則會直接去加載對應類
成功加載類之后,如果有注解,則加入mapping緩存并直接返回
如果是繼承/實現了ClassLoader、DataSource 、RowSet這些類的話直接異常。
如果存在期望類,則需要加載的類是期望類的子類或實現,并直接返回,否則異常。
如果類指定了JSONCreator注解,并且開啟了SupportAutoType 則拋出異常。

最后,校驗了是否開啟SupportAutoType,然后將類添加至mapping緩存,并返回對應類。

到此就是checkAutoType的校驗與加載類的過程。
小結
可以看到雖然函數名是checkAutoType,但是其實這是一個校驗與加載類的過程。
而且真正的SupportAutoType校驗其實是被放到最后的,在這之前也存在許多加載類并返回類的地方,目的也就是一開始說的為了實現基礎類的任意反序列化的feature。
這也就意味著需要通過邏輯來保證在這之前返回的類都是安全的,但也正是因為這個原因導致了autotype被邏輯繞過。
可以看到主要有如下種情況可以直接返回class
acceptHashCodes白名單INTERNAL_WHITELIST_HASHCODES內部白名單TypeUtils.mappingsmappings緩存deserializers.findClass指定類typeMapping.get默認為空JsonType注解exceptClass存在期望類
1.2.47的繞過
主要分析思路,這回的繞過主要靠的是mappings緩存的繞過
根據之前分析的流程可以知道,當mappings緩存中存在指定類時,可以直接返回并且不受SupportAutoType的校驗。
在TypeUtils.loadClass中,如果參數中cache值為true時,則會在加載到類之后,將類加入mappings緩存

尋找所有調用了該函數,并且cache設置為true的只有它的重載函數,然后繼續尋找調用了該重載的地方

可以看到除了TypeUtils中,還有MiscCodec中調用了該方法

這里的邏輯是當class是一個java.lang.Class類時,會去加載指定類(從而也就無意之間加入了mappings緩存)

而java.lang.Class同時也是個默認特殊類,可以直接反序列化。

因此就可以首先通過反序列化java.lang.Class指定惡意類,然后惡意類被加入mappings緩存后,第二次就可以直接從緩存中獲取到惡意類,并進行反序列化。
Throwable和1.2.68的繞過
這兩個的繞過主要都是基于exceptClass期望類的feature特性。
之前分析的時候提到,期望類的功能主要是實現 繼承了期望類的class能被反序列化出來(并且不受autotype影響)
但是默認情況下exceptClass這個參數是空的,也就不存在期望類的特性。所以主要關注在程序內部別的地方的調用。
全局搜索一下可以看到主要有ThrowableDeserializer和JavaBeanDeserializer兩個類中有調用到。

先來說ThrowableDeserializer,它主要是對 Throwable異常類進行反序列化的。

在ThrowableDeserializer中可以根據第二個@type的值來獲取具體類,并且傳入指定期望類進行加載。

因此對一個異常類進行反序列化時,則可以依賴exceptClass期望類的特性去反序列化一個繼承異常類的class。
但沒有gadget時這也只能算作一個feature,本意也就是為了反序列化出異常類,并且異常類的限制其實比較苛刻。
其實一開始看淺藍師傅發了這個之后,自己也關注到了JavaBeanDeserializer中的期望類調用,然后開始嘗試看何種情況會調用JavaBeanDeserializer。
ParserConfig.getDeserializer中可以看到,其實JavaBeanDeserializer的優先級其實是最低的(通常情況下都是一些第三方類才會調用到這里)
當時就草草看了下一些默認的基礎類發現貌似沒有可以走到這部分邏輯的就沒整了(然后就被打臉了)。

1.2.68的繞過主要靠的就是AutoCloseable類,恰好fastjson沒有為它指定特定的deserializer,因此會走到最后的else條件,創建對應的JavaBeanDeserializer。并且它是默認在mappings緩存中的,可以無條件反序列化。
在JavaBeanDeserializer中也和之前一樣,會根據第二個@type的值去獲取對應的class
這里的exceptClass期望類也就是當前類AutoCloseable

而且相較于Throwable來說,AutoCloseable的范圍則會大得多,常用的流操作、文件、socket之類的都繼承了AutoCloseable接口。

之后的工作則是需要找一個gadget,但相較于1.2.47的繞過來說,exceptClass期望類的返回位置相對比較靠后。
因此會存在黑名單的校驗與ClassLoader、DataSource、RowSet的校驗。
也就意味著之前的gadget是都不能用了,要找一條新的基于AutoCloseable的gadget。
至于后面的利用FieldDeserializer去拓展gadget就不在這里展開說了。
最后
以我個人的分析來看,主要原因還是在于Fastjson為了維護最開始那些基礎類的無限制反序列化的特性。
導致即使開發人員關閉了SupportAutoType屬性,但并不能阻止所有反序列化的情況。
Fastjson內部也是通過邏輯來保證校驗前的返回類不會出現惡意類的情況,但是當整個項目變大之后,相互之間的調用會使得邏輯變得復雜,從而也就出現了邏輯繞過。
一次次的繞過和修復,對研究人員的代碼功底要求也比較高,這種相互之間的博弈也相當精彩,值得好好學習一番。
參考鏈接
本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/1236/