1、前言
Spring Cloud Function是基于Spring Boot 的函数计算框架(FaaS),当其启用动态路由functionRoute时,HTTP请求头spring.cloud.function.routing-expression参数存在SPEL表达式注入漏洞,恶意攻击者可通过此漏洞进行远程命令招待漏洞
2、影响版本
3.0.0.RELEASE <= Spring Cloud Function <= 3.2.2
3、漏洞复现
本次复现的漏洞环境是Spring Boot 2.6.5 和Mac系统,通过"spring.cloud.function.routing-expression:T(java.lang.Runtime).getRuntime().exec("open -a calculator.app")" 来触发漏洞。
通过错误信息分析,安全问题可以在RoutingFunction.java文件中进行分析,我们将断点设定127行,进行单步跟进。
从调试信息来看,可以清楚地看出获取Headers的数据,由函数functionFromExpression来处理。
private FunctionInvocationWrapper functionFromExpression(String routingExpression, Object input) {
Expression expression = spelParser.parseExpression(routingExpression);
if (input instanceof Message) {
input = MessageUtils.toCaseInsensitiveHeadersStructure((Message<?>) input);
}
String functionName = expression.getValue(this.evalContext, input, String.class);
Assert.hasText(functionName, "Failed to resolve function name based on routing expression '" + functionProperties.getRoutingExpression() + "'");
FunctionInvocationWrapper function = functionCatalog.lookup(functionName);
Assert.notNull(function, "Failed to lookup function to route to based on the expression '"
+ functionProperties.getRoutingExpression() + "' whcih resolved to '" + functionName + "' function name.");
if (logger.isInfoEnabled()) {
logger.info("Resolved function from provided [routing-expression] " + routingExpression);
}
return function;
}
在functionFromExpression函数中,参数routingExpression会被传入到spelParser.parseExpression()方法中。
private FunctionInvocationWrapper functionFromExpression(String routingExpression, Object input) {
Expression expression = spelParser.parseExpression(routingExpression);
if (input instanceof Message) {
input = MessageUtils.toCaseInsensitiveHeadersStructure((Message<?>) input);
}
String functionName = expression.getValue(this.evalContext, input, String.class);
Assert.hasText(functionName, "Failed to resolve function name based on routing expression '" + functionProperties.getRoutingExpression() + "'");
FunctionInvocationWrapper function = functionCatalog.lookup(functionName);
Assert.notNull(function, "Failed to lookup function to route to based on the expression '"
+ functionProperties.getRoutingExpression() + "' whcih resolved to '" + functionName + "' function name.");
if (logger.isInfoEnabled()) {
logger.info("Resolved function from provided [routing-expression] " + routingExpression);
}
return function;
}
在判断context是否为null值时,会进入doParseExpression(expressionString, context)。
再new一个 InternalSpelExpressionParser 类调用 doParseExpression方法,继续跟进。
protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context)
throws ParseException {
try {
this.expressionString = expressionString;
Tokenizer tokenizer = new Tokenizer(expressionString);
this.tokenStream = tokenizer.process();
this.tokenStreamLength = this.tokenStream.size();
this.tokenStreamPointer = 0;
this.constructedNodes.clear();
SpelNodeImpl ast = eatExpression();
Assert.state(ast != null, "No node");
Token t = peekToken();
if (t != null) {
throw new SpelParseException(t.startPos, SpelMessage.MORE_INPUT, toString(nextToken()));
}
Assert.isTrue(this.constructedNodes.isEmpty(), "At least one node expected");
return new SpelExpression(expressionString, ast, this.configuration);
}
catch (InternalParseException ex) {
throw ex.getCause();
}
}
在此函数中,会使用到tokenizer.process()对token进行源码和字节的判断。之后将会new SpelExpression类,并将expressionString传递过去。
在SpelExpression函数中,进行参数$expression的赋值。然后跟进doParseExpression和
parseExpression,然后进入functionFromExpression函数。
protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context)
throws ParseException {
try {
this.expressionString = expressionString;
Tokenizer tokenizer = new Tokenizer(expressionString);
this.tokenStream = tokenizer.process();
this.tokenStreamLength = this.tokenStream.size();
this.tokenStreamPointer = 0;
this.constructedNodes.clear();
SpelNodeImpl ast = eatExpression();
Assert.state(ast != null, "No node");
Token t = peekToken();
if (t != null) {
throw new SpelParseException(t.startPos, SpelMessage.MORE_INPUT, toString(nextToken()));
}
Assert.isTrue(this.constructedNodes.isEmpty(), "At least one node expected");
return new SpelExpression(expressionString, ast, this.configuration);
}
catch (InternalParseException ex) {
throw ex.getCause();
}
}
在functionFromExpression函数中,会调用MessageUtils.toCaseInsensitiveHeadersStructure() ,通过
MessageStructureWithCaseInsensitiveHeaderKeys()来获取message的headers的数据。
public static MessageStructureWithCaseInsensitiveHeaderKeys toCaseInsensitiveHeadersStructure(Message<?> message) {
return new MessageStructureWithCaseInsensitiveHeaderKeys(message);
}
最后进入漏洞触发函数execute函数进行命令执行
public TypedValue execute(EvaluationContext context, Object target, Object... arguments) throws AccessException {
try {
this.argumentConversionOccurred = ReflectionHelper.convertArguments(
context.getTypeConverter(), arguments, this.originalMethod, this.varargsPosition);
if (this.originalMethod.isVarArgs()) {
arguments = ReflectionHelper.setupArgumentsForVarargsInvocation(
this.originalMethod.getParameterTypes(), arguments);
}
ReflectionUtils.makeAccessible(this.methodToInvoke);
Object value = this.methodToInvoke.invoke(target, arguments);
return new TypedValue(value, new TypeDescriptor(new MethodParameter(this.originalMethod, -1)).narrow(value));
}
catch (Exception ex) {
throw new AccessException("Problem invoking method: " + this.methodToInvoke, ex);
}
}
4、修复方案
确定受影响版本3.0.0.RELEASE <= Spring Cloud Function <= 3.2.2,可按官方建议升级修复
https://github.com/spring-cloud/spring-cloud-function/commit/0e89ee27b2e76138c16bcba6f4bca906c4f3744f