作者:Lucifaer
博客:https://www.lucifaer.com/

  • 其實最近爆出的這個rce在去年的時候就有更新,poc在github的commit記錄中也有所體現,之前已經有很多非常好的分析文章對整個漏洞進行了詳盡的分析,我這里只記錄一下自己的跟蹤過程,以及在跟蹤時所思考的一些問題。

0x01 Fastjson化流程簡述

廖大2017年的一篇博文中就對Fastjson的反序列化流程進行了總結:

img

在具體的跟進中也可以很清晰的看到如圖所示的架構。

對于編程人員來說,只需要考慮Fastjson所提供的幾個靜態方法即可,如:

  • JSON.toJSONString()
  • JSON.parse()
  • JSON.parseObject()

并不需要關注json序列化及反序列化的過程。深入Fastjson框架,可以看到其主要的功能都是在DefaultJSONParser類中實現的,在這個類中會應用其他的一些外部類來完成后續操作。ParserConfig主要是進行配置信息的初始化,JSONLexer主要是對json字符串進行處理并分析,反序列化在JavaBeanDeserializer中處理。

在真實的調試過程中會遇到一些非常好玩的問題,而在其它文章中并沒有對這些進行完整的敘述,我這里結合自己的理解來說一說。以下的調試的例子的demo為:

img

jsonString即為poc的內容:

  {"name":{"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"},"f":{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://asdfasfd/","autoCommit":true}},age:11}

poc(或者不如說是對于傳入的json字符串)的處理過程簡單來說分為這幾部分:

  1. DefaultJSONParser的初始化

  2. 這一步是parseObject()是否指定了第二個參數,也就是是否指定了clazz字段:

    • 如果指定了clazz字段,則首先根據clazz類型來獲取相應deserializer,如果不是initDeserializers中的類的話,則會調用JavaBeanDeserializer#deserialze轉交FastjsonASMDeserializer利用Fastjson自己實現的ASM流程生成處理類,調用相應的類并將處理流程轉交到相應的處理類處理json字符串內容。(這里的描述有一些些問題,后面會盡量相近的描述一下)
    • 如果未指定,則直接交給StringCodec類來處理json字符串。
  3. 最終都轉交由DefaultJSONParser#parse中根據lexer.token來選擇處理方式,這里的例子中都為12也就是{(因為要處理json字符串需要一個起始標志位,所以判斷當前json字符串的token是很重要的),接下來就是對json字符串進行處理(這里是一個循環處理,摘取類似"name":"123"這樣的關系)。

  4. 判斷解析的json字符串中是否存在symbolTable中的字段(如@type$ref這樣的字段),如果出現了@type則交由public final Object parseObject(final Map object, Object fieldName)來處理,然后重復步驟2的過程知道執行成功或報錯。

1.1 DefaultJSONParser的初始化過程

初始化過程非常的簡單,分兩部分,一部分為ParserConfig的初始化,另外一部分為DefaultJSONParser的初始化。

ParserConfig的初始化是在com.alibaba.fastjson.JSON中調用的:

img

一路跟到ParserConfig#ParserConfig方法中:

img

前面指定了asm的工廠類,并進行了實例化,后面是初始化deserializers,將用戶自定義黑白名單加入到原有的黑白名單中。

DefaultJSONParser的初始化是在com.alibaba.fastjson.JSON#parseObject中調用并完成的:

img

這里初始化了DefaultJSONParser之后調用了其parseObject方法進行后續的操作。

跟進DefaultJSONParser可以看到JSONScanner的實例化以及lexer.token的初始化設置:

img

1.2 獲取對應的derializer

進入到這里步就稍微有點復雜了,需要仔細跟進一下。根據上一節我們可以看到完成初始化操作后主要的處理流程集中于T value = (T) parser.parseObject(clazz, null);這一步的操作中,跟進看一下具體流程:

img

簡單來說就是一個根據type獲取對應的derializer并且調用derializer.deserialze進行處理的過程,這里的config是之前初始化的ParserConfig。這里要注意的是type這個參數,跟蹤了整個流程后會發現,如果在寫代碼時指定了第二個參數如Group group = JSON.parseObject(jsonString, Group.class);則第二個參數也就是Group.class即為type如果未指定第二個參數的話將會獲取第一個參數的類型作為type,當未指定第二個參數的時候將會調用與第一個參數類型相符的方法來處理:

img

了解了這些后,就可以跟進看一下getDeserializer的實現了:

img

首先會嘗試在deserializers中匹配type的類型,如果匹配到了就返回匹配的derializer,否則就判斷是否是Class泛型的接口,如果是則調用getDeserializer((Class<?>) type, type)繼續處理,這一部分代碼很長,我只截最關鍵的一個地方:

img

當類不顯式匹配上面的情況時,就會調用createJavaBeanDeserializer來創建一個新的derializer,并將其加入到deserializers這個map中。接下來跟進createJavaBeanDeserializer的處理流程,我截取了關鍵的一部分:

img

在這里首先會根據類名和propertyNamingStrategy生成beanInfo,之后利用asm工廠類的createJavaBeanDeserializer生成處理類:

img

寫過asm的應該可以一眼看出這里是用asm來生成處理類,分別生成構造函數,deserialze方法和deserialzeArrayMapping方法。我們來看一下asm生成的類是什么樣的。這里由于代碼很多我只截取一些關鍵的地方:

img

img

至此便完成了利用asm生成處理類的過程了。

1.3 處理類的處理流程

上一節中我們已經動態生成了FastjsonASMDeserializer_1_Group這個處理類,那么現在可以繼續向下跟進,看看后續的處理流程是怎么樣的。

首先,跟進一下構造函數:

img

這里利用createFieldDeserializertype類中的變量等信息轉換為FieldDeserializer類型,并存儲到sortedFieldDeserializers這個數組中,這里可以記一下這個數組的名字,后面會用到:

img

在完成構造函數后,根據上文的跟蹤,就會調用asm生成的處理類中的deserialze方法,由于我這里是把生成的bytecode抓下來寫成文件來看的,所以很多東西看的不是很清晰,但是整段處理的關鍵點在于最后的return:

img

其中的各個參數為:

img

img

跟進parseRest來看一下:

img

這里直接調用了JavaBeanDeserializer#deserialze。這里我截取幾處比較關鍵的代碼:

img

這里需要注意的有兩個變量:beanInfosortedFieldDeserializers,這兩個變量的生成過程上文都有提及,根據這兩個變量的值,我們能很好的理解JavaBeanDeserializer#deserialze這部分的代碼,這里會遍歷整個sortedFieldDeserializers中所有的key,并嘗試根據類型來提取jsonstring中相應的信息,如果成功則轉交給asm生成的處理類的createInstance實例化對象,如果不成功則掃描jsonstring中是否具有特殊的指令集,如果有,則嘗試解析指令集否則就報錯。下面具體看一下處理的流程:

img

如果失敗則嘗試解析指令集:

img

可以看到這里會嘗試解析$ref@type,如果匹配到了@type且其內容為string,則嘗試利用lexer.stringVal()通過字符串截取來獲取其內容:

img

但是由于我們發送的jsonstring中是沒有與sortedFieldDeserializers所對應的鍵名的,所以這里仍無法匹配到。因為沒有辦法找到與設定的type相符的鍵,這個時候獲取到的內容為空,fastjson會將當前這個字段判斷為一個鍵值,根據當前符號的下一個符號來判斷這個鍵所對應的值是什么類型,如果是{則這個鍵所對應的值也是一個key-value的格式,如果是"則為具體的值。在當前例子中,我們知道下一個字段應為{,fastjson在處理時會再次調用parseObject來處理這個新的鍵值對格式,下面便是如何將處理流程轉交parserObject進行二次處理的過程。這里需要用到FieldDeserializers來進行解析了:

img

跟進parseField中,關鍵的處理流程為:

img

繼續跟進:

img

這里首先通過fieldInfo.fieldClassfieldInfo.fieldType來獲取fieldValueDeserilizer由于這里對應的jsonstring是string類型,則這里最后獲取到的fieldValueDeserilizerStringCodec。所以接下來就是跟進StringCodec#deserialze中:

img

傳入的clazz應為String類型,而非StringBuffer或StringBuilder,所以繼續跟進deserialze

img

最終調用DefaultJSONParser#parse解析jsonstring:

img

現在解析的位置應為{所對應的的token,所以應為12,也就是LBRACE,這里將調用parseObject來對jsonstring進行解析,我這里截取關鍵部分:

img

在這段代碼的前面都是lexer對jsonstring的截取和處理操作,當檢測到jsonstring中含有以@type為鍵名的字段后,獲取其值,將值傳入checkAutoType中做長度檢測以及黑白名單的檢測:

img

如果通過的話,則調用config.getDeserializer獲取clazz的類:

img

根據jsonstring中的val字段來獲取obj的值:

img

img

這里將objVal的名稱以字符串的形式賦值給strVal。后面會根據clazz的類型將處理流程轉交給不同的流程這里由于指定了java.lang.class所以是轉交到TypeUtils.loadClass來處理的:

img

img

前面將對傳入的className進行解析,如果符合相應格式就會進行相應的解析(這里也是之前漏洞所在地),而后面的則會判斷cache是否為true,如果為真則將實例化后的類加入到mappings中(這也是這次漏洞的核心),最終都將把實例化后的類進行返回。

0x02 Fastjson gadget流程

其實在前文都有涉及,在這里將化繁為簡,總結一下關鍵點在哪幾個地方。

2.1 jsonstring解析簡述

縱觀整個Fastjson的處理流程,可以注意到對jsonstring的核心處理流程是在DefaultJSONParser#parse(Object fieldName)中根據jsonstring的標志位來進行分發的,常見有兩種情況:

  # 正常的kv結構
  {"k":"v"}

  # 嵌套結構
  {"k":{"kk":"vv","kk":"vv"},"k":{"k":"kk","kk":"vv","kk":"vv"}},k:v}

而Fastjson的解析方式會首先判斷當前標志位是什么,這里拿完整的解析過程來舉個例子:

最開始解析的標志位為{

  1. 判斷下一個標志位是否為",如果是"則提取key值,這時的標志位為"

  2. 判斷下一個標志位是否為:

    • 如果為:則判斷下一個標志位是否為",如果是,則獲取value值,這時的標志位為"
    • 如果為{則重復1、2的過程。
  3. 判斷下一個標志位是否為}

    • 如果為}則表示這一個單元的解析結束
    • 如果為,則表示要解析下一個kv的數據,重復1、2、3

根據不同的標志位進行不同的解析。當解析的過程中碰到了@type$ref時,將當做特殊的標志做相應的處理。

2.2 checkAutoType黑名單檢測

當解析過程中找到了@type這個關鍵的標志時,將提取其所對應的值,并檢測這個值是否在黑名單中:

img

img

先過黑名單再過白名單,這樣保證了@type所引用的類是較為安全的。

2.3 deserialze流程

jsonstring經過解析且經過安全性驗證后,最終都要變成相應的對象,而變成對象的過程就是利用反射完成的,這個過程就是反序列化的過程。而該過程主要在DefaultJSONParser#parseObject中調用deserializer.deserialze()完成:

img

這里會根據@type所指定的類來獲取或生成反序列化類,完成反序列化過程,這里如果是在預定數組中的類的話就可以直接調用相關類的deserialze方法完成反序列化操作:

img

如果沒有則會進入asm創建處理類的流程。

2.4 gadget執行的關鍵——反射調用

在具體進行反射前還有一個操作,將會解析看jsonstring中是否存在val字段,如果有,則將其提取出來賦給objVal,并將objVal的類名賦值給strVal

img

img

之后根據clazz類型交由不同的流程來處理:

img

clazz是一個class類型時,就會進入TypeUtils.loadClass中根據strVal進行類調用:

img

這里有兩個點需要注意,而這兩個點就是造成Fastjson兩個rce的關鍵點。

  • 第一個點會對傳入的@type的值進行解析,如果符合相應的格式則直接進行類加載。

  • 第二個點首先會反射調用@type的值所設置的類,然后將其加入到mappings中,當后面再次經過checkAutoType時,將會調用:

    img

將首先從mappings中獲取和typeName相同的類,也就是說這里在進行黑名單檢測前就已經返回了類,從而繞過了黑名單。

0x03 總結

就目前來說,針對Fastjson的攻防集中于對于@type的檢測的利用以及黑名單的繞過這兩部分。而從整體的運行邏輯上來看,由于Fastjson很多地方寫的比較死,很難出現重新調用構造方法覆蓋黑名單或者覆蓋mapping的操作,所以就現在最新版的Fastjson而言是比較難以繞過防護措施的。

未來可以參考struts2 ognl的攻防手法,看是否能從置空黑名單或者操作mappings來嘗試繞過防護。

0x04 Reference


Paper 本文由 Seebug Paper 發布,如需轉載請注明來源。本文地址:http://www.bjnorthway.com/994/