Spring Cloud Function SpEL远程命令执行漏洞分析与修复

 

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

  • 1
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

信息安全方案

搜索「我的资源」,获取免费下载

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值