迟来的SpringMVC
框架RCE
分析。本文章简单介绍了SpringMVC
框架请求处理流程,并以此对漏洞进行了分析与复现。
框架浅析
SpringMVC
其本质上是一个Servlet
,它的请求处理主要是在DispatcherServlet
中,这里大概有四步:
- 1. 根据
Request
找到Handler
- 2. 根据
Handler
找到HandlerAdapter
- 3. 用
HandlerAdapter
调用Handler
处理请求 - 4. 处理结果并渲染输出给用户
借用一张图来看下这个流程
Handler
是用来处理请求,SpringMVC
内置了大量的Handler
,我们重点关注下其中对参数进行处理的,主要是HandlerMethodArgumentResolver
和HandlerMethodReturnValueHandler
,前者表示一个参数解析器,后者除了解析参数之外还可以处理相应类型的返回值。以下是HandlerMethodArgumentResolver
的实现类
它们基本上都实现了:
代码语言:javascript复制public boolean supportsParameter(MethodParameter parameter) //和
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)
当RequestMapping
对应参数符合supportsParameter
会使用resolveArgument
解析请求,并最终得到参数的值传入RequestMapping
,这里以RequestParamMapMethodArgumentResolver
简单介绍下:
@Override
public boolean supportsParameter(MethodParameter parameter) {
RequestParam requestParam = parameter.getParameterAnnotation(RequestParam.class);
return (requestParam != null && Map.class.isAssignableFrom(parameter.getParameterType()) &&
!StringUtils.hasText(requestParam.name()));
}
@Override
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
......
else {
Map<String, String[]> parameterMap = webRequest.getParameterMap();
Map<St ring, String> result = CollectionUtils.newLinkedHashMap(parameterMap.size());
parameterMap.forEach((key, values) -> {
if (values.length > 0) {
result.put(key, values[0]);
}
});
return result;
}
}
首先看其支持类型,需要有RequestParam
注解,且参数类型为Map
,所以可以定义如下接口:
@ResponseBody
@RequestMapping("/mvc/world")
public String world(@RequestParam HashMap<String, String> map) {
return "successfuladd";
}
该接口就会被RequestParamMapMethodArgumentResolver
处理,很容易看出这里简单的做了个类型转换,这里的result
就是我们需要的参数了。
有趣的是这里如果两个相同参数的请求,其只会取第一个的值,而如果是RequestParamMethodArgumentResolver
进行处理时会把两个参数值通过,
进行连接。
部分解析器及其作用:
漏洞分析
前面扯了那么多,现在终于是进入正题了,先来搭建下漏洞环境:
- • JDK:11.0.14
- • Tomcat:9.0.60
- • Spring 5.3.17
主要代码如下:
代码语言:javascript复制@Controller
public class TestController {
@ResponseBody
@RequestMapping("/mvc/hello")
public String hello(User user) {
System.out.println(user.getName());
return "success";
}
}
//User
public class User {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
PoC:class.module.classLoader.resources.context.parent.pipeline.first.pattern=***
单独看PoC
可能会疑惑这个参数是怎么来的,所以这里要结合着环境进行分析。可以看到hello
的参数User
,这是一个没有注释的非通用类型参数,而上文中有提到不同参数类型的解析器也不一样,现在的情况会由ModelAttributeMethodProcessor
进行处理,跟进其resolveArgument
方法,它会尝试从当前请求中获取值并绑定到user
上。
一路跟进bindRequestParameters
函数直到org.springframework.validation#applyPropertyValues
。
这里经过getPropertyAccessor()
我们实际上获取到了一个User
对象的BeanWrapper
实例。
在这里我们补充下BeanWrapper
相关的内容,在Spring
中,BeanWrapper
接口是对Bean
的包装,定义了对包装对象的属性值的访问与修改的接口,BeanWrapperImpl
则是对BeanWrapper
的默认实现,BeanWrapperImpl
类有多个设置bean
属性值的重载方法,其中就有public void setPropertyValue(PropertyValue pv)
,PropertyValue
以对象的方式存储键值对,比Map
使用起来要灵活,通过BeanWrapperImpl
设置属性值:
public class BeanWrapperTest {
public static void main(String[] args) {
User user=new User();
BeanWrapper bw= PropertyAccessorFactory.forBeanPropertyAccess(user);
bw.setPropertyValue(new PropertyValue("name","bean"));
System.out.println(user.getName());
}
}
也可以通过getPropertyDescriptors
获取所有属性值:
public class BeanWrapperTest {
public static void main(String[] args) {
User user=new User();
BeanWrapper bw= PropertyAccessorFactory.forBeanPropertyAccess(user);
for (PropertyDescriptor p :
bw.getPropertyDescriptors()) {
System.out.println(p.getName());
}
}
}
//output
class
name
可以看到除了除了name
之外还会有一个class
,那这是不是说明class
也可以被我们修改呢?看一下setPropertyValue
的代码,它会进入getPropertyAccessorForPropertyPath
,它支持两种方式的属性值,一种是直接用name
进行操作,一种则是user.name
的形式进行递归逐步获取到user
后对name
进行操作,这里对第二种情况进行分析:
添加新类God
:
public class God {
private String name;
public String getName(){
return name;
}
public void setName(String name){
this.name = name;
}
}
同时在User
中加入:
private God god = new God();
public God getGod(){
return god;
}
最后运行
代码语言:javascript复制public class BeanWrapperTest {
public static void main(String[] args) {
User user = new User();
BeanWrapper bw= PropertyAccessorFactory.forBeanPropertyAccess(user);
bw.setPropertyValue(new PropertyValue("god.name","bean"));
System.out.println(user.getGod().getName());
}
}
第一次解析god
,如果之前未解析过bean
类,首先会对该类进行分析并缓存,使用的方法是CachedIntrospectionResults.forClass
,在获取到所有get,set
方法后循环判断了该类为Class
的同时属性是不是classLoader
,防止了直接class.classLoader
来进一步获取值
缓存之后就开始获取属性值了,如果该属性可读的话就会在getValue
时执行其get
方法,这里的Value
就是God
实例。
最后会以该实例生成一个新的nestedPa
返回并进入第二次循环。
不过第二次时已经没有.
了,所以直接返回this
,也就是god
,并以此知道要设置的值为god.name
,所以后续就进入了设置属性值的流程,只有当该属性值存在且可写的情况下才可以继续往下执行。
至此整个流程就结束了,让我们回到漏洞
setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields())
其实也调用了setPropertyValue(PropertyValue pv)
那么结合上文对setPropertyValue
流程的分析,其实我们已经大致理解了payload
的格式,包括为什么用class.module.classLoader
而不是直接class.classLoader
。在Tomcat
中是ParallelWebappClassLoader
,而且其有一个属性getResources
,就这样层层递归,最终操作日志,达成任意文件写入,从而实现RCE
,在SpringBoot
的LaunchedURLClassLoader
中并不存在getResources
所以直接使用SpringBoot
的情况下上述Payload
是不起作用的。
修复方案
针对该漏洞Spring
以及 Tomcat
都做出了修复。
Spring:
Class
类仅可以获取name
相关的值了,而且对没有写操作权限的ClassLoader
以及ProtectionDomain
做了限制。
Tomcat
则是直接把getResources
返回为空了。
参考文章
- • https://paper.seebug.org/1877/
- • https://blog.csdn.net/zhiweianran/article/details/7919129
- • http://rui0.cn/archives/1158