CVE-2022-22965 Spring 核心框架漏洞原理分析

 

漏洞说明

近日,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 已经很多了,并且大多一致,这里就不贴了,发送如下的请求包:

img

触发漏洞之后会在 tomcat webapp/123(通过上图的 derectory 指定) 路径中生成 localhost_access_log.2022.jsp

img

访问 123/localhost_access_log.2022.jsp?cmd=ls

img

漏洞原理分析

漏洞触发流程

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 进入参数的处理

img

this.bindRequestParameters 进入参数绑定过程

img

绑定过程中对参数进行赋值。applyPropertyValues 中调用 AbstractPropertyAccessor.setPropertyValues 对属性进行赋值。

img

setPropertyValues 会循环遍历用户传入的参数并赋值。

img

当前的 pv 就是我们传入的 class.module.classLoader.resources.context.parent.pipeline.first.suffix

img

进入 AbstractNestablePropertyAccessor(抽象可嵌套属性访问器)的 setPropertyValue 方法,该方法调用 getPropertyAccessorForPropertyPath 提供嵌套属性的递归解析,获取到对应类的 BeanWrapper,然后调用重载的 setPropertyValue 对属性进行赋值,其本质也是通过反射。

img

这里就是该漏洞的利用点,通过 getPropertyAccessorForPropertyPath 获取到 classLoader,进而篡改其属性。

继续深入 getPropertyAccessorForPropertyPath。

首先根据 . 进行切分,得到路径中的第一个元素,也就是 class,调用 getNestedPropertyAccessor 获取 class 的属性访问器,然后对剩余的部分 module.classLoader.resources.context.parent.pipeline.first.suffix 进行递归处理。

img

跟进 getNestedPropertyAccessor 中的 getPropertyValue。

img

调用 getLocalPropertyHandler

img

getLocalPropertyHandler 中会根据传入的参数名,从 cachedIntrospectionResults 中获取对应的属性描述符 (PropertyDescriptor)

img

当前 cachedIntrospectionResults 的内容如下:

img

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 类的描述符。

img

可以看到 Class 类对应的 cachedIntrospectionResults 中是存在 module 类的描述符的。

继续递归,可以看到 Module 类对应的 cachedIntrospectionResults 中出现了 classLoader 类的描述符!

img

因此 通过 class.module.classLoader 访问到 UserPojo 的 classLoader。

再通过 resources.context.parent.pipeline.first 就可以访问到 Tomcat AccessLogValve 对象。

img

这里我们就要想,明明 Class 类中就有 getClassLoader 方法,为什么不能直接通过 class.classLoader 获取 classLoader 呢?

这就需要追溯到 CVE-2010-1622,

CVE-2010-1622 补丁

CVE-2010-1622 的漏洞原理与上面一致,该漏洞的利用方式就是 通过 class.classLoader 获取 classLoader 。其补丁如下,在 CachedIntrospectionResults 的构造函数中

img

可以看到在生成 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 等模块

img

在模块化的实现中,引入了一个新的类:Module(java.lang.Module)

注意到 Module 类中存在一个 getClassLoader 方法。因此 Module 类对应的 cachedIntrospectionResults 是存在 classLoader 的描述符的。

img

回到 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 的属性通过,这基本上再难绕过了。

如有问题请大佬们纠正。

参考资料