2026/10/21 7:17:10

Fastjson反序列化漏洞全解析:从AutoType到RCE的攻防博弈

Fastjson反序列化漏洞全解析:从AutoType到RCE的攻防博弈 从2017年第一次公开绕过细节开始Fastjson就一直在安全圈保持着极高的“出镜率”。一个JSON解析库能连续多年成为漏洞研究的热点在整个Java生态里都很难找到第二个。我见过不少团队被这个库折腾得够呛有的因为线上系统用了旧版本被扫描到风险后只能连夜升级有的则是在代码审计时发现自己写的接口直接用了JSON.parseObject(input)完全控制了解析入口。其实搞懂Fastjson的漏洞原理不需要把每个利用链都背得滚瓜烂熟关键是理解它为什么会被绕过、不同版本之间卡的点在哪、以及你在评估一个目标时“出不出网”为什么是首要问题。这篇文章我就按自己的理解把1.2.24、1.2.47、1.2.80这三段经典的版本漏洞、利用思路、不出网判断方法以及修复路径完整梳理一遍。适合看这篇文章的人有三类一是做Java安全测试的同行需要快速理清漏洞脉络二是负责中间件或Java系统的运维开发想知道修复方案到底选哪个三是刚接触反序列化漏洞想把“为什么一条JSON能变成命令执行”这件事从根上搞懂的同学。1. 先搞明白一个JSON解析库为什么会变成RCE入口1.1 反序列化的本质把数据变回对象先说最基本的概念。JSON本身只是一段有格式的文本它描述的是数据。而反序列化做的事情就是把这段文本按照某种规则还原成内存里的Java对象。比如前端传过来{name:张三,age:18}服务端解析之后得到一个User对象name字段是张三age字段是18。这个流程在正常情况下没有任何问题。但Java对象的还原并不只是“把字符串塞进字段”这么简单。当一个对象的属性被赋值时Java会调用对应的setter方法当某些字段被访问时会调用getter方法。这些方法里写的是业务逻辑但也可能藏着危险动作。如果攻击者能够控制“反序列化成哪个类”以及“给哪些属性赋值”就相当于拿到了一个可以任意调用类方法的入口。Fastjson和普通的JSON库有一个关键区别它支持在JSON文本里通过type字段指定要反序列化的具体类名。也就是说解析器拿到一段JSON之后会看里面有没有type如果有就按照这个类名去加载类然后反序列化。这个特性本意是为了解决多态问题——比如一个接口有多个实现类序列化时记录下具体是哪个实现类反序列化时才能还原正确。但它同时也打开了一扇门如果这个类名和属性可以被攻击者控制攻击者就能让服务器去加载任意类。1.2 AutoType漏洞的关键入口type这个特性在Fastjson里被叫做AutoType意思是“自动类型”。它在1.2.x系列里默认是开启的不同小版本策略略有差异。解析时Fastjson会调用checkAutoType方法传入从JSON里提取出来的类名然后决定是否允许加载这个类。核心问题就出在这条加载链路上。一个类被加载之后Fastjson会查找它的setter方法并按照JSON里的字段一一赋值。如果这个类本身是一个“危险类”——比如它的某个setter或getter会触发JNDI查询、会加载字节码、会读写文件——那么攻击者就能在反序列化的过程中触发远程命令执行。打个简单的比方AutoType相当于火车站的乘客安检口。正常情况下它检查每个人的票但总有一些人能伪造票或者从别的入口溜进去。Fastjson从1.2.24到今天大部分漏洞的本质都是在和这个“安检”机制博弈。1.3 从一条JSON到命令执行中间发生了什么用最直白的方式描述整条链路服务端代码调用了JSON.parseObject(input)input是攻击者可控的HTTP请求体。解析器看到输入里有type字段提取出类名。checkAutoType对这个类名做校验校验逻辑涉及黑名单、缓存、期望类等机制。类加载通过后解析器实例化对象并逐个调用可用的setter或getter。某个setter或getter内部触发了危险操作比如JdbcRowSetImpl的setAutoCommit会触发connect()进而执行JNDI lookup。JNDI lookup返回一个远程加载的恶意类引用服务器执行了恶意代码。所以反序列化漏洞的利用通常要满足两个条件一是解析入口可控二是存在一条可用的“危险类”利用链。版本演进的本质就是在不断封堵这些危险类和绕过手段。2. 三个版本段的利用思路黑名单、缓存绕过与新探索2.1 1.2.24及以下没有黑名单的“裸奔”时代在1.2.24及更早版本中AutoType机制刚出来不久几乎没有什么完整的防护。只要目标代码用了JSON.parseObject或者JSON.parse并且能控制JSON内容攻击者就可以直接在type字段里写上任意类名。当时最出名的利用链之一是通过com.sun.rowset.JdbcRowSetImpl这个JDK自带类来完成的。这个类有一个dataSourceName属性设置后当setAutoCommit(true)被调用时会触发connect()然后对该数据源发起JNDI lookup。攻击者可以搭一个恶意的JNDI服务比如通过RMI或者LDAP协议在返回的对象里嵌入一段恶意字节码。服务器端一旦执行lookup就会把恶意类加载进JVM完成命令执行。还有另一条链是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl。这条链不需要JNDI服务因为它直接在本地通过defineClass加载字节码。攻击者可以把恶意的字节码base64编码后塞进JSON的特定字段反序列化时触发newTransformer()字节码就被加载执行。这条链的利用条件更苛刻对Fastjson版本和JDK版本都有要求但它“不出网也能打”的特点让它成为了一个经典研究案例。这个时代给人的感觉就是“完全没有防御”。开发者如果直接用了1.2.24及以下的版本攻击者只需要知道目标的JSON接口地址再准备一个可用的利用链就能尝试RCE。2.2 1.2.25到1.2.47黑名单与缓存绕过1.2.25开始官方加入了黑名单机制和checkAutoType校验。黑名单里收集了一批已知的危险类比如JdbcRowSetImpl、TemplatesImpl这些都被加了进去。同时checkAutoType会对类名做一些简单的关键字匹配拦截掉看起来可疑的加载请求。这套机制刚上线时确实挡住了一批脚本扫描。但安全问题从来不是“加个名单”就能一劳永逸的。1.2.47这个版本被曝出的绕过思路在当时的圈子引起了很大震动先利用java.lang.Class将某个类“缓存”起来再第二次加载时绕过黑名单校验。攻击者可以通过两步构造第一次先让Fastjson加载一个无害的Class对象并把目标类放进缓存第二次解析时checkAutoType发现类是缓存命中的就直接放行不再做黑名单检查。这个绕过思路的价值在于它打的不是“名单漏了某个类”的简单错误而是打在了“缓存机制”这个设计缺陷上。只要目标版本在1.2.25到1.2.47之间且AutoType没有被完全关闭攻击者就可以用同样方法重新启用黑名单中的类套用旧的利用链完成攻击。这就是为什么很多修复建议里会特别强调不要只修到1.2.47要再往上升就是因为1.2.47虽然修正了部分绕过但后续还是被不断挖掘出新的方式。实操中我看到过的情况是很多自研框架为了兼容性把fastjson版本锁在1.2.4x认为“只要不是最老的版本就安全”结果扫描器一发一个准。2.3 1.2.80旧链被封后的新探索1.2.80是2022年中被曝出存在新绕过方式的版本。在这个时间点官方已经做了大量加固工作黑名单扩充了好几轮、加入了safeMode安全模式、对Class的缓存行为也做了限制。按常理说利用的难度应该大幅提升。但安全研究员们发现在特定的期望类场景下仍然存在绕过checkAutoType的可能性。1.2.80的绕过利用思路集中在“期望类”上。简单说如果反序列化代码本身指定了某个父类或接口而攻击者传入的type是它的合法子类那么checkAutoType会直接以“期望类”身份放行跳过黑名单。这时候如果能在期望类的上下文里找到某个危险子类利用就能成立。公开讨论中比较多的入口包括Throwable、AutoCloseable这些基础类型——它们的子类数量非常多而且有些子类是第三方库提供的黑名单根本覆盖不过来。这个版本的利用难度比之前高了不少往往需要结合目标环境里实际存在的第三方库才能找到可用链条。但它说明了一个很残酷的现实只要AutoType还开着黑名单的思路就会被不断地用“新类”去挑战。你不可能通过“不停地往名单里加类”来获得永久的安全。2.4 版本对比速查版本范围核心防护机制关键绕过思路利用难度1.2.24及以下基本无防护直接指定黑名单外的危险类JNDI/字节码利用低1.2.25 - 1.2.47引入黑名单和checkAutoType借助java.lang.Class缓存绕过黑名单校验中1.2.48 - 1.2.79黑名单持续扩充限制Class缓存依赖期望类上下文或新发现的可用类中高1.2.80大量历史链路已封堵利用期望类放行机制寻找危险子类高这张表可以解释一个最常见的困惑为什么漏洞公告总是按版本号划界因为在不同的时间节点上防护机制和绕过思路确实有质的差异。排查系统时先确认fastjson的具体版本比什么都重要。3. 不出网判断评估危害时绕不开的一步3.1 “出网/不出网”到底指什么为什么先判断它“出网”这个词在利用评估里指目标服务器能否主动访问外网。为什么它如此关键因为绝大多数反序列化利用链的最经典打法都需要让服务器主动连外要么是JNDI到攻击者控制的LDAP/RMI服务要么是服务器请求一个远程URL上的恶意类或恶意配置。目标服务器如果根本无法访问外网那么这类利用方式就直接失效了。换句话说判断出不出网是决定“攻击者能不能用标准打法成功利用”的首要问题。也是防御方评估实际风险的关键步骤。一个内网深处的系统即便存在fastjson漏洞攻击者想利用它来弹出命令难度会大几个量级。理解这个判断思路能帮你在做风险评估时给出一个更客观的结论。3.2 三种常用判断手法DNS解析外呼判断。攻击者可以准备一个自己可控的域名在Payload里插入一段让目标解析该域名的逻辑。比如通过JNDI注入让服务器去lookup一个包含恶意域名的地址或者利用某些类的属性让目标发起DNS解析。如果随后在域名解析日志里看到了来自目标服务器的IP就说明“能出网”。DNS是判断出网最常用的手法因为很多网络环境即使禁了HTTP也会放行DNS而且整个过程不像HTTP请求那样容易引起警觉。HTTP请求判断。让目标发起一个到攻击者可控HTTP服务器的请求。通过观察服务器收到的连接日志确认目标是否能访问指定的TCP端口。这个方法比DNS更直接能同时确认协议层和端口层是否通畅。但它更容易被网络策略拦截如果目标环境有出网白名单或者对HTTP请求做了代理管控这个方法就会失败。延时判断。有些环境既不能DNS外呼也不放行HTTP但仍然存在一种“弱出网”的可能性——比如TCP层能出去但没有应用层协议。这时可以通过恶意类里的sleep逻辑做时间延时判断如果反序列化后服务器响应明显变慢说明代码被成功执行了只是结果传不回来。这种方式牺牲了“证据强度”但有时候是唯一可行的确认手段。3.3 判断完之后的利用方向如果目标不出网不代表漏洞完全没用只是利用路径变了。常见的思路有使用本地字节码类加载链不需要远程连接利用TemplatesImpl这类在服务器内直接拼接字节码的机制。通过错误信息回显来“带外”比如让恶意代码把执行结果写入HTTP响应的某个字段中。寻找目标内网可达的其他服务利用服务器作为跳板发起内网探测。对防御方来说判断“不出网”的直接价值是你初步评估的风险等级可以降一档。但要注意不出网只是暂时的攻击者可以在内网其他机器上做跳板所以修复建议不能因为“不出网”就无限期拖延。4. 修复方法与防御落地不要只改一行配置4.1 升级版本是第一优先级如果你确认系统在用fastjson且版本低于1.2.83那修复方案里最核心的一条就是升级。1.2.83是目前官方发布的最后稳定版本修复了1.2.80的已知绕过问题。升级时需要注意fastjson的小版本间API兼容性整体比较好但某些旧的序列化方式在新版中行为可能略有差异建议在测试环境先跑一遍bug回归测试。升级前先确认项目是通过哪种方式引入的fastjson。Maven项目直接在pom.xml里改版本号Gradle则在build.gradle里调整。如果项目里同时存在多个依赖间接引入了fastjson还需要检查依赖树确保最终生效的版本是新的。这一步经常被忽略——明明pom里写了新版本但一个间接依赖又带入了旧版本导致修复无效。4.2 AutoType安全模式把门彻底锁上单纯升级到1.2.83也不能保证100%安全因为只要代码里用了AutoType新的利用链理论上是无法被穷尽的。更彻底的做法是直接关闭AutoType。1.2.68及以上版本提供了safeMode安全模式。开启方式有三种// 方式一代码中全局开启 ParserConfig.getGlobalInstance().setSafeMode(true);// 方式二JVM启动参数 -Dfastjson.parser.safeModetrue// 方式三全局配置文件 fastjson.parser.safeModetruesafeMode开启后type字段会被完全忽略所有反序列化都按照JSON对应的类型来处理不再支持多态。这就从根本上关闭了AutoType的攻击面。如果你的业务里确实需要用到多态序列化官方也提供了一种替代方案使用Feature.SupportAutoType白名单方式只允许指定类被加载而不是一刀切全部放行。这里要提醒一点safeMode对你业务功能的影响需要提前测试。如果项目里大量依赖多态比如把各种子类对象序列化后存入Redis再读出来还原那直接开safeMode可能导致反序列化结果变成父类类型功能错乱。4.3 代码侧排查找到所有解析入口升级和开safeMode是从运行时层面防御。但还有一个事必须做代码审计。你需要找到项目里所有调用fastjson解析的地方包括JSON.parseObject(input)JSON.parse(input)JSONObject.parseObject(input)JSONArray.parseArray(input)等对每个入口确认传入的JSON内容是否可能被外部用户控制。如果解析的是自己内部配置、不涉及用户输入风险相对较低如果直接解析了HTTP请求体、消息队列的消息、第三方接口返回值就需要重点关注。把入口整理成一个清单逐个确认修复方案是否覆盖到位。在实际代码审计中我见到的典型问题是开发为了性能或方便直接在Controller里写JSON.parseObject(request.getBody())完全不做类型校验。这种代码结构给漏洞利用提供了很好的条件。4.4 修复自测清单修复不能“改完就当结束了”需要验证。这里给一份可以直接拿来用的自测清单项目自测内容预期结果版本确认检查依赖树确认实际生效版本必须是1.2.83或更高安全模式访问一个携带type字段的请求被拒绝或忽略不触发目标类加载功能回归测试项目中所有依赖反序列化的业务核心功能不报错数据还原正常依赖梳理扫描所有间接依赖是否携带旧版fastjson无遗留低版本日志检查查看升级前后日志中是否有checkAutoType异常记录无异常说明AutoType调用不再暴露这份清单在每次修复后跑一遍基本能覆盖绝大部分踩坑点。5. 常见问题与实操排查记录5.1 怎么快速确认项目里有没有用到Fastjson最简单的方法是直接搜代码依赖。Maven项目执行mvn dependency:tree | grep fastjsonGradle项目执行gradle dependencies --configuration runtimeClasspath | grep fastjson如果项目是直接解压的war包或jar包也可以搜索包里是否包含com.alibaba.fastjson目录。另外运行中的Java进程可以通过查看进程加载的jar包来确认jps -l jinfo pid | grep fastjson相比代码搜索这种方式更直观——即使代码里没发现显式import但如果某个第三方框架内部用了fastjson一样会加载到JVM里。5.2 怎么确认当前版本是否受影响确认版本号之后对照官方的安全公告和已知漏洞版本范围比较。这里有个容易被忽略的点fastjson内部版本号不一定和依赖声明的一致。有些框架做了版本封装源码里的版本号可能是内置的。最可靠的方式是直接在ClassPath下找到对应的jar包看jar包名里的版本号。如果需要进一步确认运行时的实际行为可以构造一个包含type字段的测试请求指向一个无害的类比如java.lang.String观察服务端是否返回了autotype相关错误信息。如果返回了说明AutoType解析在运行如果没有说明要么被safeMode关闭了要么请求根本没到达解析逻辑。5.3 修复后如何验证有效性升级到1.2.83并开启safeMode后可以用一个标准验证流程确认修复效果。先构造一个使用已知危险类名的type请求发送到接口预期结果是服务端响应报错或者完全不理解这个JSON的结构而不是尝试加载并执行。同时观察应用日志不应该出现对危险类的加载记录。还有一个比较容易忽略的验证点生产环境里可能要重启应用才会真正加载新的jar包和配置。如果你只是替换了jar包而没有重启旧版本依然在运行自测却看的是新代码就会得出误判。我在实际排查中遇到过一种情况运维替换了jar包但应用是通过JDK自带的类加载机制按整个lib目录加载的替换时目录里同时存在了新旧两个版本jar包最终被加载的是旧版本。所以验证时不要只看pom要确认进程内实际加载的jar包路径和版本。6. 最后再分享两点个人的经验第一点处理Fastjson这类历史漏洞心态上不要追求“一劳永逸”。它是攻击面和修复方持续对抗的典型样本。今天把1.2.83修了明天如果业务里引入了一个新的第三方库库内部如果透出了Autotype的调用面风险可能会再次出现。所以日常开发里要养成一个习惯凡是涉及反序列化的代码入口必须做类型白名单不依赖全局默认配置。第二点我在评审代码时发现不少修复只关注了“直接引用的fastjson”忽略了“间接引用”的场景。比如某个中间件客户端内部封装了fastjson或者某个消息序列化组件间接依赖了它。这种间接依赖在升级主版本后仍可能传递旧版本造成漏修。花十分钟梳理一次完整依赖树比事后在线上排查到半夜要划算得多。Fastjson的攻防历史是整个Java反序列化安全问题的一个缩影。理解它不只是为了修好一个库也是为了在面对其他序列化组件时能更快地判断风险、设计防御方案。