Spring-Core RCE分析

2022-06-06 09:16:19 浏览数 (1)

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

框架浅析

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

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

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

Handler是用来处理请求,SpringMVC内置了大量的Handler,我们重点关注下其中对参数进行处理的,主要是HandlerMethodArgumentResolverHandlerMethodReturnValueHandler,前者表示一个参数解析器,后者除了解析参数之外还可以处理相应类型的返回值。以下是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简单介绍下:

代码语言:javascript复制
    @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,所以可以定义如下接口:

代码语言:javascript复制
    @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设置属性值:

代码语言:javascript复制
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获取所有属性值:

代码语言:javascript复制
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:

代码语言:javascript复制
public class God {
    private String name;

    public String getName(){
        return name;
    }

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

同时在User中加入:

代码语言:javascript复制
    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,在SpringBootLaunchedURLClassLoader中并不存在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

0 人点赞