JavaSec系列 - 5. SpEL注入

本章源码: https://github.com/hey3e/JavaSec-Code/tree/main/javasec5

先前,我们在系列2中jdk>8u191的环境下实现JNDI注入时,使用到了EL表达式。SpEL,是EL的一种,”Sp”代表”Spring”,即用于Spring的表达式。而最近,Spring Cloud爆出两个RCE,其原理均为SpEL注入,因此我们下面通过接触这两个实例来了解SpEL注入的实现。

(1) SpEL基础

下面是SpEL表达式解析的过程:

1
2
3
4
5
6
7
void testSpEL() throws Exception {
ExpressionParser parser = new SpelExpressionParser(); //a
Expression expression = parser.parseExpression("('Hello' + ' World').concat(#end)"); //b
EvaluationContext context = new StandardEvaluationContext(); //c
context.setVariable("end", "!"); //d
System.out.println(expression.getValue(context)); //e
}

输出为Hello World!。其中:

  • a:通过SpelExpressionParser类构造解析器。
  • b:传入SpEL表达式。
  • c:通过StandardEvaluationContext构造上下文。
  • d:在上下文中,我们可以自定义变量、函数等。
  • e:调用getValue方法,根据上下文解析SpEL表达式。

我们重点来关注一下SpEL如何来实现类:

1
2
3
4
5
public void testClass() {
ExpressionParser parser = new SpelExpressionParser();
Class<String> stringClass = parser.parseExpression("T(String)").getValue(Class.class);
System.out.println(stringClass);
}

输出如下:

可知,通过SpEL表达式T(type),可以实现类型为type的类。非常像反射。意味着如果攻击者可以控制传入的SpEL表达式,就可能实现RCE。

(2) Spring Cloud Gateway RCE (CVE-2022-22947)

Spring Cloud Gateway (以下简写为SCG),提供了Spring上的路由功能。

默认情况下,SCG的属性management.endpoint.gateway.enabledtrue,即允许远程地调用/actuator/gateway对路由进行配置。如POST到/gateway/routes/{id_route_to_create}添加路由,并再次POST到/actuator/gateway/refresh以使配置生效。

下面开始分析其是否存在SpEL注入的可能。我们先来看SCG源码,根据SpEL基础,我们全局搜索SpelExpressionParser,在ShortcutConfigurable接口中,我们找到了getValue方法:

可见,若entryValue以”#{“开头、以”}”结尾,则将其看作SpEL表达式并进行解析。因此,若entryValue可控,则可能实现SpEL注入。那接下来的任务便是寻找entryValue的来源。

向上回溯,ShortcutConfigurable接口下的三个ShortcutType枚举均调用了getValue方法:

注意entryValue的传入,即entry.getValue(),可知来自于键值对形式的args变量的值。

继续向上,normalize()方法调用自ConfigurationService类的normalizeProperties()方法,而args即该类的properties属性:

normalizeProperties()调用自同类下的bind()

其中强调了nameproperties属性不能为空。

bind()RouteDefinitionRouteLocator类的loadGatewayFilters()中被调用:

着重分析下该方法。可见,参数filterDefinitions包含多个filter的定义,在127行中,将filter实例化为GatewayFilterFactory对象,并自138行开始configure:name属性即filter的name,properties属性即filter的args。

到这里,payload的构造已经清晰:我们要创建一个路由,其filter是实现了GatewayFilterFactory接口的对象,args包含恶意的SpEL表达式。

随意选择一个实现GatewayFilterFactory的类,如Retry

结合官方提供的路由创建样例,构造payload:

1
2
3
4
5
6
7
8
9
{
"id": "javasec",
"filters": [{
"name": "Retry",
"args": {
"value": "#{T(java.lang.Runtime).getRuntime().exec(\"calc\")}"
}
}]
}

此处args中SpEL表达式的key,经测试可随意更改。

路由创建成功后,返回201 Created:

刷新路由,弹出计算器,实现SpEL注入:

查看调用栈,与前文分析一致:

由于是本地测试,我们可以选择任意一个实现了GatewayFilterFactory的类作为filter。但若是远程测试需要回显,则需要选择AddResponseHeaderGatewayFilterFactory类。

(3) Spring Cloud Function RCE

老样子,在Spring Cloud Function (以下简写为SCF) 框架中,全局搜索SpelExpressionParser,定位到RoutingFunction类的functionFromExpression的方法:

很明显,要利用的目标是routingExpression。向上跟:

可见,routingExpression来自于请求头中的spring.cloud.function.routing-expression参数。而想要触发SCF的function routing功能,根据官方文档,要访问的接口应为functionRouter,其在RoutingFunction类中也有定义:

构造POST请求:

发现未能弹出计算器。调试发现,在route()方法中,传入的inputFluxEmpty类,因此未能进入functionFromExpression方法:

向上追溯到FunctionController类的post()方法中,会发现input来自于请求的body:

于是加入body,再次请求,实现SpEL注入:

调用栈如下:

另外,官方文档提供了访问任意路径触发function routing的方法,即在application.properties中添加如下属性:

测试:

(3) Countermeasures

SCG的漏洞修复,可以设置management.endpoint.gateway.enabled属性为false以拒绝远程调用。

更为通用的修复方法,我们可以看到两个框架中对SpEL的处理均使用的StandardEvaluationContext

StandardEvaluationContext提供了全部SpEL语法,因此,将其替换为不能实例化类的SimpleEvaluationContext会更加安全。

Author

yekc1m

Posted on

2022-04-01

Updated on

2022-04-02

Licensed under