漏洞
Fastjson 让外来类字节替自己作证:@JSONType、Spring Boot Loader 与 JDK 8 类初始化
SOSEC 在官方 Fastjson 1.2.83 、Temurin JDK8u492 与两代 Spring Boot 经典 loader 上重建了一个 JSON 类型值怎样把 classpath 外的类送进 JVM 的完整路径: ParserConfig.checkAutoType() 先信任外来类字节中的 @JSONType 并提前返回,直到类初始化留下无害 marker 后,调用端才抛出 ClassCastException ;本地矩阵最早在 1.2.48 命中,而真实暴露仍取决于解析入口、loader/TCCL、JDK、SafeMode、handler 与出网条件。

文章导航
1 marker 先出现,类型错误后到
一次 typed DTO 对照结束时,Java 按预期抛出了 ClassCastException。如果只看接口返回,这像是一份再普通不过的失败请求:Fastjson 没能把结果当成调用者要求的 Dto。但临时目录里已经多出一枚名为 PWNED2 的文件,回环监听器也在异常出现前收到了资源请求。测试类的初始化器只做了这一件事;它没有启动子进程,没有执行系统命令,也没有连接实验环境之外的地址。真正决定漏洞等级的不是最后那次类型转换,而是它来得太晚——JVM 已经定义并初始化了一个原本不在普通应用 classpath 中的类。
这枚文件把调查从“有人展示了 shell”推进到了可逐步核对的运行事实。在官方 Fastjson 1.2.83、Temurin OpenJDK 1.8.0_492-b09 和被测 Spring Boot 经典 loader 的组合里,同一份候选类型先引发资源读取,随后被当作类定义,最后在正常对象创建中触发初始化。把 loader 换回普通 AppClassLoader,清洗后的日志没有观察到回环回调,类初始化和 marker 也未出现;在支持 SafeMode 的 1.2.83 对照中启用内建闸门,路径在资源请求前终止。正例说明这套条件足以越过代码执行边界,负例则标明了阻断发生在哪一层。
7 月 19 日,Kirill Firsov 的首条公开帖文把 Fastjson 1.2.83 的结果称为 “gadget-free RCE”。一段很短的录屏从回环地址上的解析接口走到了交互式 root shell;后续帖文又把范围写成 1.2.68 至 1.2.83,并建议启用 SafeMode 或迁移到 Fastjson2。画面足以拉响警报,却没有给出请求体、解析接口实现、Fastjson JAR 摘要、loader、线程上下文类加载器(TCCL)或调用栈。录屏里的 root 只说明实验容器内目标进程的身份,不能自动推出提权、容器逃逸或互联网暴露。
真正缺少的不是第二段“成功”录像,而是一条能从输入走到结果的因果 trace:哪一个解析入口接受了类型值,checkAutoType() 走了哪条分支,哪个 loader 把资源名解释为可读取位置,JVM 在何处定义并初始化类,最后的异常又发生在副作用之前还是之后。只有把这些问题锁定在同一进程、同一组制品和同一次运行中,才能区分单纯的外部读取、已经完成的类定义与真正跨过执行边界的初始化。SOSEC 因此没有把公开演示的结果当作根因,而是从制品身份和状态顺序重新开始。
SOSEC 随后固定全部依赖,将外部可观察效果限定为回环 HTTP 与一个惰性 marker 文件,并为普通 classpath、SafeMode、typed DTO、两代 Boot loader、不同 TCCL、多个 Fastjson 版本和显式 AutoType 开关分别建立对照。固定源码行号、可观察事件顺序和正负结果共同构成了可检验的证据链。接下来先把一次“成功”拆成四个状态变化,再沿着每个变化回到实际执行的代码。
1.1 先把一次“成功”拆成四个可观察事件
主验证对象是 Maven Central 的 Fastjson 1.2.83。上游标签指向提交 26f13f84fdd522de10678e43f55fde918ab7b347,被测 JAR 的 SHA-256 是 641A4D65AB32FBFDCCD9C718E3F83EBC4CAABDB5E4FE5B3D51527C5FE692631D,JVM 是 Temurin OpenJDK 1.8.0_492-b09。Spring Boot loader 2.7.18 与 1.5.22.RELEASE 分别作为 ParserConfig 的显式 default loader 时,都出现了同样的顺序:回环服务收到读取,候选类被定义,初始化器写出 marker,解析调用随后返回对象或暴露类型错误。
换成普通 JDK AppClassLoader,清洗后的日志没有观察到回环回调,marker 不再出现,Fastjson 回到 autoType is not support;这份负控制没有提供可宣称为零的精确 HTTP 计数。这组对照排除了两种最简单的误判:测试类原本就在应用 classpath 中,或 marker 是上一轮遗留。
实验把一个常被写成“RCE 成功”的结果拆成四步。第一步是资源服务收到请求,它只证明 loader 已经把类资源查找带到网络边界,最多先确认外部读取或 SSRF。第二步是 JVM 接受类定义;第三步是类初始化器运行并留下 marker;第四步才是 Fastjson 调用返回或抛错。四步必须分别记录,因为它们并不总是一起发生。在 Fastjson 由 Boot loader 定义、但解析线程仍携带外层 launcher TCCL 的对照中,回环服务收到了请求,类却没有完成初始化;把同一线程的 TCCL 换成 Boot loader,marker 才出现。这一个差异就足以说明:网络命中不能直接写成 RCE,最终异常也不能证明前面没有执行。
1.2 源码和实验在同一个早返回处分手
回到 Fastjson 1.2.83 的源码,marker 为什么能赶在异常前面已经很清楚。checkAutoType() 先把候选类型名改写成 .class 资源,再让 loader 返回字节流;ASM 扫描器若在这些字节里看见运行时可见的 @JSONType,局部变量 jsonType 就变成真。随后代码调用 TypeUtils.loadClass(),并在 jsonType 分支立即返回已经加载的类。危险父类检查与 expectClass.isAssignableFrom(clazz) 都写在这个返回之后。错误不是“黑名单少了一个名字”,而是刚从待验证资源里读出的注解,反过来成了这份资源自己的通行证。
现在,故事里的每个角色都有了名字:类型文本、.class 资源、带 @JSONType 的候选字节、参与查找的 loader,以及最后才执行的类型转换。下一章不再把它们写成一串配置项,而是跟着同一个候选类型走一遍,看它怎样在四个组件眼里先后变成资源、通行证、Java 类和已经初始化的对象。
2 同一个候选类型,先后被四套规则解释
过去分析 Fastjson RCE,第一步往往是翻依赖表:目标 classpath 里有没有一枚已经存在的危险类,构造器、setter 或工厂方法能不能被拼成利用链。这里最反常的地方,正是最终写下 marker 的类并没有作为普通应用类预装在 classpath 中。它最初只是一段不可信的类型文本。Fastjson 先把文本改写成类资源名;Spring Boot loader 的 handler fallback 与 JDK URL 处理规则给这个名字加上 jar-like 路由含义;返回的类字节又用自己的 @JSONType 说服 Fastjson 继续加载;最后,Temurin 8u492 接受类定义,正常对象创建触发初始化。
所以,“不依赖既有 classpath gadget”只表示少了一个旧前提,不表示链条里没有类、字节或 loader。外来类字节是新的代码载体,Boot/JDK 路径负责把它送到 JVM 门口,Fastjson 的注解分支负责放行,类初始化才让它真正获得执行机会。四个阶段分别属于不同组件,也留下不同的检测信号。依赖扫描不能因为没找到历史 gadget 就结束,网络告警也不能因为见到一次 HTTP 就跳过类定义和初始化证据。
2.1 第一次改写:类名先变成一个待查找的 .class 资源
checkAutoType() 开始注解检查时,还不知道候选类是否已经存在于 JVM。它先做一次在普通场景里很自然的转换:把 Java 二进制名称中的句点换成斜杠,再附加 .class。对正常模型类,这个资源名会落在应用目录或依赖 JAR 中;问题在于,候选文本还没有被限制为一个规范、只指向本地类空间的 Java 名称,就已经进入通用的 getResourceAsStream()。从这一刻起,Fastjson 不再独自决定字符串的含义,收到它的 loader 也开始解释。
这一步发生在真正的 loadClass() 之前,形成一条容易被漏报的前置网络路径。getResourceAsStream() 的接口只承诺取得资源流,没有承诺资源一定来自磁盘或当前 JAR;具体来源由 loader 与它理解的 URL handler 决定。在普通 AppClassLoader 对照中,清洗后的日志没有观察到回环回调,marker 未出现,Fastjson 最终拒绝 AutoType;这与本地候选没有形成可用资源流的路径一致,但该记录不提供精确 HTTP 计数。Boot loader、handler fallback 与 JDK URL 处理路径面对相同资源名时能够取得回环服务提供的 jar-like 资源,监听器因而先于任何类定义收到请求。这已经是安全边界突破,即使后续 JDK 拒绝类名、TCCL 找不到类或解析以异常结束,出网和内部地址访问仍需作为 SSRF 处理。
Fastjson 选择哪个 loader 做资源探测也不是一个抽象实现细节。ParserConfig.java 第 1482—1487 行先看实例的 defaultClassLoader;它非空时直接调用该 loader,未设置时才调用 ParserConfig.class.getClassLoader()。于是,应用显式配置一个 fat-jar loader 会同时改变资源来源与后续类加载首选项;即使没有显式配置,只要 Fastjson 自身由 Boot loader 定义,注解扫描也可能进入外部资源读取。两条路径对 RCE 尾段并不完全等价,却都说明“代码里没有 URL 客户端”不能证明解析器不会触发 URL 语义的资源访问。
2.2 第二跳是注解信任:扫描字节本想避免初始化,却替攻击者建立了准入通道
拿到资源流后,Fastjson 没有立即加载类,而是用自己的 ASM 读取器查看类文件元数据。固定提交中的 ClassReader.java 第 90—140 行解析类文件结构,TypeCollector.java 第 71—108 行记录运行时可见的 Fastjson @JSONType。这项设计的表面优势是可以先看注解、后决定是否加载,避免仅为反射检查便触发类初始化;安全缺陷在于被检查的字节来源未必可信,而注解又由这份字节自行声明。候选材料同时充当了申请者和推荐信。
@JSONType 原本是 Fastjson 用于描述序列化、反序列化和多态行为的库内注解,应用开发者给本地模型类添加它时,注解通常继承了构建与 classpath 的信任。loader 链打破了这个隐含前提:扫描器没有先证明资源来自已经批准的本地制品,便把“字节里自报含有注解”写入 jsonType=true。接下来的加载条件是 autoTypeSupport || jsonType || expectClassFlag,因此即使 autoTypeSupport=false,注解仍单独足以打开 TypeUtils.loadClass()。SOSEC 的阴性与阳性 canary 由此同时解释了“AutoType 关闭”与执行效果为何能够共存:AutoType 总开关不是这项信任的唯一来源。
更关键的是信任结果在控制流中的位置。类加载成功后,只要 jsonType 为真,方法便立即返回;危险 ClassLoader、DataSource、RowSet 父类检查以及 expectClass.isAssignableFrom(clazz) 都排在后面。这不是“检查存在但规则不够全”,而是一个顺序错误:受不可信资源控制的注解分支绕开了本应约束返回类型的后置判定。修复思路因此不能只增加一个恶意名称特征;永久修复必须让 classpath 外资源中的自声明注解不再构成信任,或者保证所有返回路径都在返回前执行来源、类名与期望类型约束。
2.3 第三、第四跳由 Boot/JDK 组合路径完成:同一资源能被读取、定义并最终初始化
Spring Boot 经典可执行 JAR 必须在一个外层归档中继续找到 BOOT-INF/classes 和嵌套依赖,因此提供了 LaunchedURLClassLoader 与配套的 jar URL 处理。这项能力本来服务于正常启动;危险出现在 Fastjson 把尚未约束的类型文本交给这套资源解释规则。两边单看都在完成自己的工作,组合以后却没有任何一方保证“这个资源名只能落在应用已经信任的本地类空间”。
Spring Boot 2.7.18 的标签指向 commit 0c8b382d42db22b92efcf47000d0ff9ef4971629。LaunchedURLClassLoader.java 第 93—156 行把资源查找与普通类加载交给 URLClassLoader。Handler.java 第 86—121 行在 Boot 无法打开 jar URL 时转入平台 fallback;第 366—393 行则把 Boot 自己处理的根归档限制为 file:。已经取得 Boot JarFile 根后,JarURLConnection.java 第 244—267 行才沿连续的 !/ 段查找内部嵌套归档。外部读取因此来自 Boot fallback 与 JDK URL 处理的组合,不是 LaunchedURLClassLoader 自带远程下载功能。
Boot 1.5.22.RELEASE 的标签指向 commit 34c62cc05919259cf75b925f88465638d1744b75。这一代的LaunchedURLClassLoader.java 第 55—99 行同样负责委托;Handler.java 第 92—115 行负责 fallback,第 302—330 行限制 Boot 根。JarURLConnection.java 第 250—274 行再在本地根内遍历嵌套段。两代 loader 都完成 marker 对照,说明结果不是 2.7.18 的偶然单点;这两个样本仍代表不了所有 Boot 版本、启动方式和新 loader 布局。
为避免把 parser、loader 和 marker 混成一个黑箱,SOSEC 的受控实验分别隔离了普通 classpath、Boot loader、资源读取、checkAutoType、类加载、解析和 marker 类的职责,并通过同步线程与带 Boot TCCL 的工作线程核对后半段。Boot loader 2.7.18 与 1.5.22.RELEASE 在显式作为 default loader 时都能走到 marker;这一运行时结论由固定源码结构与本地回环记录共同支撑,而不是从一处源码链接直接外推。
JDK 8 是本地完整执行矩阵的一部分,而不是随手记录的运行版本。类文件内部名称经过专门设计,既能在 Fastjson 的资源名重构后落入 loader 可理解的 jar-like 位置,又必须在稍后的类定义中被 JVM 接受。这个 bytecode-only 内部名称无法用普通 Java 源码声明忠实表达;源码语言校验不是 JVM 唯一的命名边界。Temurin 1.8.0_492-b09 接受本地测试类的定义,随后正常对象物化触发初始化。SOSEC 尚未在 JDK 9 及以上版本复跑同一矩阵,因此不给出版本豁免;即使新 JVM 截断类定义,资源探测造成的 SSRF、凭据转发风险与其他 loader 差异仍要分别验证。
图中的门不是一个统一开关。Fastjson 先用 loader 取资源流,稍后 TypeUtils.loadClass() 又可能选择另一 loader;JVM 加载、链接与初始化也是不同阶段。正因如此,同一测试在“Fastjson 由 Boot loader 定义,但当前线程仍保留外层 launcher TCCL”时可以产生 HTTP,却没有 marker;把线程 TCCL 设为 Boot loader 后,后半段才完成。只观察应用打包方式而不观察实际线程,容易把 SSRF-only 环境误报成完整 RCE,也容易漏掉异步工作线程、调度线程或消息消费者中的真实暴露。
2.4 同一个应用里,不同线程可以停在不同一步
真正需要在生产里回答的,不是“是否使用 Spring Boot”,而是四个连续问题:不可信数据能否控制 Fastjson 处理的类型标识;资源查找实际交给哪个 loader;显式 default loader 或当前线程 TCCL 中,谁能按同一名称定义类;JVM 与对象创建是否又让它从“已定义”走到“已初始化”。这些答案要在执行解析的那条线程上取得。依赖清单、控制器签名和主线程启动日志只能提供线索,不能替每个 worker 作答。
现实入口也可能彼此不同。一个同步 HTTP 请求线程、应用启动时创建的 worker、容器管理的调度池与消息框架消费者可以携带不同 TCCL;同一个 ParserConfig 全局实例还可能被启动代码设置 default loader。测试只覆盖某个健康检查线程,不能替其他执行池签字;相反,一个请求线程测试如果只得到 SSRF,也不能证明后台解析任务安全。清点时应从每个不可信 JSON 消费者出发,记录线程创建者、TCCL 继承与后续修改,而不是给整套 Spring Boot 应用贴一个统一标签。
责任也要按边界落到具体团队。应用负责人确认不可信 JSON 是否能携带类型元数据、Fastjson 实际收到的 loader、SafeMode 与 handler 状态,以及解析进程能否到达任意网络目的地;平台负责人掌握 JDK、打包和 launcher;网络负责人验证强制出站策略。Spring Boot 的正常嵌套 JAR 支持是组合前提与加固面,漏洞性的先信任后检查顺序位于已经归档的 Fastjson 1.x。即使临时移除了某个环境前提,只要服务仍让 1.x 处理不可信输入,移除这条依赖路径的迁移责任就没有结束。
到这里,候选类型已经换过三次身份:先是 JSON 里的文本,再是 loader 要查找的资源,随后又成为带着 @JSONType 的待加载类。还差最后一步——Fastjson 怎样把这个类交给普通反序列化器,并让类型错误拖到初始化之后。第三章沿通用 @type 和 typed JavaBean 两个入口走进同一个 checkAutoType(),把这次迟到的检查落到具体行号。
3 源码顺序决定后果:类型相容性检查迟到,类初始化与对象物化先发生
loader 链解释了字节怎样到达 JVM,Fastjson 固定提交则解释这些字节为什么得到执行机会。完整路径不是一个孤立的 if:通用对象解析或 typed JavaBean 先识别类型键,把文本与期望根类型交给 checkAutoType();该函数先探测资源和注解,再选择 loader;返回的类被交给反序列化器创建对象和写字段;只有这些步骤之后,Java 调用端才可能发现返回对象不是它要求的 DTO。按这个顺序阅读,三个看似矛盾的本地结果便能同时成立:autoTypeSupport=false,Dto.class 已明确给出,最终仍先有 marker、后有 ClassCastException。
3.1 通用 @type 与 JavaBean typeKey 是两条入口,却汇入同一个策略函数
通用入口位于DefaultJSONParser.java 第 315—406 行。解析器遇到默认类型键时,先读取 typeName;DisableSpecialKeyDetect 会让通用入口不再把特殊键交给这条分支,IgnoreAutoType 则会把命中的类型值当普通数据跳过。它会直接识别少数普通 map 类型,并从这条类型解析路径拒绝纯数字值;未命中特例时,第 343 行调用 config.checkAutoType(typeName, null, lexer.getFeatures())。如果没有解析出类,代码把类型文本留在 map 中继续;一旦得到类,后续分支会按当前对象状态执行类型转换、clazz.newInstance(),或取得对应 ObjectDeserializer 并调用 deserialze()。也就是说,策略函数返回一个类不是日志意义上的“识别”,而是直接改变对象物化的目标。
typed JavaBean 入口的位置更能解释固定 DTO 的局限。在JavaBeanDeserializer.java 第 786—838 行,键只要匹配 bean 自己配置的 typeKey 或默认类型键,解析器就读取候选名称。它先尝试 seeAlso 关系和反序列化器级 handler;没有结果时,第 806 行从调用类型提取 expectClass,第 823 行把候选名称、期望类型与有效特性位交给同一个 checkAutoType()。返回类随后立刻用于选择新的反序列化器,第 828 行按这个 userType 物化对象,第 838 行返回 typed object。根 DTO 因而限定了初始 deserializer,却没有把解析过程冻结为“只能创建 DTO 本身”。
这里还要区分默认 @type 与模型声明的自定义 typeKey。两者在 JavaBean 分支中共享类型切换逻辑,但应用可达性取决于实际模型、注解、字段顺序、解析重载与特性位;只在网关上搜索一个固定键名,覆盖不了所有 typed 入口。
3.2 checkAutoType() 的五个决策点,把“注解存在”变成了“类型可返回”
第一段状态在函数开头形成。ParserConfig.java 第 1311—1336 行先让注册的 AutoTypeCheckHandler 有机会直接给出类,再合并实例、调用特性与默认特性中的 SafeMode;SafeMode 为真时立即拒绝,随后才计算 autoTypeSupport。这段顺序解释了两个处置边界:没有自定义 handler 的本地 SafeMode 对照在任何 HTTP 前结束;存在宽松 handler 的应用却可能从第 1320 行提前返回,所以 SafeMode 验证必须包含 handler 清单。显式 AutoType 开关稍后只是加载条件的一部分,不控制所有返回来源。
第二段是名称、哈希与既有映射的传统 AutoType 策略,负责长度、首尾字符、期望类标志、deny/accept 哈希、缓存映射和已注册反序列化器等决定。本文链条没有必要逐一绕过一串公开黑名单;它在这些决策之后进入第三段,也就是第 1479—1498 行的注解资源扫描。此处创建的局部布尔值 jsonType 初始为假,资源读取和 ASM 扫描成功且发现运行时可见注解后才变真;异常会被捕获并跳过,资源流在 finally 中关闭。网络命中但类文件无效、注解缺失或扫描异常时,路径可以停在这里。
第四段把扫描结果变成类加载授权。第 1500 行的条件是 autoTypeSupport || jsonType || expectClassFlag,任意一项为真便调用 TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);缓存标志在 AutoType 或注解信任成立时为真。于是 autoTypeSupport=false 并不能抵消 jsonType=true,而给出一个非空 expectClass 也不等于已经完成相容性验证。代码在这一刻只决定“允许尝试加载”,尚未执行后面的 isAssignableFrom()。
第五段才是根因转折。第 1505 行确认 clazz != null 后,jsonType 分支在第 1506—1510 行直接返回;只有没有走这个分支的类,才会在第 1513—1517 行被检查是否属于 ClassLoader、DataSource 或 RowSet 危险父类,并在第 1520—1528 行与 expectClass 做可赋值判断。控制流不是“检查失败后仍被利用”,而是“自报注解的类在检查开始前已经离开函数”。这也说明简单把危险父类表加长无法修复本链:任何位于早返回后的规则都没有机会评价该类。
3.3 loadClass() 解决“由谁定义”,对象物化随后解决“何时初始化”
Fastjson 的类加载包装器按可观察的优先级选择 loader。在TypeUtils.java 第 1759—1788 行,显式传入的 classLoader 非空时先调用它的 loadClass(),成功便可写入 mapping 并返回;失败被捕获后,代码取得当前线程 TCCL,只要它与前一个 loader 不同,便再调用一次,最后还会尝试 Class.forName(className)。这个回退顺序把 ParserConfig.defaultClassLoader 和线程执行环境接到了一起:显式 Boot loader 能直接闭合前后两次查找;没有显式 loader 时,定义 Fastjson 的 loader 可以完成资源扫描,真正的类定义却仍可能取决于 TCCL 或最终回退。
ClassLoader.loadClass() 通常负责查找、定义和链接,并不保证类已经初始化。marker canary 刻意利用这个分界:监听器记录资源,loader 日志记录类定义,而 marker 只在类初始化真正发生时出现。通用解析器在空对象快捷分支可于 DefaultJSONParser.java 第 370 行调用 clazz.newInstance();常规 JavaBean 路径则由JavaBeanDeserializer.createInstance() 第 190—283 行调用默认构造器或工厂方法。构造一个此前尚未初始化的类会先执行其类初始化,因此 marker 可以在业务代码取得返回值以前落盘。
对象创建以后,FieldDeserializer.setValue() 第 59—220 行还可能调用 setter、更新 get-only 容器或直接写字段。这些是反序列化的一般副作用面,不能在没有 trace 时统称为本次 RCE sink;本地 marker 类只需类初始化就能证明已经执行到外来字节。这样的最小 canary 反而更有归因力:它不依赖某个业务 setter、外部命令解释器或网络回连,失败点可以精确落在“没有定义”“定义未初始化”或“初始化完成”三种状态之一。
3.4 typed DTO 先物化外来类型,调用端的强制转换随后才宣布失败
typed DTO 对照使用正常的 JSON.parseObject(..., Dto.class) 调用,并让 Fastjson 的全局配置显式指向 Boot loader。运行结果按时间是:监听器收到回环资源请求,候选类被定义,类初始化写入 marker,解析器物化并返回该对象,最后调用端看到 ClassCastException。异常没有回滚静态初始化,也不会删除已经写出的文件、已发出的网络请求或其他外部效果;Java 异常传播只改变当前控制流,不提供事务补偿。
源码能够完整解释这个顺序。JavaBean 分支把 Dto.class 提炼为 expectClass 并传入 checkAutoType(),但 jsonType 早返回绕过了 expectClass.isAssignableFrom()。反序列化器随后按攻击者选择的 userType 创建对象,在第 838 行以泛型 T 返回;Java 泛型在运行时擦除,库内这次 `(T)` 不一定产生具体 DTO 的即时检查。具体调用点需要把返回的 Object 当作 Dto 使用或赋值时,编译器生成的 checkcast 才抛出可见异常。被绕开的 parser 策略检查和最终调用端类型检查处在两个不同时间点,后者不能替前者承担安全职责。
Dto.class 锁住的是调用者期望,不是此前每一次类型选择;@JSONType 早返回让外来类先物化,红色类型屏障在对象已经初始化后才报告不相容。固定 DTO 仍然是值得保留的设计约束:具体、final、只含标量字段且没有多态扩展的模型通常能缩小攻击面,也便于迁移和验证。但对本文路径,它只能成为分层防御的一层,不能替代支持版本上的 SafeMode、loader 约束、出网控制与永久升级;对没有 SafeMode 的旧版本,则更不能把固定 DTO 当作等价补偿。响应人员应把“接口最后返回 500 或类型转换异常”视为可能发生过前置副作用的告警条件,向前关联同一请求中的资源读取、类加载和文件/进程事件,而不是把异常当成攻击已经失败的证明。
至此,调用路径已经从入口走到迟到的异常。最后还需回答两类容易在公告中混在一起的问题:这项机制从哪个 Fastjson 版本开始在本地成立,哪些版本只发生资源读取或走的是更早的 AutoType 行为;以及同一个版本为什么会随 JDK、Boot loader、default loader 和 TCCL 改变结果。第四章用固定矩阵划出这些边界,不再用一个宽泛的“≤1.2.83”覆盖所有状态。
4 版本号只标识代码快照,loader、TCCL、JDK 与出网条件才决定执行终点
把 1.2.68—1.2.83 直接写成统一“受影响版本”,会同时丢失两种重要信息:源码中的注解资源扫描实际更早出现,而同一个版本在普通 classpath、Boot loader 不同配置和不同线程上又会停在不同阶段。版本回答“Fastjson 带有哪些分支”,环境回答“这些分支能否取得资源、定义类并初始化”;只有两者相交,才构成本文验证的 RCE。因而本章不追求一个看似简洁的总范围,而是先定位代码引入点,再公布固定矩阵,最后把部署判定写成可采集的运行时条件。
4.1 1.2.48 才是这条注解扫描分支的源码起点,1.2.68 是另一层公开范围
上游历史给出了清楚的分界。Fastjson 标签 1.2.47 固定在提交 03d77b6e629b6ff825a1c8cc3f7cbba7feb98395,1.2.48 固定在 28da8e194bb7bca71676bb10b9cb343653289de6。两者之间的提交 11b92d9f33119ca2af1a3fe6f474de5c1810e686为 ParserConfig 引入 ClassReader、TypeCollector、.class 资源扫描、jsonType 加载条件与注解早返回;紧随其后的39ccafb821276bec50e59e9cb7617627632bdb7c进一步整理 expectClassFlag 条件。1.2.47 没有这套字节扫描,1.2.48 已经包含。源码引入点与本地 marker 首次出现的版本相合。
1.2.68 仍有两层历史意义:Firsov 以它作为公开场景的下限,Fastjson 也从这一代提供可验证的内建 SafeMode;它却不是注解扫描分支的引入点。SOSEC 的 1.2.48 阳性结果回答的是,在固定 Temurin 8u492 与 Boot loader 环境中哪个抽样版本最早完成本文机制。两者不能合并成维护者确认的连续范围,中间未执行的发布物也不能由首尾版本代填。
4.2 十个版本测试点显示了三种结果:旧 AutoType、仅资源访问和完整初始化
第一轮版本矩阵因父 classpath 残留 1.2.83 而整体作废;第 5.2 节会交代发现和重跑过程。下表只采用修正后的结果:同一 Temurin JDK 1.8.0_492-b09、Spring Boot loader 2.7.18、显式 Boot defaultClassLoader、回环监听器与 marker-only classpath 外候选类,每次都从干净 JVM 和不存在的 marker 开始。HTTP 次数受缓存、解析入口和 loader 实现影响,不是可移植指纹;表格分别记录资源访问与类初始化。
| Fastjson | HTTP 请求 | 类初始化 | marker | 解释 |
|---|---|---|---|---|
1.2.24 | 2 | 是 | 有 | 早期 AutoType 开放行为能够接受目标;不能归入后来新增的 @JSONType 字节扫描路线 |
1.2.25 | 0 | 否 | 无 | 候选在本测试环境中被拒绝,没有网络前置效果 |
1.2.46 | 2 | 否 | 无 | 出现资源访问,但未完成目标类初始化 |
1.2.47 | 2 | 否 | 无 | 仍停在网络效果;标签尚无新的注解字节扫描分支 |
1.2.48 | 4 | 是 | 有 | 首个包含资源扫描与 jsonType 早返回的标签,本地完整路径成立 |
1.2.66 | 4 | 是 | 有 | 完整路径成立 |
1.2.67 | 4 | 是 | 有 | 完整路径成立 |
1.2.68 | 4 | 是 | 有 | 完整路径成立;这一代同时提供可验证的内建 SafeMode |
1.2.80 | 4 | 是 | 有 | 完整路径成立 |
1.2.83 | 4 | 是 | 有 | 完整路径成立;本调查的源码与制品基准 |
1.2.24 是矩阵中最容易误读的一行。它产生与后期版本相同的 marker,不代表相同根因跨越了全部 1.x 历史;在 1.2.25 强化 AutoType 以前,解析器通过更宽松的旧决策接受候选,执行没有依赖后来才加入的 ASM 注解探测。按最终效果划分,它当然仍是高风险旧版本;按这项机制划分,它却必须单列。否则修复史会被倒写成“@JSONType loader 链从 1.2.24 已存在”,与源码相矛盾。
1.2.46 与 1.2.47 则说明没有 marker 不等于无安全后果。两版在同一测试环境中都产生两次回环 HTTP 访问,却没有初始化目标类;这类结果至少证明不可信类型处理能够影响资源获取,应按 SSRF、内部服务探测和可能的凭据泄露面继续调查。它们不应被放进“本地完整 RCE”栏,也不应因最终异常而被放进“完全不受影响”栏。版本公告如果只允许一个布尔字段,至少应在备注中把网络前置效果与代码执行分开。
1.2.48 是修正矩阵里的首个完整正例;之后实际运行的 1.2.66、1.2.67、1.2.68、1.2.80 与 1.2.83 也都出现 marker。固定提交解释了为什么 1.2.48 与 1.2.47 分开,但中间未运行的发布物仍是未运行,不应被区间横线悄悄改成已验证。需要签发精确采购、监管或扫描范围的团队,还应补齐这些标签以及 noneautotype、Android、厂商 fork、shaded 副本等变体。
4.3 同一个 1.2.83 在六项环境条件下会分别停在拒绝、SSRF 或 RCE
第一项条件是入口。攻击者必须能控制进入类型选择的文本;仅序列化输出、解析完全可信的常量配置或业务层在 Fastjson 前完成不可绕过的模式验证,不与公开 RCE 自动等价。但“接口使用 Dto.class”不是充分的不可达证明,因为 typed JavaBean 分支仍接受默认类型键或模型 typeKey,而早返回绕过了期望类型。入口盘点必须覆盖同步控制器、消息体转换器、缓存反序列化、RPC 编解码、定时导入、异步队列与二次类型转换,尤其关注不同线程池。
第二项是资源 loader。普通 AppClassLoader 负控制的清洗日志没有观察到回环回调;经典 Spring Boot LaunchedURLClassLoader 2.7.18 和 1.5.22.RELEASE 在显式设为 default loader 时均完成路径。这两个成功点不能外推为“所有 Spring Boot 版本受影响”,更不能外推到展开目录启动、传统 WAR、Boot 3 新 loader 布局、应用服务器自定义 loader 或原生镜像;这些形态应分别以 getResourceAsStream() canary 测量。反过来,风险也不只属于 Spring Boot 品牌:任何把候选资源名解释为外部 URL 或嵌套归档资源、又能按同一名称定义类的自定义 loader,都满足机制所需的能力形状。
第三项是 Fastjson 的 default loader,第四项是当前线程 TCCL。显式 setDefaultClassLoader(fatClassLoader) 让资源探测与 TypeUtils.loadClass() 使用同一对象,正例最稳定。未显式设置时,ParserConfig.class.getClassLoader() 仍可能经 Boot/JDK URL 处理路径取得 classpath 外资源,但 TypeUtils 的后续回退落到请求线程 TCCL;本地外层 launcher TCCL 只能到 SSRF,换成 Boot TCCL 才初始化。生产证据应在实际解析线程当场记录两者,不要只在 main() 启动日志打印一次。
第五项是 JVM。本地完整正例只对 Temurin JDK 8u492 签字。概念验证作者提出一项仍待独立验证的 JDK 9+ 命题:名称校验会阻断类定义,但不会阻断前面的外部资源访问。SOSEC 尚未把这项命题纳入本地矩阵。架构、供应商、补丁级别、启动选项与安全代理都可能改变类加载行为,资产记录因此要保留完整运行时字符串和镜像摘要,不能只写一个大版本号。升级到受支持的新 JDK 是重要的纵深控制,却不能替代 Fastjson 修复和出网限制;没有本应用 canary 前,也不应把“Java 17”三个字当成已验证的阴性结果。第六项是网络:目标 JVM 必须能够取得类资源才会沿已确认路径继续,严格 egress allowlist 能切断外部资源投递,但网络阻断没有删除 parser 的注解信任与早返回,配置漂移、代理例外和内部制品站点仍需监控。
SafeMode 与 AutoType 开关横跨这些条件。内建 SafeMode 在本地 1.2.83 无 handler 对照中最早拒绝,网络计数与 marker 都归零;对 1.2.68 及以后的 1.x 部署,它是目前最直接的临时阻断。低于 1.2.68 的版本没有同等 SafeMode 开关,响应动作应是迁移、入口下线、schema 约束、loader 隔离与 egress 收缩,而不是登记一个不存在的配置项。显式 setAutoTypeSupport(true) 没有缓解,完整路径仍成立;在不可信输入上打开它还会扩大其他历史类型选择面。固定 DTO 最终抛错也没有缓解,因为初始化已经发生。三项结果给出一条简单的运营原则:只承认能让资源请求、类定义和 marker 同时消失的控制,且必须在实际入口、实际线程和干净 JVM 中复测。
4.4 范围声明要同时交代代码、环境与最终后果
对外只写“Fastjson 1.2.83 RCE”会漏掉决定结果的部署条件。更准确的说法是:在被测 Temurin 8u492、Spring Boot 经典 fat-jar loader 能参与资源查找与类定义、Fastjson default loader 或当前线程 TCCL 能到达它、目标可取回资源且不可信输入进入类型选择时,SOSEC 在 1.2.48 及其后五个抽样版本(最晚为 1.2.83)验证了 @JSONType classpath 外候选类初始化链。普通 AppClassLoader 对照的清洗日志没有观察到回环回调,marker 也未出现;另一些 Boot/TCCL 组合只走到资源访问。这样写,读者能直接看出本地 RCE 已经跑通,也能看出自己的部署还需要核对什么。
Firsov 给出的 1.2.68—1.2.83 范围、尚未本地验证的后续 JDK 行为、Fastjson2 的迁移预期以及在野利用状态,都必须与 SOSEC 已验证的矩阵分开。新的维护者公告、修复提交或公共技术说明出现后,可以更新这些结论,却不能反向改写本地记录。尤其是修复版不能靠版本号猜测:维护方需要改变不可信资源可自证 @JSONType 的信任关系或消除早返回,并用回归测试证明 SafeMode、handler、typed root、loader 与合法注解兼容性;运营团队还要核对生产实际加载的字节与固定构建一致。
防守定位已经精确到 ParserConfig.java 第 1479—1528 行、TypeUtils.java 第 1759—1788 行、两条解析入口、Spring Boot loader/TCCL 条件、Temurin 8u492、回环 HTTP 与 marker 三阶段判据。生产判定不依赖攻击字符串、命令执行类或远程投递步骤。可审计证据应使用固定源码、不可执行 canary 哈希、资源计数、loader 记录、环境清单和正负控制;入口遏制、修复验收与事件分级分别以资源是否被读取、类是否被定义、marker 是否出现以及最终返回为观察点。
源码、loader 与修正后的版本结果现在已经接上。把它们带进生产之前,还要交代这些对照怎样排除了旧 marker、错误 JAR 和预先可见的测试类;否则一张成功截图仍可能把完全不同的原因混在一起。第五章回到实验台,重点不是再讲一次机制,而是说明每个对照究竟排除了什么,以及第一轮版本表为什么必须整份丢弃。
5 真正有用的复现,不是屏幕上那行 marker
第 1 章已经列出制品摘要、源码提交、JDK 构建和两代 Boot loader。每份证据记录把这些身份绑定到同一次运行:一个进程、一个 ParserConfig 来源、一组 loader 状态、归零后的回环计数和运行前不存在的 marker。Firsov 的公开材料提出待检验命题;制品身份、日志和运行结果则由 SOSEC 的独立对照建立。
SOSEC 将两个容易混在一起的 canary 分开运行。第一种类预先放在应用 classpath 上,只回答 Fastjson 会不会在 autoTypeSupport=false 时把带 @JSONType 的可见类选出来,以及 typed root 的相容性检查是否来得太晚。第二种类不作为普通应用类出现,候选字节必须先经过 Boot 的资源语义被取回,再由 loader 定义和初始化。第一种证明 parser 的信任顺序有问题;第二种才把结论推进到“新的类字节能够进入 JVM”。如果把两者合并成一张成功截图,最关键的外部加载问题反而会被藏掉。
5.1 八组对照,逐根拔掉这条链上的支撑
| 安全测试行 | 观察结果 | 它证明什么 | 它不证明什么 |
|---|---|---|---|
普通 AppClassLoader | 清洗日志未观察到回环回调;无 marker;解析被拒绝(未取得精确 HTTP 计数) | 本次链依赖额外的资源解释与类加载语义 | 不能据此证明普通 classpath 上不存在别的 Fastjson 风险 |
Boot 2.7.18 loader,并显式设为 ParserConfig.defaultClassLoader | 外部候选字节被读取,类被定义并初始化,marker 出现,解析返回对象 | 固定的 Temurin 8u492、Fastjson 与 loader 条件足以闭合无害链 | 不能推广到所有 Spring Boot、JDK 或网络边界 |
| Boot 1.5.22 loader,其他条件相同 | 同样取得字节并出现 marker | 结果并非只存在于 2.7.18 这一单点版本 | 两个样本不等于全部 Boot 版本矩阵 |
| Fastjson 由 Boot loader 加载,线程 TCCL 仍是外层 loader | 出现外部资源请求,但无 marker,解析被拒绝 | 资源探测与最终类定义可以在两个阶段分离 | 网络请求本身不是 RCE 判据 |
| 线程 TCCL 也设为 Boot loader | 外部资源请求与 marker 均出现,解析返回对象 | 实际解析线程的 TCCL 决定后半段能否继续 | 不能仅凭启动类的 loader 推断所有 worker |
固定根 Dto.class 的 typed 路径 | marker 先出现,随后抛 ClassCastException | 类型不兼容错误发生得晚于被测类初始化 | 不能证明每个 DTO 图或每个转换器都可达 |
| 无自定义 handler,启用内置 SafeMode | 零请求、无 marker,在类型选择前拒绝 | 内置 SafeMode 对被测无 handler 路径有效 | 不能代替对应用 handler 与其他配置实例的审计 |
| 显式启用 AutoType | 仍取得字节并出现 marker | 开启 AutoType 不是缓解措施 | 不能据此推断 AutoType 开关与所有其他路径无关 |
每行测试都以干净进程开始,先删除旧 marker,确认回环资源服务的请求计数归零,再记录 parser 配置、默认 loader、当前线程 TCCL、解析结果和运行后 marker。对照之间只改变一个变量;资源服务只返回实验固定的类字节,且目标 JVM 无生产凭据、无主机挂载、无外部路由。证据包同时保存 JAR 与外层 fat JAR 摘要、JDK 版本输出、测试程序 commit、类加载来源、回环访问日志、marker 内容和异常堆栈。若只保留一张显示 PWNED2 的截图,审阅者无法判断文件来自旧运行、测试初始化还是目标类;只有时间、进程与摘要相互对应,marker 才能承担初始化判据。
5.2 一次整齐得可疑的结果,迫使 SOSEC 推翻第一轮矩阵
版本测试第一次跑完时,十行结果漂亮得近乎完美:从 1.2.24 到 1.2.83,每个进程都记下四次回环请求,每个进程都有 marker。它也立刻显得不对。1.2.47 的固定源码里还没有后来那段 @JSONType 字节扫描,1.2.25 又正是旧 AutoType 行为发生明显变化的版本;如果十个标签毫无差别,最先该怀疑的不是源码历史,而是测试究竟加载了哪一个 JAR。
检查启动 classpath 后,问题出现了:父 classpath 里仍留着 fastjson-1.2.83.jar。测试日志虽然把待测版本写进 Boot loader 的 URL 列表,那个文件名却不能证明 ParserConfig 真由它定义;父加载路径上的 1.2.83 足以污染每一行。于是第一轮矩阵整体作废,既不用于版本范围,也不拿来支持 RCE。这次作废记录揭示了 Java 依赖测试中的常见陷阱:改了 URL、manifest 或文件名,不等于改了运行中的类。
第二轮把每个待测 JAR 放在 Java classpath 最前,并把同一绝对路径传给测试程序;每次运行都打印实际版本,删除旧 marker,重置回环计数,再以独立 JVM 执行。结果终于出现与源码相符的分叉:1.2.24 通过更早、更宽松的 AutoType 路径产生两次请求并初始化;1.2.25 在请求前拒绝;1.2.46 与 1.2.47 各有两次资源访问,却没有 marker;首次完整命中的是 1.2.48,随后抽样的 1.2.66、1.2.67、1.2.68、1.2.80 与 1.2.83 都得到四次请求和 marker。第四章公布的是这轮修正结果,而不是那份全绿的废表。
这次返工也限定了证据边界:1.2.48 是本地最早成功样本,引入注解扫描的上游提交已经进入该标签,其后另有五个成功样本;未逐个运行的 1.2.49—1.2.65、1.2.69—1.2.79 与 1.2.81—1.2.82 不能被写成“全部验证”。Android、noneautotype、厂商回补、私有 fork 和 shaded 副本也都需要自己的制品身份。可信范围来自源码历史、实际加载字节和离散运行结果的相互校验,而不是表格首尾之间的一条横线。
同一制品还应分别做冷启动和热进程观察。jsonType 命中时,1.2.83 的调用点允许把成功加载的类写入 Fastjson mapping;JVM 和 loader 自身也会缓存已经定义的类。第一次请求可能经历资源读取,后续请求却直接复用进程状态,或者以另一种异常结束。版本对照必须用全新 JVM,生产追查则要保留进程启动时间和可疑请求顺序。缓存不是本次链成立的额外前提,却会改变可见日志;把不同生命期的两次请求当作同一个实验,很容易误判某项控制是否真正生效。
6 离开实验室后,先找到真正处理请求的那一个 JVM
实验室里可以把 JAR、线程和网络一项项固定,生产环境却不会主动把答案摆好。依赖扫描器最多告诉你仓库或镜像里有 Fastjson;它不知道哪一份 ParserConfig 正在处理请求,也不知道那个线程拿着什么 TCCL。调查必须沿着一条真实数据流倒查:谁提交了 JSON,哪段代码解析它,实际加载的是哪份 1.x 字节,候选资源能否被取回,类能否定义和初始化,最后进程有没有出现额外后果。少走一步,结论就会把“磁盘上有旧 JAR”误写成 RCE,或把“请求最后报错”误写成安全。
本地正例需要几件事同时发生:不可信数据进入类型选择,资源探测落到能解释嵌套 JAR 语义的 loader,显式 default loader 或当前线程 TCCL 能继续定义类,被测 Temurin 8u492 接受这次定义,服务又能取到候选资源。SafeMode 可以在最前面关门,自定义 handler 也可能在它之前作决定;进程权限则决定初始化之后能造成多大损害。这里没有任何一项能从 pom.xml、容器里的一张文件清单或一句“我们用的是 Spring Boot”推出来。
6.1 不要从 pom.xml 开始,要从运行中的 ParserConfig 倒查
第一步是识别真正定义 com.alibaba.fastjson.JSON 与 ParserConfig 的字节。构建侧应查直接与传递依赖、fat JAR 的 BOOT-INF/lib、shaded 类、插件目录和应用服务器共享库;运行侧则记录 code source、JAR 哈希、类加载器身份、镜像摘要、进程启动时间与完整 Java 版本。经典 Spring Boot 1.x/2.x fat JAR 中的 LaunchedURLClassLoader、展开目录下的普通 AppClassLoader、WAR 容器和自定义插件 loader 是不同的风险对象。磁盘上存在一份闲置 JAR 只是清点事实,解析请求实际调用哪一份类才是暴露证据。
在实际解析线程读取 ParserConfig.class.getProtectionDomain().getCodeSource(),记录返回位置和定义它的 ClassLoader;JDK 8 可在隔离复跑中用 -verbose:class 观察类来源,较新 JDK 则可使用统一类加载日志。随后从同一来源提取 JAR 并计算摘要,同时检查 fat JAR、应用服务器共享目录和插件 loader 是否还藏有第二份 Fastjson。CodeSource 为空时,改用受控 instrumentation、类资源读取或 loader 制品清单补证。最终记录要把 ParserConfig、TypeUtils 的来源、loader 对象、解析线程 TCCL、进程启动时间、镜像摘要和 JVM 参数绑在一起;父子 loader 都能看到同名类时,还要写明最终委派给了谁。
第二步是在真实解析线程观察 ParserConfig.defaultClassLoader 与 TCCL。控制器线程、@Async 任务、CompletableFuture、自建线程池、队列监听器、定时任务和插件线程可能继承不同的 context loader;主线程输出不能替代这些值。应为每个入口记录具体 Fastjson API、声明目标类型、配置实例、有效 feature、SafeMode、默认类型键、全局及注解级 AutoTypeCheckHandler。本地对照已经说明,Fastjson 本身由 Boot loader 加载只足以产生资源请求,并不保证最终初始化;真正的 worker TCCL 可以决定链停在哪一层。
第三步是找全不可信输入,而不是只找公开 HTTP controller。除了反向代理后的 JSON 接口,还要检查 Webhook、文件导入、批处理、消息队列、缓存反序列化、RPC 适配器、Redis serializer、管理功能、灾备副本与长期未重启的消费者。身份认证只缩小输入生产者集合:普通租户、合作方、被接管账户或跨租户消息仍跨越信任边界。固定 DTO 也不是否定证据;typed canary 证明最终 cast 失败可能晚于类初始化。只有业务不再需要的入口被真正移除,或数据在进入 Fastjson 前由独立的严格模式验证并消除了类型选择能力,才能把相应路径从暴露图中删去。
入口不必靠猜 JSON 外形来找。从 JSON.parse()、JSON.parseObject()、HTTP message converter、自定义反序列化器和二次 JSONObject.toJavaObject() 调用出发,在运行时记录进入 checkAutoType() 的候选名称、expectClass 与执行线程,更容易把真正可达的路径从静态依赖噪声中分离出来。
6.2 一条请求有两张地图:它怎么进来,JVM 又能去哪里
入站追踪从不可信生产者开始,依次经过 CDN、WAF、Ingress 或反向代理、应用监听端口、鉴权和过滤器,最后落到确切解析调用。研究录像里的回环 /parse 只是实验室包装器,不是 Fastjson 自带端点,也不证明互联网可达。生产核验应使用无害且符合业务模式的请求与关联 ID,确认代理确实把数据交给目标实例并进入相应解析方法;代理缓存命中、统一错误页或响应码本身都不能完成这项证明。还要排查直连容器端口、内部域名、旧版路由、备用监听器与非 HTTP 消费路径是否绕过同一策略。
出站侧要同时核对 DNS、直连 IP、JVM 代理、透明网关、sidecar、service mesh、内部对象存储和东西向网络。完整外部资源加载需要 JVM 取得候选资源,但“禁止公网”并不等于“没有取件能力”:攻击者控制的租户资源、被劫持的内部服务或宽泛代理都可能仍可达。若默认拒绝策略在所有实际 pod 或主机上生效,且遥测证明直连与代理路径均被阻断,这可以切断本次链的外部取类阶段;它不能消除 Fastjson 在本地 classpath 上的类型选择风险,也不能反证别的解析漏洞。
JDK 也要画进这两张地图。本地完整正例只在 Temurin 8u492 上跑通;SOSEC 尚未在后续 JDK 上完成相应对照,因此不能给出大版本豁免。升级到仍受支持的 JDK 本身是应做的平台维护,却不是 Fastjson 的修复证明。升级后的验收仍要看三件事:资源请求是否消失、陌生类是否仍被定义、解析入口是否还允许外来类型参与选择。只有实际观察到停止位置,才能说新版 JDK 在这套部署中切断了哪一步。
6.3 没有现成 payload 指纹时,就追它前后发生的事
没有公开且可审计的完整输入,检测就不应围绕一个猜测字符串建立。入口侧可关注解析路由上的异常延迟、重复失败、非预期类型元数据、safeMode not support autoType、autoType is not support、ClassCastException 与类名校验错误,并把请求 ID 关联到实例、线程、配置和部署摘要。这些日志只说明解析机会或拒绝结果;资源探测会吞掉部分异常,成功路径也可能保持安静,所以“没有 Fastjson 报错”不是否定证据。
网络侧应寻找 Java 工作负载在解析时间窗内首次访问的域名或地址、与业务职责不符的 HTTP 取件、短时间重复连接、直连 IP、异常代理请求和东西向会话。一次被拒绝的连接既说明出站控制可能生效,也说明相关输入值得保全;一次成功取件证明资源读取阶段可达,却仍不等于类已经定义。更强的证据来自 class-load 事件、Boot loader 定义来源异常的类、JAR 或其他文件写入、Java 子进程、凭据读取、云角色调用、控制面访问与非部署触发的重启。SOC 应按 pod、cgroup、PID、请求 ID 和时间把入口日志、JFR 或 APM、EDR/eBPF、DNS、代理与流量记录关联起来。
6.4 只有说清哪一步断了,才能把一条链判为阴性
有效的否定证据必须对应机制中的某一个必要条件。“运行时没有加载 Fastjson 1.x”要由处理目标请求的进程 code source 与类加载记录支持;“输入不可达”要覆盖反向代理、内部直连、消息和批处理入口;“SafeMode 已阻断”要同时证明实际配置实例、handler 清单和真实解析路径;“loader 不成立”要在执行解析的线程观察,而不是读取启动线程;“没有出站”要覆盖 DNS、直连、代理和东西向网络,并由拒绝遥测确认。每项结论还要绑定镜像摘要、部署副本和观察时间,因为滚动发布、线程池重建或插件更新都可能使昨天的否定证据今天失效。
| 生产状态 | 判定依据 | 处置含义 |
|---|---|---|
| 已确认完整暴露 | 未信任入口、被测 Fastjson 路径、相容的资源与类加载条件、Temurin 8u492 或部署证据证明类定义成功、可用出站与无有效 SafeMode 均由运行时证据闭合 | 立即切断入口或出站,按最高优先级迁移;尚无异常效果时仍是暴露,不等于已入侵 |
| 可能暴露 | 存在 Fastjson 1.x、fat JAR 与不可信解析,但 loader、TCCL、JDK、handler 或网络证据不全 | 缺失事实不是安全事实;在限定时限内补测,并先收缩权限与出网 |
| 已观察到资源探测 | 解析窗口出现对应 DNS 或 HTTP 请求,但没有类定义或初始化证据 | 按 SSRF 或外部取件事件保全输入与网络记录,继续追踪类加载 |
| 疑似或确认事件 | 非预期类初始化、子进程、文件、凭据、角色或控制面行为与解析窗口相关 | 立即进入事件响应,不等待 CVE、公开 payload 或 shell 回连 |
| 特定链已切断 | 真实路径证明未加载 1.x,或 SafeMode 在无 handler 路径前置拒绝,或直连与代理出站均被强制阻断;取件后的阴性还要同时证明没有类定义与初始化 | 只关闭相应链路命题,保留 Fastjson 1.x 生命周期与迁移任务 |
生产记录可以分成四组。制品组写服务、负责人、镜像、实际 Fastjson 摘要和完整 JDK;执行组写入口、数据生产者、解析 API、目标类型、ParserConfig、SafeMode、handler、default loader 与线程 TCCL;网络与权限组写入站、出站、凭据和进程权限;观测组写无害验证、遥测覆盖、观察时间与下一次复核日期。未知项就保留为未知并安排补证,不用“看起来是默认值”代填。这样既方便 SOC 关联异常,也能证明某个旧副本已经真正离开流量,而不只是从依赖报告中消失。
7 先把火隔开,再把归档的 Fastjson 1.x 搬出门
这里有两件不同的工作,不能互相等待。今天要做的是让外来 JSON 选不到 Java 类、让 JVM 取不到任意资源,并把解析进程能碰到的凭据和主机能力降到最低;接下来要做的是把已经归档的 Fastjson 1.x 从所有未信任入口撤掉。前者能迅速缩小机会,后者才结束对一条只读依赖线的长期押注。只做迁移计划,眼前暴露仍在;只开临时开关,配置漂移或旧副本又会把门打开。
7.1 SafeMode 确实拦住了本地链,前提是 handler 没有先放行
无法立即移除、且使用提供 SafeMode 的 1.2.68 及以上版本时,应优先在 JVM 启动阶段设置 -Dfastjson.parser.safeMode=true,并重启所有处理相关入口的实例。更早版本没有同等内置闸门,应优先下线入口、强制迁移、施加 schema 约束、隔离 loader 并阻断出网,而不是登记一个不存在的开关。
应用也可以在第一次解析前调用 ParserConfig.getGlobalInstance().setSafeMode(true),但程序化设置容易漏掉自建配置、初始化顺序和未重启的旧进程。验收时要逐个记录真实解析路径使用的 ParserConfig、SafeMode 值和 feature mask。SOSEC 的 1.2.83 typed 对照没有自定义 handler:打开内置 SafeMode 后,资源请求和 marker 都归零。这证明它拦住了本地所测路径,不应被误写成“SafeMode 失效”。
这个前提来自调用顺序。提交固定的 ParserConfig.checkAutoType():1316–1331先遍历已注册的 AutoTypeCheckHandler,随后才执行内置 SafeMode 拒绝。若 handler 已经返回一个类,应用自己的信任决定在 SafeMode 之前就完成了。应搜索全局注册、注解级 handler、框架模块、插件与每个自建 ParserConfig,删除没有业务必要的处理器;确需保留者只能接受有限且经过代码审查的业务类型,并对相似包名前缀、不同 loader、嵌套位置和不兼容预期类型做负向测试。
验证不能止于看到 HTTP 500。使用隔离的 marker-only 回归测试,分别覆盖应用真实的通用解析、typed DTO、消息消费者和异步 worker;预期结果是资源服务没有收到请求、marker 不出现,并且日志能定位到 SafeMode 拒绝。再用正常业务样本证明合法请求不受破坏。若某条路径仍发起取件,应先检查它是否使用另一份 Fastjson、另一份配置或 handler,而不是立刻宣布 SafeMode 的源码行为与本地对照相反。
7.2 在 JSON 进入 Fastjson 之前,拿走它选择 Java 类的权力
业务不需要多态类型元数据时,应在进入 Fastjson 之前由独立、不会先构造任意 Fastjson 对象图的组件拒绝特殊类型键与协议未定义字段。Fastjson 的默认类型键可以由进程更改,因此控制对象应是运行时实际配置、允许的字段集合和有限业务判别值,而不是只搜索一个固定字符串。确需多态时,外部判别值先映射到服务器维护的业务类型枚举,不应直接成为 Java 类名或类资源选择。具体 DTO、字段白名单、深度和集合上限能显著缩小攻击面,但本地 typed 对照已经证明,仅有固定根 DTO 不能承担完整阻断职责。
业务不再需要的导入、预览、Webhook、诊断、通用转换和旧消息消费者,应从路由或订阅层真正删除,而不是在 Fastjson 解析后才返回错误。仍保留的入口要限定调用者、租户、内容类型、原始与解压后体积、嵌套深度、并发量、超时与队列上限,并确认内部域名、直连端口和灾备副本执行同一策略。WAF 可以临时执行路径、速率、体积与已知元数据规则,却无法覆盖自定义类型键、编码和压缩差异、网关与后端规范化差异以及非 HTTP 消费路径;在没有公开完整输入时,更不能把厂商规则命中率当成永久修复证据。
出站网络应默认拒绝,并同时覆盖 DNS、直连 IP、JVM 代理、透明代理和原始套接字。只挡公网却放行所有内网,仍可能暴露元数据、对象存储、控制面与相邻服务;只配置 HTTP 代理,也不能证明其他路径不可达。确需访问的目的地按业务逐项允许,解析进程不得携带无关云凭据、部署密钥、容器管理套接字或可写共享卷,并使用非 root 身份、只读根文件系统与最小 capabilities。出站与权限控制限制的是链条机会和失陷后果,不修复 Fastjson 的早返回本身。
7.3 迁移不是改一行坐标:Fastjson2 与兼容层必须分别验
Fastjson 1.x 仓库已经归档并只读;2026 年 7 月 20 日完成的公开资料核查未发现针对这项机制的维护者修复提交或新的 1.x 修复版可供验收。永久处置不是在 1.x 版本之间寻找一个未经确认的“安全点”,而是让归档解析器不再处理未信任输入:迁移到仍维护的 Fastjson2,或改用另一条受支持且采用显式模式的解析路径。Firsov 建议迁移到 Fastjson2,但 SOSEC 尚未在 Fastjson2 原生 API 和 1.x 兼容坐标上完成本机制矩阵,因此把它视作应验收的迁移方向,而不是已经由本地证据签字的修复下限。
迁移要分别测试原生 com.alibaba.fastjson2 API 与兼容层,而不能假定二者共享同一策略。使用经过隐私处理的生产样本与人工边界用例,比较日期与时区、数字精度、空值、未知字段、泛型、字段顺序、多态映射、注解、自定义 reader/writer、异常类别、缓存和消息积压;对拒绝样本还要比较副作用缺失。影子读取不得重复写库、回调、计费或通知。fat JAR、插件目录、应用服务器共享库、旧任务包和 shaded 类也要同时清除,运行时应证明未信任路径已无法加载 Fastjson 1.x 字节,而不只是 pom.xml 改了一行。
如果维护者以后发布补丁,验收对象不能只是“第 1506 行的 return 被删了”。源码顺序至少要恢复四件事:
- 未经约束的类型文本不得先触发外部资源读取;
- 候选字节里的
@JSONType不能给这些字节自行授权; - 准备返回的每一个类都要先通过危险父类、
expectClass.isAssignableFrom()与配置策略; - 扫描的资源与最终加载的类要有可验证的来源关系,避免一个 loader 扫描 A、另一个 loader 定义同名 B。
回归用例还要同时覆盖通用解析、typed DTO、显式 default loader、不同 TCCL、SafeMode、handler、缓存过的类和合法注解模型。补丁、发布制品与这些正负结果共同成立后,才能把新版本写成永久修复;在那之前,迁移仍是主线。
回归测试要让“在哪里拒绝”可见,而不能只断言最后抛了异常。给资源 loader 加计数器,记录每个定义 loader 实际返回的类,并保证运行前 marker 不存在;负向用例只有在三项都符合预期时才通过。还应在父、子 loader 后各放一份同名但内容不同的无害类:若扫描器从 A 资源读到注解,最终却由另一 loader 定义 B 类,即使类名一致也必须判失败。
每个用例分别在全新 JVM 和合法注解模型已经进入缓存的热进程中执行,以免补丁只修冷路径。这组对照能够同时抓住“检查仍晚于外部取件”“检查的是 A、加载的是 B”和“缓存绕过新策略”三种不完整修复,又不需要命令类或回环之外的网络。
每条结果至少保存资源读取次数、定义 loader、CodeSource 或字节摘要、marker 状态与最终返回;合法注解模型还要作为正例,证明修复没有破坏满足期望类型与配置策略的应用自有类型。
存量与排队数据让迁移成为双向协议变更。应清点由 1.x 写入、且切换后仍可能被读取的缓存、延迟消息、重试与死信队列、快照、会话状态以及经过签名或哈希的 JSON;确认替代实现是否以不同方式解释旧类型提示、日期、数值形式、空值、重复键和自定义注解,再决定离线转换、受限旧读取器或到期清除。确需保留的旧读取器只能绑定封闭存储与严格 schema,继续启用 SafeMode 和出站限制,计量每次调用并设定删除日期;它不能成为任意新不可信数据的通用回退。只要新数据仍可进入这条兼容路径,迁移就没有完成。
仍停留在 JDK 8 的服务也应迁到受支持的运行时,但理由要说准确:JDK 8 已不该继续承担面向未信任数据的老旧解析入口,而不是因为本文已经证明 JDK 11、17 或 21 会挡住这条链。SOSEC 尚未跑完这些版本的本地矩阵;即便后续确认类定义被拒绝,也要单独检查资源取件是否仍发生。JDK 升级改善平台基线,SafeMode 关闭眼前分支,网络和权限控制限制后果,解析器迁移移除根因——四件事各有职责。
7.4 真正切流量前,要回答语义、安全和回滚三个问题
替换解析器的第一组验收是语义矩阵。选取经过隐私处理、能够代表各业务版本的正常生产样本,再补充空值、未知字段、重复字段、超大数字、时区与日期边界、泛型集合、枚举、继承层次和自定义序列化器等人工用例,分别送入现有路径与候选路径。比较的不是“都没有抛异常”,而是对象值、业务决策、序列化输出、持久化表示、缓存键、签名输入、错误类别与重试行为。字段顺序若无业务语义就比较对象;若签名或下游协议依赖精确字节,则必须逐字节验证。任何差异都要由业务所有者判定可接受,而不是由迁移脚本静默归一化。
第二组验收是安全矩阵。对原生 Fastjson2、兼容坐标和实际框架集成分别记录制品摘要、解析 API、类型特性、扩展 reader、mix-in 或 handler、类加载器与 JDK;使用不包含命令能力的拒绝测试,确认特殊类型元数据、未批准判别值、过深结构、超限集合和不受支持类都在取件或类定义之前终止,同时没有文件、进程或异常外联。因为 SOSEC 尚未验证 Fastjson2 兼容模式,不能把原生 API 的结果借给兼容层,也不能把默认配置结果借给启用了 SupportAutoType 或自定义 reader 的应用。
第三组验收是上线与回滚。顺序可以从离线回放、无副作用影子读取、受限调用者金丝雀、单个 worker 池逐步扩大,每一步都比较接受率、拒绝率、延迟、内存分配、队列积压、网络和主机遥测。影子路径不得重复写库、通知、计费或回调。回滚方案要预先证明旧节点能够读取候选版本写入的数据;若发生回滚,也不能连带撤销入口限制、SafeMode、handler 收窄、出站控制与进程隔离。最终关闭条件是运行中所有未信任路径都不再定义 1.x 类,且旧任务、灾备和插件副本同样清零。
每项结果都要绑定证据窗口,记录部署与镜像摘要、解析器制品、JDK 与 loader 身份、配置哈希、测试时间、参与副本、预期遥测和差异复核负责人。框架升级改变 launcher 或线程工厂、插件引入 reader 或 handler、网络策略或代理路由变化、新协议版本增加多态能力时,都要重新执行聚焦验收。这些变化会使前提已经改变的结论失效;单个原生 API、worker TCCL 或出站策略的结果不能悄悄升级为全舰队保证。
| 问题 | 通过标准 | 阻断切流量的结果 |
|---|---|---|
| 语义是否一致 | 正常与边界样本的对象值、业务决策、存储表示、缓存键、签名输入和错误类型符合已批准差异 | 出现未经业务所有者确认的数据、签名、缓存或重试变化 |
| 拒绝是否足够早 | 原生 API、兼容层和实际框架集成都在取件、类定义或其他副作用前拒绝未批准类型 | 任何资源请求、陌生类定义、marker、文件、进程或异常外联 |
| 切流与回滚是否可控 | 影子读取无副作用,金丝雀指标正常,旧数据可读;回滚不恢复旧入口、宽松 handler、出网或 1.x 字节 | 只能依靠重新暴露 Fastjson 1.x 才能恢复业务,或仍有旧任务、插件和灾备副本 |
8 本地路径已经闭合,四项未知仍不能越界推断
本地 marker 改变了一个关键判断:在被测 Fastjson、Boot loader 与 Temurin 8u492 条件下,classpath 外的类字节确实能够走到 JVM 初始化。它没有补齐原始演示未公开的候选类、loader 路径和初始化点,也没有提供真实受害者或在野利用遥测。2026 年 7 月 20 日完成的公开数据库与维护方资料核查未发现能够对应这项 2026 机制的新 CVE/GHSA、维护者修复提交或 1.x 修复版。把这些空白写清,不会削弱已经闭合的本地结论;它防止一条真实的技术链被顺手扩写成一组没有来源的事实。
| 证据来源 | 能够支持的最强结论 | 仍不能据此写成的事实 | 运营用途 |
|---|---|---|---|
| Firsov 帖文与约 8.6 秒录像 | 具名研究者声称 Fastjson 1.2.83 可实现不依赖 classpath gadget 的 RCE;画面显示本地端点后的 root shell | 未公开请求、端点代码、JAR 摘要、loader、JDK 完整构建和因果 trace,不能归因到本文机制或推广到生产 | 触发紧急调查,并把研究者的版本、DTO、SafeMode 与 2.x 说法列为待验证命题 |
| 固定的 1.2.83 上游源码 | checkAutoType() 的注解资源探测、类加载早返回与后置相容性检查在同一提交中可逐行确认 | 源码结构本身不能证明生产一定具有可读取 loader、可接受的 JDK 名称语义或可达入口 | 定位根因、设计补丁属性,并把遥测与验收放在真正改变状态的分支上 |
| Maven Central 制品与运行身份 | JAR 摘要、CodeSource、JDK 构建和 loader/TCCL 把源码解释绑定到实际执行字节 | 一次固定身份不能覆盖 shaded 副本、插件、其他镜像、异步线程或回滚环境 | 驱动资产清点,排除“构建文件已升级但运行时仍加载旧副本”的假闭环 |
| SOSEC 本地 marker 正反对照矩阵 | 固定 JAR、Temurin 8u492 与被测 Boot/JDK URL 处理条件闭合外部字节取件、类定义和初始化;SafeMode、普通 loader、typed DTO 与 TCCL 对照会改变停止点 | 抽样结果未执行命令,不能填满所有中间版本,也不能替外部演示归因、JDK 9+、Fastjson2、其他框架 loader 或在野利用签字 | 把生产调查从假设推进到条件化机制,为每项临时控制定义可观察的通过条件,并指导清点、检测与修复验收 |
| 生产因果遥测 | 同一请求和进程中的类型选择、异常资源读取、新类定义及主机效果可以确认实际暴露或事件 | 没有异常的有限窗口不能证明从未利用,单个网络事件也不能独自证明代码执行 | 决定事件等级、隔离范围、证据保全和何时从潜在 SSRF 升级为执行事件 |
8.1 生产里一旦出现取件或陌生类,就别再等漏洞编号
事件处置先确保主动行为停止,再保存容易消失的事实。隔离受影响工作负载的入站与出站,保留镜像摘要、Fastjson JAR 与外层 fat JAR 哈希、JVM 启动参数、类加载器与线程信息、进程树、打开的连接、代理与 DNS 记录、容器事件、相关请求元数据和业务身份。是否在采集前终止进程由当前破坏性决定:仍在执行或外联时,遏制优先;已经静止且取证条件可控时,先保全易失数据,并记录决定、时间与负责人。
精确恶意请求、取回对象和可疑类字节只保存在受限事件库中;每份原件与派生副本都记录采集时间、来源传感器、加密摘要、访问日志,并保留未经修改的原件。普通工单与广泛沟通渠道只传递事件编号、哈希、净化后的事件顺序和最早已证实状态,不传播可复用输入。时间统一换算为 UTC,同时保留源时区与时钟偏差;系统重建前保存请求、pod、PID、线程和消息标识。这条保管链能够区分生产证据与实验 canary,并把晚到的 parser 异常接回更早的网络或 loader 活动。
从可信镜像重建之后,轮换该工作负载可访问的凭据和令牌,检查相邻服务与历史副本,并判断排队或留存消息是否会重放同一路径。搜寻窗口应向披露日前延伸到该暴露路径首次上线或最后一次已知安全变更,而不是只从 7 月 19 日开始。被拒绝的出网说明某项控制可能生效,也说明触发输入值得保全;成功取件只证明资源可达,不能单独证明类初始化。
没有事件迹象也需要一份可审计的覆盖说明:哪些入口有请求日志,哪些 JVM 有 class-load 或 APM 数据,哪些主机有 EDR/eBPF,DNS 与代理日志保留多久,哪些消息消费者完全没有关联 ID。监控缺口不能被写成“未发现利用”。同理,实验室的 PWNED2 不进入生产 IOC;真正有用的是条件化关联——解析机会附近出现陌生取件、异常 loader 行为与主机侧后果的组合。
8.2 什么时候才可以把这次调查关掉
关闭条件不需要一套虚构的四小时、一天或七十二小时模板,只需要回答真实系统里的问题。如果运行进程没有 Fastjson 1.x,要留下 code source、类加载记录和部署摘要;如果 1.x 仍在但这条链被切断,要指出是哪个入口已经移除、哪个 SafeMode 实例在没有 handler 先行返回时于资源取件前拒绝,或哪条强制出站策略覆盖了哪些副本;如果链仍然成立,就保留暴露状态和迁移负责人;如果某个事实尚未测到,就继续写“未知”。扫描器转绿、控制器返回 500、配置仓库里出现一个 JVM 参数,都不是运行时答案。
对内和对客说明也应沿同一事实线写:哪些产品实际加载 1.x,哪些入口处理外来 JSON,已观察到的停止点在哪里,有没有资源取件、类加载或主机异常,检索了哪个遥测窗口,当前措施是什么,下一次何时更新。不要借用属于历史 Fastjson 问题的 CVE-2022-25845 填补编号空白。也不要把“当前遥测窗口没有看到异常”改写成“从未被利用”。仍在测试的事实应明确写成“验证中”。
8.3 从 2022 年的“修好”到 2026 年重新打开问题
Fastjson 1.2.83 作为最后一条 1.x 发布线中的安全更新发布;它修复的是 2022 年既有 AutoType 问题,不能把 CVE-2022-25845 复用为本次机制的标识符。
上游 1.x 仓库转为只读,迁移成为生命周期处置的一部分;归档事实本身不是本次漏洞的修复证明。
Firsov 发布 Fastjson 1.2.83 本地演示,并随后声称 1.2.68–1.2.83、无需 classpath gadget、SafeMode 可缓解及 Fastjson2 不受影响;公开材料没有提供完整输入、服务端程序或 trace。
SOSEC 在官方 Fastjson 1.2.83、Temurin 8u492 与两代被测 Boot/JDK URL 处理条件上,以只写 PWNED2 的 canary 闭合外部资源取件、注解识别、类定义和初始化;随后因父 classpath 污染作废第一轮版本矩阵,重跑并固定了 typed DTO、TCCL、SafeMode、AutoType 与十个离散版本的对照结果。
仍需验证的缺口必须原样保留。第一,SOSEC 尚未在 JDK 9、11、17、21 或其他供应商构建上确认链停在资源读取、类定义还是更早位置,因此在实际复测前不能把升级记作已经降低类定义条件,并且仍需防范 SSRF。第二,Fastjson2 原生 API 与 1.x 兼容坐标尚未完成同等矩阵,不能把“2.x 不受影响”升级为本地事实。
第三,源码起点已经由固定提交与本地矩阵共同定位到 1.2.48,但中间版本、noneautotype、Android、厂商 fork、shaded 副本、维护者确认的修复提交和 1.x 修复版仍未补齐。第四,Firsov 的原始演示与 SOSEC 本地机制之间仍缺少可审计的共同 trace;在野利用也缺少受害者、样本、时间窗和检测方法。这些缺口不延迟即时遏制,而是限定后续回归测试与生产结论的边界。
8.4 读者可以从这些原始材料继续往下查
持续监测应优先覆盖研究者原帖、维护者控制的 Fastjson 与 Fastjson2 仓库、正式公告数据库和有遥测支撑的事件报告。概念验证材料只能提出待检验命题,不能替代固定源码、制品摘要和独立对照。维护者确认或否认、可重建实验、补丁 commit、发布版、匹配的新 CVE/GHSA、可信利用证据或研究者更正,只改变其直接支持的机制、范围、修复或威胁结论;同一帖文被更多账号重复、浏览量上升或出现无归属扫描规则,不改变证据等级。
Kirill Firsov:Fastjson 1.2.83 初始主张与演示;版本范围与不依赖 classpath gadget 的后续说明;SafeMode 与迁移 Fastjson2 的后续说明;typed DTO 后续说明。
长亭安全应急响应中心复现通报;奇安信 CERT QVD-2026-43021 通报索引;Hunter 攻防实验室通报索引;腾讯云公告 2381《Fastjson 远程代码执行风险》。
Fastjson 1.2.83 固定源码树;Maven Central Fastjson 1.2.83 制品目录;Fastjson 1.x SafeMode 固定文档;Fastjson 1.x 迁移到 Fastjson2 的固定中文指南。
调查停在一个可复核的位置:固定源码说明信任检查为何迟到,本地实验已经把外来类字节推进到被测 Temurin 8u492 的初始化,受污染的第一轮矩阵也已作废并重跑;尚缺的是维护者修复、后续 JDK 与 Fastjson2 的独立矩阵、Firsov 演示的共同 trace,以及可核验的在野事件。生产团队不必等这些空白全部填完才行动。先找到真正解析数据的 JVM,在最早的可控位置关门,再证明旧字节已经离开流量。新的原始证据出现时,SOSEC 将沿同一条因果线更新相应结论,而不把新闻声量当成新的技术事实。
研究记录
9证据、对象与来源
下面保留本文实际使用的标识、时间和原始材料,便于继续调查。
9.1研究对象
报告涉及的产品、行为者、技术、受影响对象和控制点。
不得借用旧 Fastjson 漏洞编号,也不能把第三方通报编号写成维护者或标准数据库确认;这是有日期边界的核查结果。
在被测 Temurin 1.8.0_492-b09 与 Spring Boot loader 条件下得到的离散 marker-only 成功样本;不能据此断言所有中间版本均受影响。
执行边界是否成立还取决于实际资源与类加载器、TCCL、JDK、解析入口和出网条件。
已验证它会在 1.2.83 内置路径取件前拒绝;自定义 AutoTypeCheckHandler 的提前决策仍须单独审计。
固定的 1.2.83 源码中,@JSONType 早返回排在危险父类与 expectClass 相容性检查之前。
9.2事件时间
- Fastjson 1.2.83 发布
Fastjson 1.2.83 作为最后一条 1.x 发布线中的安全更新发布;它修复的是 2022 年既有 AutoType 问题,不能把 CVE-2022-25845 复用为本次机制的标识符。
- Fastjson 1.x 仓库归档
上游 1.x 仓库转为只读,迁移成为生命周期处置的一部分;归档事实本身不是本次漏洞的修复证明。
- Firsov 发布 RCE 主张
Firsov 发布 Fastjson 1.2.83 本地演示,并随后声称 1.2.68–1.2.83、无需 classpath gadget、SafeMode 可缓解及 Fastjson2 不受影响;公开材料没有提供完整输入、服务端程序或 trace。
- SOSEC 闭合 marker-only loader 链
SOSEC 在官方 Fastjson 1.2.83、Temurin 8u492 与两代被测 Boot/JDK URL 处理条件上,以只写 PWNED2 的 canary 闭合外部资源取件、注解识别、类定义和初始化;随后因父 classpath 污染作废第一轮版本矩阵,重跑并固定了 typed DTO、TCCL、SafeMode、AutoType 与十个离散版本的对照结果。
9.3来源与材料
- Kirill Firsov:Fastjson 1.2.83 初始主张与演示https://x.com/k_firsov/status/2078872293745570032
- 版本范围与不依赖 classpath gadget 的后续说明https://x.com/k_firsov/status/2078872296476057713
- SafeMode 与迁移 Fastjson2 的后续说明https://x.com/k_firsov/status/2078872298619314610
- typed DTO 后续说明https://x.com/k_firsov/status/2079058061801726124
- 长亭安全应急响应中心复现通报https://mp.weixin.qq.com/s/cfz3mrZdnobLEjQ9f6UWjA
- 奇安信 CERT QVD-2026-43021 通报索引https://weixin.sogou.com/weixin?type=2&query=%E6%97%A0%E9%9C%80%20gadget%20%E9%BB%91%E5%90%8D%E5%8D%95%E5%A4%B1%E6%95%88
- Hunter 攻防实验室通报索引https://weixin.sogou.com/weixin?type=2&query=%E3%80%90%E6%BC%8F%E6%B4%9E%E9%A2%84%E8%AD%A6%E3%80%91Fastjson%20%E8%BF%9C%E7%A8%8B%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C%E6%BC%8F%E6%B4%9E%E9%A2%84%E8%AD%A6%EF%BC%9A1.2.68%E2%80%931.2.83%20%E5%8F%97%E5%BD%B1%E5%93%8D
- 腾讯云公告 2381《Fastjson 远程代码执行风险》https://cloud.tencent.com/announce/detail/2381
- Fastjson 1.2.83 固定源码树https://github.com/alibaba/fastjson/tree/26f13f84fdd522de10678e43f55fde918ab7b347
- Maven Central Fastjson 1.2.83 制品目录https://repo1.maven.org/maven2/com/alibaba/fastjson/1.2.83/
- Fastjson 1.x SafeMode 固定文档https://github.com/alibaba/fastjson/wiki/fastjson_safemode/72c9ccb91d0de628712583f8b8a40555dcb84748
- Fastjson 1.x 迁移到 Fastjson2 的固定中文指南https://github.com/alibaba/fastjson2/blob/f68fd88fa63575b8b2956c8c5458be718023974d/docs/fastjson_1_upgrade_cn.md#L3-L38