Files

372 lines
17 KiB
Markdown
Raw Permalink Normal View History

2026-08-11 07:52:33 -04:00
# CVE-2026-41843 Spring Framework路径遍历漏洞浅析
> QIANXIN Team
> 来源:https://forum.butian.net/share/4953
# 0x00 CVE-2026-41843
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-30f0e42ef5c0138b248863ee65707b3695133f6f.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 Spring中的**资源版本控制**
静态资源缓存是 Web 性能优化的基础手段。
浏览器会长期缓存 CSS、JS、图片等静态资源,避免重复请求。但当服务端资源更新时,如果 URL 保持不变,浏览器会继续使用本地缓存,导致用户无法获取最新内容。
资源版本控制(也叫缓存击穿 / Cache Busting)就是为了解决这个问题:在静态资源的 URL 中嵌入版本标识,资源更新时版本标识同步变化,让浏览器将其视为全新的资源 URL,强制重新加载。
Spring Framework 提供了标准化的静态资源版本控制能力,通过资源处理链(Resource Chain) 实现:
- 资源处理链由一系列 `ResourceResolver`(资源解析器)和 `ResourceTransformer`(资源转换器)按顺序组成;
- 其中 `VersionResourceResolver` 是版本控制的核心解析器,负责识别请求中的版本号、剥离版本号后定位真实资源;
- Spring 内置了两种开箱即用的版本策略:`FixedVersionStrategy`(固定版本策略)和 `ContentVersionStrategy`(内容哈希版本策略)。
这一版本化机制也是 CVE-2026-41843 路径遍历漏洞的必要触发前提:只有应用显式配置了带版本策略的资源处理链,漏洞才有可能被利用。
## 1.1 FixedVersionStrategy
FixedVersionStrategy可以使用某项属性,或者日期之类的作为版本。
其使用一个全局统一的固定字符串作为版本号,官方内置实现为路径前缀式,版本号统一添加在资源路径的最前端,例如 `/v1.0.0/css/style.css`。
所有资源共用同一个版本号,配置简单,版本号与单个文件内容无关,随应用整体发版同步更新。
下面是具体的demo:
首先通过实现 `WebMvcConfigurer` 配置静态资源映射与版本策略:
```Java
@Configuration
public class FixedVersionConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/static/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new VersionResourceResolver()
.addFixedVersionStrategy("v1.0.0", "/**"));
}
}
```
在 `src/main/resources/static/css/` 目录下创建 `style.css`:
```CSS
body {
margin: 0;
padding: 0;
color: #333;
}
```
启动应用后,访问带版本前缀的 URL 即可正常获取资源:
```Plain
/static/v1.0.0/css/style.css
```
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-7b827a30e7ff14b19776999793bfd53d45ded137.png)
Spring在解析时会自动剥离路径前缀 `v1.0.0`,最终映射到 `classpath:/static/css/style.css`。
## 1.2 ContentVersionStrategy
ContentVersionStrategy会基于每个资源文件的内容计算 MD5 哈希值,将哈希作为版本号嵌入到文件名中。版本号会插入在文件名主体和扩展名之间,例如 `style-2f9a2f4b5c6d.css`;
由于会根据文件内容生成唯一哈希,文件内容不变则哈希不变,缓存持续有效,内容更新则哈希自动变化,触发浏览器缓存刷新。
下面是具体的demo:
首先通过实现 `WebMvcConfigurer` 配置静态资源映射与版本策略:
```Java
@Configuration
public class ContentVersionConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/res/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new VersionResourceResolver()
.addContentVersionStrategy("/**"));
}
}
```
可以定义一个辅助获取版本 URL 的接口,内容哈希由文件内容动态计算,可通过 `ResourceUrlProvider` 获取带版本号的完整资源 URL:
```TypeScript
@RestController
public class VersionController {
@Autowired
private ResourceUrlProvider resourceUrlProvider;
@GetMapping("/get-version-url")
public String getVersionUrl() {
return resourceUrlProvider.getForLookupPath("/res/css/style.css");
}
}
```
同样的在`src/main/resources/static/css/` 目录下创建 `style.css`。
启动应用后,获取对应的版本号,拼接获得对应的URL:
```Plain
/res/css/style-7345fe45f851042f3865836ec904d1ce.css
```
直接访问该 URL 即可正常获取资源;修改文件内容后重新请求,哈希值会自动同步变化。
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-6da4150cadfce62a737fa472a7a5d0456e70c225.png)
# 0x02 漏洞分析与复现
## 2.1 分析过程
以org.springframework:spring-webmvc:6.2.17为例:
基于上面的背景,简单分析下具体静态资源版本的解析过程:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-b96820b10cba2a7bbdae4c7aac2009ce8242fc50.png)
这里实际会调用VersionResourceResolver#resolveResourceInternal方法进行处理:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-d54beceaf82bfa50928c7aa7e5c95cbfe1be3893.png)
resolveResourceInternal方法是 Spring 静态资源版本化机制的核心执行入口,职责是识别请求中的版本号、剥离版本号后定位真实资源、并完成版本一致性校验,最终返回包装后的版本化资源对象。
下面逐个进行分析:
首先直接把原始请求路径交给解析链后续的解析器(通常是 `PathResourceResolver`)尝试查找资源,这里是为了兼容不带版本号的普通静态资源请求,如果路径能直接命中资源,直接返回,跳过后续复杂的版本提取、校验流程:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-f8efe38c91d687d782455bee51e08954d3e4255c.png)
然后是匹配路径对应的版本策略,根据请求路径,匹配预先配置的版本策略(固定版本 / 内容哈希),不同的 URL 模式可以对应不同的策略,若无匹配策略直接返回 null,说明该路径不需要版本化处理:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-ffde175077aca46f31b315b9e5ec18f5b269df2b.png)
然后调用对应策略的extractVersion方法,从请求路径中解析出版本号字符串,提取失败说明路径格式不合法、不带版本号:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-60d2540dfc5472b966d611f80b328913c292c747.png)
然后调用对应策略的removeVersion方法,从请求路径中移除版本号,还原出真实的资源查找路径,然后用相关的路径解析真实的资源:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-307e8dc4b02890f5e86a33d6de8cbc9b1436a04f.png)
如果相关的资源真实存在,那么计算对应的版本号,和请求携带的候选版本做对比:
- 如果一致:将原始资源包装为 `FileNameVersionedResource` 返回,携带版本元数据
- 不一致:判定为非法请求,返回 null
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-8a4e5b0f60344f04d989c9618653ec250a9423c1.png)
以上便是VersionResourceResolver大致的解析过程。
前面提到了,在解析时,会根据匹配的策略,分别调用对应的extractVersion、getResourceVersion方法以及removeVersion方法进行处理,下面看看FixedVersionStrategy和ContentVersionStrategy的区别:
- **FixedVersionStrategy**
可以看到,FixedVersionStrategy的处理是在PrefixVersionPathStrategy中实现的:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-d8f90071c45c2af3ec26df1784ff4cc1fc14fad3.png)
其中getResourceVersion方法比较简单,定义的version是什么就返回什么。
另外两个方法可以看到,FixedVersionStrategy主要检查路径开头是否匹配版本前缀,在剔除版本号时,会直接截断对应的版本内容:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-aa0fbc4607a08c8b8b852c51da2eea9f664990b3.png)
按照之前的demo,那是不是意味着如果以如下URL进行访问,即可成功引入路径穿越符:
```Plain
/res/v1.0.0../application.properties
```
但是实际复现时会发现,Spring本身存在一定的拦截机制,并不能完美引入`../`
- **ContentVersionStrategy**
可以看到,FixedVersionStrategy的处理是在FileNameVersionPathStrategy中实现的:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-73bdef20ea25af371c699564325f9958b64cb38b.png)
其中getResourceVersion方法就是根据文件内容计算MD5 Hash。
查看FileNameVersionPathStrategy具体方法定义,它会在路径中查找所有形如 `-xxx.` 的片段,并把中间的 `xxx` 捕获出来,如果捕获到的内容里还包含横杠,就取最后一个横杠之后的子串作为最终版本号。
而在删除版本号的时,`StringUtils.delete` 的作用是删除字符串中所有出现的目标子串,也就是说会把整条路径里所有 `-版本号` 子串全部无差别删掉:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-b9207be8241b806396cdb2f6423890828a096792.png)
基于`StringUtils.delete`,那么前面提到的无法完美引入就迎刃而解了,例如下面的例子,在移除版本号后,确实可以得到`../`,但是进一步调试发现,依旧会因为种种限制导致无法利用:
```Plain
/res/css/.-7345fe45f851042f3865836ec904d1ce./.-7345fe45f851042f3865836ec904d1ce./application%2eproperties
```
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-5c083b599c9158bb08cafac1c2ee0bcec48de1da.png)
## 2.2 利用限制
在1.1节提到了两种策略解析时的利用思路,但是都因为一些限制的原因导致无法利用,下面简单整理下具体的限制。
### 2.2.1 ResourceHttpRequestHandler
SpringMVC主要是利用ResourceHttpRequestHandler来处理静态内容的,它对静态资源的映射提供了默认的配置。
其在解析资源之前,会对请求的path进行合法性检查,这也就解释了为什么没办法直接在路径中引入`../`
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-f9133cc5e0122d08cc79b74fb6151b079f5ee7fb.png)
### 2.2.2 版本一致性检查
前面提到,ContentVersionStrategy在移除版本时,会把所有版本号都移除掉,那确实可以规避ResourceHttpRequestHandler的安全检查,但是即使获取到了资源路径,在最后版本一致性检查时,会因为目标与恶意获取的文件名hash不一致,导致无法正常获取文件:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-8713fe550ac8fd26c8aef6607e5b2402a0f501fb.png)
### 2.2.3 资源读取限制
除此之外,在获取实际路径的资源时,同样存在限制:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-5350299957bd34afdd10e19d886bf67cc70071f6.png)
这里会调用PathResourceResolver#getResource方法进行处理:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-c3ec5ef3cd2aabd6ee6eab8d2f95dd744a5446be.png)
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-ab6354fcfceccb8d1d8cdc8ec2026496dc355ba9.png)
如果资源路径可达,这里会判断对应的资源是否在允许的location下,如果不允许,会返回null:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-ba2892ebbda98b80d4c76ca83d4862788a4e91ef.png)
## 2.3 漏洞复现
前面提到了很多利用层面的限制,例如Spring 原生的 `checkResource`方法 兜底确实能拦住标准场景,但生产环境中很多业务为了支持跨目录资源、软链接、自定义存储协议等需求,会自定义 `PathResourceResolver` 并弱化 / 关闭边界校验。这是该漏洞最主要的实战危害场景,也是官方将其定级为中等危害的核心依据。
另外,还可以考虑不同中间件解析差异带来的绕过,这里不进一步展开。
下面简单编写具体的demo进行漏洞复现:
首先自定义后缀式固定版本策略,版本号直接定义为v1,FileNameVersionPathStrategy直接复用AbstractVersionStrategy.PrefixVersionPathStrategy内容:
```Java
public class FixedFileNameVersionStrategy extends AbstractVersionStrategy {
public FixedFileNameVersionStrategy() {
super(new com.atguigu.boot.strategy.FileNameVersionPathStrategy());
}
@Override
public String getResourceVersion(Resource resource) {
return "v1";
}
}
```
然后自定义一个资源解析器,为了方便直接返回true:
```Java
public class UnsafePathResourceResolver extends PathResourceResolver {
@Override
protected boolean checkResource(Resource resource, Resource location) {
return true;
}
}
```
然后实现 `WebMvcConfigurer` ,指定自定义的静态资源映射与版本策略:
```TypeScript
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/res/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new VersionResourceResolver()
.addVersionStrategy(new FixedFileNameVersionStrategy(),"/**"))
.addResolver(new UnsafePathResourceResolver());
}
}
```
resources的目录结构如下:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-17ee6229efd2e3e2a651c4717a53f1aca3628b20.png)
这里尝试读取application.properties:
```Plain
/res/css/.-v1./.-v1./application%2eproperties
```
可以看到成功读取对应的文件信息:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-5cb6cbaca978a819ac305d28309f531da3691470.png)
如果使用File协议的话,还可以尝试读取/etc/passwd,有兴趣的师傅可以尝试下。
# 0x03 修复方案
以修复版本org.springframework:spring-webmvc:7.0.8为例:
**主要修复了两处:**
- **ResourceHandlerUtils#shouldIgnoreInputPath方法**
在VersionResourceResolver 的资源解析流程中,剥离版本号得到最终路径后,新增了ResourceHandlerUtils#shouldIgnoreInputPath方法调用进行校验:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-404a0d7ed16d514d9d6837a8999ceebd94a1cffa.png)
shouldIgnoreInputPath主要由三个方法组成:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-01ca24b74b4674f2b3749cb4b02f4ada57316024.png)
核心方法主要是isInvalidPath方法,里面主要对类似`../`以及一些敏感目录的输入进行了检查:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-1f53724d3ba9e64a86595e5cf958df5315ffe8b9.png)
isInvalidEncodedPath方法主要是考虑到URL编码的情况,解码后继续调用isInvalidPath方法进行判断:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-8faa2ee32e971fdaae8ba9502581ad0fc7183ee8.png)
- **FileNameVersionPathStrategy#removeVersion**
修复前,会直接扫描整个请求路径,删除所有出现的 `"-" + version` 子串,出现多少次就删多少次,不限制位置(目录名、文件名中的匹配项都会被删掉):
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-51c469f90ca0c4044c3fa9b529627d2536dc9bf3.png)
修复后,找到 `-` + `version` 在路径中最后一次出现的位置,只删掉这一处匹配项,路径前面所有的匹配内容完全保留。只针对路径末端的文件名部分操作,不会触碰前面的目录层级:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-8dc070b903144c04739ff5df73fd583393a45c0b.png)
可以看到,同样的环境跟poc,在修复后,被应用进行了拦截,无法进一步进行路径遍历漏洞利用:
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-8658af3cf2f0284aac895e24b2a40aed67e2f923.png)
![image.png](https://cdn-yg-zzbm.yun.qianxin.com/attack-forum/2026/07/attach-d8e48aed70a5ed630ea908ec06fed131da1cdea3.png)