漏洞说明
近日,spring 核心框架被曝出远程命令执行漏洞漏洞,漏洞编号为 CVE-2022-22965,被视为 CVE-2010-1622 的补丁绕过,运行在 JDK 9+ 上的 Spring MVC 或 Spring WebFlux 应用程序容易受到该漏洞的攻击。经过详细分析,该漏洞利用了 JDK9 的 Module 新特性,结合 Spring pojo 参数绑定与链式解析机制,成功获取 classLoader 对象并篡改其内容,该漏洞需要结合具体的中间件进行利用,当前曝光的 exp 均针对 tomcat 中间件,由于Java Web 应用程序部署时可选的中间件类型众多,其他中间件也有可能构建出完整利用链。
目前来看利用该漏洞所需的条件如下:
- JDK 9+
- 使用 Tomcat 中间件,并开启日志记录
- Spring-webmvc 或者 spring-webflux 依赖
- 存在 pojo 实体参数解析
- Spring Framework 满足受影响的版本
受漏洞影响的 Spring Framework 版本如下:
- 5.3.0 to 5.3.17
- 5.2.0 to 5.2.19
- 旧的、不受支持的版本也会受到影响
不受影响的版本:
- 5.3.18+
- 5.2.20+
漏洞复现
新建一个 pojo 实体类 User
public class UserPojo {
private String username;
public UserPojo() {
}
public String getUsername() {
return this.username;
}
public void setUsername(String username) {
this.username = username;
}
}
创建一个 Controller
@Controller
public class TestController {
@RequestMapping({"/login"})
@ResponseBody
public String login(UserPojo userPojo) {
return "success";
}
}
我本地的环境仅使用 SpringMVC,生成 war 包后部署到 tomcat 9 中。
exp 已经很多了,并且大多一致,这里就不贴了,发送如下的请求包:
触发漏洞之后会在 tomcat webapp/123(通过上图的 derectory 指定) 路径中生成 localhost_access_log.2022.jsp
访问 123/localhost_access_log.2022.jsp?cmd=ls
漏洞原理分析
漏洞触发流程
springmvc 的参数解析绑定过程中会将诸多默认的参数解析器加入到 argumentResolvers 中,默认包含如下的 26 种参数解析器
argumentResolvers = {LinkedList@6202} size = 26
0 = {RequestParamMethodArgumentResolver@6205}
1 = {RequestParamMapMethodArgumentResolver@6206}
2 = {PathVariableMethodArgumentResolver@6207}
3 = {PathVariableMapMethodArgumentResolver@6208}
4 = {MatrixVariableMethodArgumentResolver@6209}
5 = {MatrixVariableMapMethodArgumentResolver@6210}
6 = {ServletModelAttributeMethodProcessor@6211}
7 = {RequestResponseBodyMethodProcessor@6212}
8 = {RequestPartMethodArgumentResolver@6213}
9 = {RequestHeaderMethodArgumentResolver@6214}
10 = {RequestHeaderMapMethodArgumentResolver@6215}
11 = {ServletCookieValueMethodArgumentResolver@6216}
12 = {ExpressionValueMethodArgumentResolver@6217}
13 = {SessionAttributeMethodArgumentResolver@6218}
14 = {RequestAttributeMethodArgumentResolver@6219}
15 = {ServletRequestMethodArgumentResolver@6220}
16 = {ServletResponseMethodArgumentResolver@6221}
17 = {HttpEntityMethodProcessor@6222}
18 = {RedirectAttributesMethodArgumentResolver@6223}
19 = {ModelMethodProcessor@6224}
20 = {MapMethodProcessor@6225}
21 = {ErrorsMethodArgumentResolver@6226}
22 = {SessionStatusMethodArgumentResolver@6227}
23 = {UriComponentsBuilderMethodArgumentResolver@6228}
24 = {RequestParamMethodArgumentResolver@6229}
25 = {ServletModelAttributeMethodProcessor@6230}
在后续的处理中,springmvc 会针对不同类型的参数调用不同的参数处理器进行处理。pojo 类型的参数使用 ServletModelAttributeMethodProcessor 处理器。
ServletModelAttributeMethodProcessor.resolveArgument 进入参数的处理
this.bindRequestParameters 进入参数绑定过程
绑定过程中对参数进行赋值。applyPropertyValues 中调用 AbstractPropertyAccessor.setPropertyValues 对属性进行赋值。
setPropertyValues 会循环遍历用户传入的参数并赋值。
当前的 pv 就是我们传入的 class.module.classLoader.resources.context.parent.pipeline.first.suffix
进入 AbstractNestablePropertyAccessor(抽象可嵌套属性访问器)的 setPropertyValue 方法,该方法调用 getPropertyAccessorForPropertyPath 提供嵌套属性的递归解析,获取到对应类的 BeanWrapper,然后调用重载的 setPropertyValue 对属性进行赋值,其本质也是通过反射。
这里就是该漏洞的利用点,通过 getPropertyAccessorForPropertyPath 获取到 classLoader,进而篡改其属性。
继续深入 getPropertyAccessorForPropertyPath。
首先根据 . 进行切分,得到路径中的第一个元素,也就是 class,调用 getNestedPropertyAccessor 获取 class 的属性访问器,然后对剩余的部分 module.classLoader.resources.context.parent.pipeline.first.suffix 进行递归处理。
跟进 getNestedPropertyAccessor 中的 getPropertyValue。
调用 getLocalPropertyHandler
getLocalPropertyHandler 中会根据传入的参数名,从 cachedIntrospectionResults 中获取对应的属性描述符 (PropertyDescriptor)
当前 cachedIntrospectionResults 的内容如下:
username 对应 就是 UserPojo 中 username 的属性描述符。
这里需要注意的是,为什么 UserPojo 的 PropertyDescriptor 会存在 class?
Java 内省机制中,只要这个类存在某个属性的 get 方法(甚至这个属性都不需要存在,只需要存在 getxxx 方法即可),那么这个类的 BeanInfo 就会存在对应属性的描描述符,因为每个类都存在 getClass 方法,因此每个类对应的 cachedIntrospectionResults 中都会存在一个 class。
属性描述符 PropertyDescriptor,可以通过如下方式获取:
Introspector.getBeanInfo(xxx.class).getPropertyDescriptors()
getBeanInfo 传入 Class 对象。
// 获取 UserPojo 的属性描述符
Introspector.getBeanInfo(Class.forName("com.dr34d.pojo.UserPojo").getClass()).getPropertyDescriptors();
// 获取 Class 类的属性描述符
Introspector.getBeanInfo(Class.class).getPropertyDescriptors();
我们传入的 class 可以获取到 Class 类的属性描述符。之后进入递归过程,在 Class 类描述符的基础上获取 module 类的描述符。
可以看到 Class 类对应的 cachedIntrospectionResults 中是存在 module 类的描述符的。
继续递归,可以看到 Module 类对应的 cachedIntrospectionResults 中出现了 classLoader 类的描述符!
因此 通过 class.module.classLoader 访问到 UserPojo 的 classLoader。
再通过 resources.context.parent.pipeline.first 就可以访问到 Tomcat AccessLogValve 对象。
这里我们就要想,明明 Class 类中就有 getClassLoader 方法,为什么不能直接通过 class.classLoader 获取 classLoader 呢?
这就需要追溯到 CVE-2010-1622,
CVE-2010-1622 补丁
CVE-2010-1622 的漏洞原理与上面一致,该漏洞的利用方式就是 通过 class.classLoader 获取 classLoader 。其补丁如下,在 CachedIntrospectionResults 的构造函数中
可以看到在生成 CachedIntrospectionResults 时,也是传入类的 Class 对象,获取所有属性的描述符,加入 propertyDescriptorCache。加入的条件为:beanClass 不为 java.lang.Class 或属性名不为 classLoader,且属性名不为 protectionDomain。
Class.class != beanClass || !"classLoader".equals(pd.getName()) && !"protectionDomain".equals(pd.getName())
也就是说,如果获取 Class.class 的属性描述符,碰到 classLoader 属性,就会被忽略。
那为什么 CVE-2022-22965 可以绕过呢? 这就利用到 JDK9+ 的新特性 Mudule
JDK9 新特性:Mudule
JDK9 之前是以不同的 package 和 jar 来进行功能的区分和隔离,JDK9 之后实现了模块化。
JDK 被分成了 java.base、java.compiler 等模块
在模块化的实现中,引入了一个新的类:Module(java.lang.Module)
注意到 Module 类中存在一个 getClassLoader 方法。因此 Module 类对应的 cachedIntrospectionResults 是存在 classLoader 的描述符的。
回到 CVE-2010-1622 的补丁处:
Class.class != beanClass || !"classLoader".equals(pd.getName()) && !"protectionDomain".equals(pd.getName())
beanClass 为 java.lang.Module 自然是不等于 java.lang.Class 的,第一个判断就实效了,”classLoader”!=”protectionDomain” 也是成立的,因此就绕过了这个过滤。
漏洞修复
Spring 官方的修补如下:
for(int var6 = 0; var6 < var5; ++var6) {
PropertyDescriptor pd = var4[var6];
if ((Class.class != beanClass || "name".equals(pd.getName()) || pd.getName().endsWith("Name")) && (pd.getPropertyType() == null || !ClassLoader.class.isAssignableFrom(pd.getPropertyType()) && !ProtectionDomain.class.isAssignableFrom(pd.getPropertyType()))) {
if (logger.isTraceEnabled()) {
logger.trace("Found bean property '" + pd.getName() + "'" + (pd.getPropertyType() != null ? " of type [" + pd.getPropertyType().getName() + "]" : "") + (pd.getPropertyEditorClass() != null ? "; editor [" + pd.getPropertyEditorClass().getName() + "]" : ""));
}
pd = this.buildGenericTypeAwarePropertyDescriptor(beanClass, pd);
this.propertyDescriptors.put(pd.getName(), pd);
Method readMethod = pd.getReadMethod();
if (readMethod != null) {
readMethodNames.add(readMethod.getName());
}
}
}
补丁限制只能为 name 的属性通过,这基本上再难绕过了。
如有问题请大佬们纠正。