Spring Framework RCE分析
p0melo

漏洞介绍

CVE编号

CVE-2022-22965

影响范围

  • JDK >= 9
  • 使用Apache Tomcat 作为Servlet容器,并且使用传统的war包部署方法
  • Spring Framework 5.3.0 - 5.3.17,5.2.0 - 5.2.19,以及更早的版本,或其他包含spring-webmvc or spring-webflux依赖的应用

漏洞复现

拉取此漏洞的vulhub代码进行复现,我本地环境是jdk11+tomcat8.5.39

漏洞分析

基础知识

Java内省机制

Java内省(Introspector)机制就是JDK提供的一套API来查找、设置JavaBean的属性,只要有 getter/setter 方法中的其中一个,那么 Java 的内省机制就会认为存在一个属性,内省的核心类就是Introspector类。

这里我们新建一个名为PersonJavaBean,使用内省的方法来调用Person类所有属性以及属性的读写方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Person {
private String name;
private Integer age;

public String getName() {
return name;
}

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

public Integer getAge() {
return age;
}

public void setAge(Integer age) {
this.age = age;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.beans.BeanInfo;
import java.beans.Introspector;
import java.beans.PropertyDescriptor;

public class Test {
public static void main(String[] args) throws Exception {
BeanInfo info = Introspector.getBeanInfo(Person.class);
PropertyDescriptor[] properties =
info.getPropertyDescriptors();
for (PropertyDescriptor pd : properties) {
System.out.println(pd.getName());
System.out.println(" [*]" + pd.getReadMethod());
System.out.println(" [*]" + pd.getWriteMethod());
}
}
}

运行结果除了包含Person类的属性和属性的读写方法之外,另外还包括class属性以及getClass方法,这是因为呢?

为什么会有class属性?

查看Introspector.getBeanInfo(Class<?> beanClass)方法,会将beanClass传入Introspector构造方法,并调用Introspector实例getBeanInfo()方法

先跟入Introspector构造方法,stopClass为空就会获取父类java.lang.ObjectBeanInfo并赋给superBeanInfo

完成构造方法后调用getBeanInfo()getBeanInfo()方法里面的getTargetMethodInfo()getTargetEventInfo()getTargetPropertyInfo()几个方法都会先获取superBeanInfo中的值并加到自己的BeanInfo

因为java.lang.Object存在一个getClass()方法,所以内省机制会认为有class属性。这也就解释了为什么Person类有class属性和getClass方法了。

SpirngBean

SpringBean可以当成JavaBean的升级版,由Spring框架的ApplicationContext操控SpringBeanApplicationContext也称控制反转(IoC)容器,是Spring框架的核心。控制反转就是用户将对象转为实例过程,变成了容器生产实例,然后通过实例去注入到对应的对象的过程

简单的可以将Spring容器理解为工厂,SpringBean的生产过程就是我们定义好什么产品(Bean)需要什么样的原材料(Bean中的属性)这样的配置信息,Spring容器负责将原材料生产(实例化)为产品并存储(Cache)

在SpringBean要使用时,第一步就是从SpringBean的注册表中获取Bean的配置信息,然后根据配置信息实例化Bean,实例化以后的Bean被映射到了Spring容器中,并且被存储在了Bean Cache池中,当应用程序要使用Bean时,会向Bean Cache池发起调用。

参考panda大佬画的一张图

关键代码分析

根据历史漏洞分析文章,看下通到CachedIntrospectionResults的调用链,可以看到在getPropertyAccessorForPropertyPath递归了8次

getPropertyAccessorForPropertyPath方法根据分隔符.将传入的字符串分割,并从左往右递归处理嵌套属性(嵌套结构的理解可以参考文章),所以如果我们想通过class去调用classLoader的属性,只需要通过class.classLoader的方式即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
protected AbstractNestablePropertyAccessor getPropertyAccessorForPropertyPath(String propertyPath) {
// 获取嵌套属性的第一个属性
// 比如对于属性: foo.bar[0].name
// 首先获取到 foo 的索引位置
// getFirstNestedPropertySeparatorIndex是详细的方法
int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);
// Handle nested properties recursively.
//递归处理嵌套属性
if (pos > -1) {
// 获取所在的属性和对应的name
String nestedProperty = propertyPath.substring(0, pos);
String nestedPath = propertyPath.substring(pos + 1);
AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);
//递归调用
return nestedPa.getPropertyAccessorForPropertyPath(nestedPath);
}
else {
return this;
}
}

所以我们可以通过Tomcat Access Log来写shell。Tomcat Access Log是通过 server.xml 配置

1
2
<Valve className="org.apache.catalina.valves.AccessLogValve" directory="logs" prefix="access." suffix=".log" 
pattern="%h %l %u %t "%r" %s %b "%{Referer}i" "%{User-Agent}i" %{X-Forwarded-For}i "%Dms"" resolveHosts="false"/>

根据前面对SpirngBean和内省机制的理解,通过xml文件加载的配置属性,实际上也是可以通过内省机制修改的,Tomcat具体有哪些属性可以参考官方文档,通过修改下面的几个属性可创建任意后缀名的文件,即可写入一个shell

1
2
3
4
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.fileDateFormat =

为什么只有 >= jdk9受影响?

此漏洞其实算是CVE-2010-1622的JDK高版本利用,CVE-2010-1622的修复增加了class.classLoader的黑名单限制,而jdk9以下版本只能通过class.classLoader利用,pd.getNameclassLoader时,beanClassClass,即所以没法利用,黑名单判断代码如下

1
2
3
if (Class.class != beanClass || !"classLoader".equals(pd.getName()) && !"protectionDomain".equals(pd.getName())) {
...... // 正常逻辑
}

jdk9引入了模块系统,可通过class.module.classLoader使得当pd.getNameclassLoader时,Class.class != beanClass,从而不走后面||判断逻辑导致绕过

补丁分析

踩坑记录

由于本地调试环境问题,导致调试前踩了不少坑,这里记录下

1.CATALINA_BASE

tomcat默认配置的CATALINA_BASECATALINA_HOME是同一目录,这两者的区别可参考官网介绍

用idea配置tomcat后,启动时CATALINA_BASE并没有和CATALINA_HOME在同一目录,而是在C盘的用户目录下

写入的shellCATALINA_BASE下,而不是tomcat的安装目录CATALINA_HOME下,这就导致生成的shell访问不到

解决办法

idea中配置tomcat环境变量,指定CATALINA_BASE为本地tomcat目录,然后重启即可

2.idea配置tomcat端口不生效

为了解决上一个问题,idea配置了CATALINA_BASE后,idea中不管怎么设置tomcat服务的HTTP port,运行时始终都是以tomcat默认的8080端口启动(一直以为是我项目配置问题,这里卡了半天也没整出来,吐了…)

还不清楚具体是什么原因导致的,如果要修改端口只能修改tomcat的server.xml配置文件,或者直接访问默认的8080端口

参考

http://rui0.cn/archives/1158

https://xz.aliyun.com/t/11129#toc-13

https://tttang.com/archive/1532/

https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement

https://github.com/vulhub/vulhub/tree/master/base/spring/spring-webmvc/5.3.17

https://github.com/vulhub/vulhub/tree/master/spring/CVE-2022-22965

https://blog.csdn.net/Honnyee/article/details/85337647

https://juejin.cn/post/6966158157202587662

 Comments