JDArmy Blog

Spring-Core RCE分析

字数统计: 1.6k阅读时长: 6 min
2022/04/11

迟来的SpringMVC 框架RCE分析。本文章简单介绍了SpringMVC框架请求处理流程,并以此对漏洞进行了分析与复现。

框架浅析

SpringMVC其本质上是一个Servlet,它的请求处理主要是在DispatcherServlet中,这里大概有四步:

  1. 根据Request找到Handler
  2. 根据Handler找到HandlerAdapter
  3. HandlerAdapter调用Handler处理请求
  4. 处理结果并渲染输出给用户

借用一张图来看下这个流程

屏幕快照 2019-10-17 下午8.49.43

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

image-20220410133631042

它们基本上都实现了:

1
2
3
public boolean supportsParameter(MethodParameter parameter) //和 
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory)

RequestMapping对应参数符合supportsParameter会使用resolveArgument解析请求,并最终得到参数的值传入RequestMapping,这里以RequestParamMapMethodArgumentResolver简单介绍下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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,所以可以定义如下接口:

1
2
3
4
5
@ResponseBody
@RequestMapping("/mvc/world")
public String world(@RequestParam HashMap<String, String> map) {
return "successfuladd";
}

该接口就会被RequestParamMapMethodArgumentResolver处理,很容易看出这里简单的做了个类型转换,这里的result就是我们需要的参数了

image-20220410151644951

有趣的是这里如果两个相同参数的请求,其只会取第一个的值,而如果是RequestParamMethodArgumentResolver进行处理时会把两个参数值通过,进行连接。

部分解析器及其作用:

image-20220410154419251

漏洞分析

前面扯了那么多,现在终于是进入正题了,先来搭建下漏洞环境:

  • JDK:11.0.14
  • Tomcat:9.0.60
  • Spring 5.3.17

主要代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@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

image-20220410174059024

一路跟进bindRequestParameters函数直到org.springframework.validation#applyPropertyValues

image-20220410181059133

这里经过getPropertyAccessor()我们实际上获取到了一个User对象的BeanWrapper实例

image-20220410181408547

在这里我们补充下BeanWrapper相关的内容,在Spring中,BeanWrapper接口是对Bean的包装,定义了对包装对象的属性值的访问与修改的接口,BeanWrapperImpl则是对BeanWrapper的默认实现,BeanWrapperImpl类有多个设置bean属性值的重载方法,其中就有public void setPropertyValue(PropertyValue pv)PropertyValue 以对象的方式存储键值对,比Map使用起来要灵活,通过BeanWrapperImpl设置属性值:

1
2
3
4
5
6
7
8
9
10
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获取所有属性值:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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:

1
2
3
4
5
6
7
8
9
10
11
public class God {
private String name;

public String getName(){
return name;
}

public void setName(String name){
this.name = name;
}
}

同时在User中加入:

1
2
3
4
5
6
private God god = new God();


public God getGod(){
return god;
}

最后运行

1
2
3
4
5
6
7
8
9
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来进一步获取值

image-20220410201409567

缓存之后就开始获取属性值了,如果该属性可读的话就会在getValue时执行其get方法,这里的Value就是God实例

image-20220410201835796

最后会以该实例生成一个新的nestedPa返回并进入第二次循环

image-20220410202130985

不过第二次时已经没有.了,所以直接返回this,也就是god,并以此知道要设置的值为god.name,所以后续就进入了设置属性值的流程,只有当该属性值存在且可写的情况下才可以继续往下执行

image-20220410202831939

至此整个流程就结束了,让我们回到漏洞

setPropertyValues(mpvs, isIgnoreUnknownFields(), isIgnoreInvalidFields())其实也调用了setPropertyValue(PropertyValue pv)

image-20220410203054865

那么结合上文对setPropertyValue流程的分析,其实我们已经大致理解了payload的格式,包括为什么用class.module.classLoader而不是直接class.classLoader。在Tomcat中是ParallelWebappClassLoader,而且其有一个属性getResources,就这样层层递归,最终操作日志,达成任意文件写入,从而实现RCE,在SpringBootLaunchedURLClassLoader中并不存在getResources所以直接使用SpringBoot的情况下上述Payload是不起作用的。

修复方案

针对该漏洞Spring以及 Tomcat都做出了修复

Spring: Class类仅可以获取name相关的值了,而且对没有写操作权限的ClassLoader以及ProtectionDomain做了限制

image-20220410204849484

Tomcat则是直接把getResources返回为空了

image-20220410205319618

参考文章

CATALOG
  1. 1. 框架浅析
  2. 2. 漏洞分析
  3. 3. 修复方案
  4. 4. 参考文章