迟来的SpringMVC 框架RCE分析。本文章简单介绍了SpringMVC框架请求处理流程,并以此对漏洞进行了分析与复现。
SpringMVC其本质上是一个Servlet,它的请求处理主要是在DispatcherServlet中,这里大概有四步:
Request找到HandlerHandler找到HandlerAdapterHandlerAdapter调用Handler处理请求借用一张图来看下这个流程

Handler是用来处理请求,SpringMVC内置了大量的Handler,我们重点关注下其中对参数进行处理的,主要是HandlerMethodArgumentResolver和HandlerMethodReturnValueHandler,前者表示一个参数解析器,后者除了解析参数之外还可以处理相应类型的返回值。以下是HandlerMethodArgumentResolver的实现类

它们基本上都实现了:
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进行处理时会把两个参数值通过,进行连接。
部分解析器及其作用:

前面扯了那么多,现在终于是进入正题了,先来搭建下漏洞环境:
主要代码如下:
@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;
}最后运行
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返回为空了。
