Spring Framework 远程代码执行 CVE-2022-22965

漏洞描述

Spring MVC框架的参数绑定功能提供了将请求中的参数绑定控制器方法中接收的参数的Java Bean对象的成员变量,攻击者通过构造恶意请求获取AccessLogValve对象并注入恶意参数触发pipeline机制可写入任意路径下的文件。

漏洞环境

  • JDK9
  • spring-web-5.2.3.RELEASE.jar
  • spring-webmvc-5.2.3.RELEASE.jar
  • Tomcat 8.5.161

Java Bean

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
package org.example.springmvc;

public class User {
private String name;
private int age;

@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}

public String getName() {
return name;
}

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

public int getAge() {
return age;
}

public void setAge(int age) {
this.age = age;
}
}

Controller

1
2
3
4
5
6
7
8
9
10
@Controller
public class HelloController {

@RequestMapping("/index")
@ResponseBody
public String mvc(User user, Model model) {
System.out.println(user);
return "index";
}
}

漏洞分析

RequestMappingHandlerAdapter#invokeHandlerMethod跟进处理进入ServletInvocableHandlerMethod#invokeAndHandle方法

image-20220329205016006

InvocableHandlerMethod#invokeForRequest方法调用getMethodArgumentValues获取控制器方法的参数并调用resolveArgument

image-20220329211300489

image-20220329211231741

ModelAttributeMethodProcessor#resolveArgument调用bindRequestParameters绑定请求参数:

image-20220329212105846

ServletRequestDataBinder#bind调用父类WebDataBinder.doBind方法,mpvs对象中封装了请求的参数信息。

image-20220329212439691

  • checkFieldDefaults方法将移除参数名中!开头的字符移除
  • checkFieldMarkers方法将移除参数明中_开头的字符移除

调用父类DataBinder#doBind方法:

image-20220329212838103

checkAllowedFields方法会检查字段名是否匹配黑名单,默认情况下alloweddisallowed均为空,最后调用applyPropertyValues方法进入关键操作。

image-20220329213132149

循环封装参数信息的PropertyValue对象并调用setPropertyValue方法

image-20220329213649248

获取参数名称调用getPropertyAccessorForPropertyPath方法

image-20220329213759687

截取.之前的字符调用getNestedPropertyAccessor方法后重复调用getPropertyAccessorForPropertyPath方法,使之能够对成员变量为对象(非基本类型)的字段进行参数绑定。

image-20220329214104606

getNestedPropertyAccessor方法将参数名封装为PropertyTokenHolder对象并调用getPropertyValue方法:

image-20220329220738363

getLocalPropertyHandler方法根据名称从CachedIntrospectionResults.propertyDescriptorCache获取对应的PropertyDescriptor对象。

image-20220329220918626

image-20220329221218496

BeanWrapperImpl#getValue方法调用其getter方法:

image-20220329221632181

回到getNestedPropertyAccessor方法后会调用newNestedPropertyAccessor方法第一个参数value即为getter方法的返回值:

image-20220330153836961

BeanWrapperImpl#newNestedPropertyAccessor返回BeanWrapperImpl实例对象,构造方法中调用父类构造器。

image-20220330154116420

image-20220330154215896

setWrappedInstance方法中将object赋值wrappedObject成员变量:

image-20220330154546604

因此嵌套字段中第一个getter的返回值会作为第二个getter调用Method.invoke时的对象参数,当循环到嵌套的最后一个属性时将调用BeanWrapperImpl#setValue方法调用其setter方法:

image-20220329222008936

从请求处理到getter方法的调用栈:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
java.lang.Thread.State: RUNNABLE
at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyValue(AbstractNestablePropertyAccessor.java:625)
at org.springframework.beans.AbstractNestablePropertyAccessor.getNestedPropertyAccessor(AbstractNestablePropertyAccessor.java:843)
at org.springframework.beans.AbstractNestablePropertyAccessor.getPropertyAccessorForPropertyPath(AbstractNestablePropertyAccessor.java:820)
at org.springframework.beans.AbstractNestablePropertyAccessor.setPropertyValue(AbstractNestablePropertyAccessor.java:256)
at org.springframework.beans.AbstractPropertyAccessor.setPropertyValues(AbstractPropertyAccessor.java:104)
at org.springframework.validation.DataBinder.applyPropertyValues(DataBinder.java:851)
at org.springframework.validation.DataBinder.doBind(DataBinder.java:747)
at org.springframework.web.bind.WebDataBinder.doBind(WebDataBinder.java:198)
at org.springframework.web.bind.ServletRequestDataBinder.bind(ServletRequestDataBinder.java:116)
at org.springframework.web.servlet.mvc.method.annotation.ServletModelAttributeMethodProcessor.bindRequestParameters(ServletModelAttributeMethodProcessor.java:158)
at org.springframework.web.method.annotation.ModelAttributeMethodProcessor.resolveArgument(ModelAttributeMethodProcessor.java:168)
at org.springframework.web.method.support.HandlerMethodArgumentResolverComposite.resolveArgument(HandlerMethodArgumentResolverComposite.java:121)
at org.springframework.web.method.support.InvocableHandlerMethod.getMethodArgumentValues(InvocableHandlerMethod.java:170)
at org.springframework.web.method.support.InvocableHandlerMethod.invokeForRequest(InvocableHandlerMethod.java:137)
at org.springframework.web.servlet.mvc.method.annotation.ServletInvocableHandlerMethod.invokeAndHandle(ServletInvocableHandlerMethod.java:106)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.invokeHandlerMethod(RequestMappingHandlerAdapter.java:894)
at org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter.handleInternal(RequestMappingHandlerAdapter.java:808)
at org.springframework.web.servlet.mvc.method.AbstractHandlerMethodAdapter.handle(AbstractHandlerMethodAdapter.java:87)
at org.springframework.web.servlet.DispatcherServlet.doDispatch(DispatcherServlet.java:1063)
at org.springframework.web.servlet.DispatcherServlet.doService(DispatcherServlet.java:963)
at org.springframework.web.servlet.FrameworkServlet.processRequest(FrameworkServlet.java:1006)
at org.springframework.web.servlet.FrameworkServlet.doPost(FrameworkServlet.java:909)

为什么需要JDK9

CachedIntrospectionResults(Class<?> beanClass)构造器中getBeanInfo方法会返回获取的BeanInfo对象中包含了类属性、方法等信息。

image-20220331113213360

但在写入propertyDescriptorCache时会判断当前beanClass如果为Class.class且属性名为classLoader是不会写入的,因此当PoC为class.classLoader时是获取不到对应的PropertyDescriptor对象的。

image-20220331113543223

而在JDK9新增的模块特性(java.lang.Module类)则可以绕过上述的限制,因此通过 class.module.classLoader 即可获取到ClassLoader,因此漏洞环境依赖JDK9+。

image-20220331114542749

漏洞探测

之前预想了两种漏洞探测方式:

  • Tomcat的利用方式直接写入文件并探测文件是否存在,弊端也很明显,一是相对路径写入的情况下Web目录不一定是默认目录,二是这样一来会将所有的访问日志追加写入同一个文件中,并且会对业务原来的日志配置产生影响。
  • 通过class.module.classLoader.resources.context.sessionCookieName的方式修改Cookie的名称(默认JSESSIONID)的方式来探测漏洞是否存在,但在实际扫描中出现漏报的概率也很大,一是漏洞位置不一定会设置Cookie不好探测扫描结果,二是也可能会对业务产生影响。

然而今天已经有人公布了一种新的探测方式,令服务端抛出异常其在返回响应请求时则会返回异常状态码(这种方式还是会存在误报的情况),探测PoC为class.module.classLoader.defaultAssertionStatus=xxx。其实原理也很简单,因为对应的setter方法需要的是一个boolean参数,而客户端传递的是一个字符串,并且这个字符串的值不是“布尔”值。

(CustomBooleanEditor#setAsText,如果字符串为true/on/yes/1 or false/off/no/0还是能转换成布尔值的。)

image-20220401221159689

CodeQL

2022/04/03补充:花了个把小时简单学习了下CodeQL发现真香,下图是用于搜索Controller注解的类并且方法的参数类型非基本类型等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import java

from Class c, Annotation ann, AnnotationType anntp, Method m
where ann = c.getAnAnnotation()
and anntp = ann.getType()
and anntp.getName() = "Controller"
and m = c.getAMethod()
and not m.getAParamType().getName() = "boolean"
and not m.getAParamType().getName() = "String"
and not m.getAParamType().getName() = "HttpServletResponse"
and not m.getAParamType().getName() = "Long"
and not m.getAParamType().getName() = "ModelMap"
and not m.getAParamType().getName() = "Integer"
select m, m.getAParamType().getName()

image-20220403152005616

漏洞利用

PoC:

class.module.classLoader.resources.context.parent.pipeline.first.pattern

等价于调用

User.getClass().getModule().getClassLoader().getResources().getContext().getParent().getPipeline().getFirst().setPattern