1.复现
2.调用栈(图示)
3.POC原理、调用链及绕过方法(设断点)
4.被利用类
1.复现
复现完成后还挺震惊的,简简单单就完成了rce
复现方法一:docker:
https://github.com/vulhub/vulhub/tree/master/spring/CVE-2022-22965
提示容器名不能带”_“,又占用了8080端口,所以最后改成:
1 | version: '2' |
这个例子特贴心的展示了get传参的内容可以拼接body传参这一现象(可操作性更强了)
复现方法二:(调试方法)
参考https://blog.joe1sn.top/2022/04/01/spring4shell/
导入idea后选tomcat方式(自动生成war包)启动,或者生成war包,放到tomcat的webapps下,一定不要选springboot方式,打成jar包就没漏洞了!
但比尴尬的是,idea调试状态下的tomcat一直和spring不太兼容
2.调用并传参
调用栈:
参考:https://xz.aliyun.com/t/11136 和 https://developer.huawei.com/consumer/cn/forum/topic/0204853302354230029?fid=0101592429757310384
然后自己实际跟了一下,并绘制了一张图表(感觉更直观一些)
1 | <init>:272, CachedIntrospectionResults (org.springframework.beans) |
3.POC原理、调用链及绕过方法(设断点)
看了漏洞的利用方法,一般都是使用class.module.classLoader.resources.context.parent.pipeline.first.pattern
的语句,这样的语句在CVE-2014-0050就有用过,而且用法几乎一模一样(class.classLoader.resources.context.parent.pipeline.first.encoding
)
一直看不明白这是什么,受群里师傅点拨,设了几个断点,加上这篇文章(https://www.anquanke.com/post/id/272149 ),才有点明白
这是spring(或者说java?)的bean机制,在这个环境下的作用就是将外部传参中的字符串填到正确的类中去。
稍微改了一下引用文章的代码:
1 | import org.springframework.stereotype.Controller; |
当请求为/addUser?name=test&department.name1=SEC
时
会自动给User的name赋值test
,然后给Department的name1赋值SEC
(页面上会显示test
,控制台会输出SEC
)
这个方法的实现是通过User.setName()
和User.getDepartment().setName1()
所以假设请求参数名为foo.bar.baz.qux,对应Controller方法入参为Param,则有以下的调用链:
1 | Param.getFoo() |
或Param.getFoo().getBar().getBaz().setQux()
通过这种方法可以实现对spring中任意文件的读写(前提是必须有调用链,即get方法)
然后是这次用到的调用链:class.module.classLoader.resources.context.parent.pipeline.first.prefix
1 | User.getClass() |
可以通过设置断点验证:
断点打在org.springframework.beans.AbstractNestablePropertyAccessor
的getPropertyAccessorForPropertyPath
方法,可以看到它每一轮都在读取其中的一层
然后再把断点打到org.springframework.beans.BeanWrapperImpl
的getValue
,会发现它每次都会打印get方法或set方法的路径,最后就整理成了上面的链子
第一个问题是:这个代码到底有什么漏洞?
这次jdk9受影响是因为新出现的module参数而产生了此次漏洞。2010年爆出的利用链是class.classLoader.resources.context.parent.pipeline.first.prefix
相当于java.lang下一定有classLoader类,在下面一定有resources类,以此类推,直到prefix类,才可实现.getclass().getclassLoader().getresources().getcontext().getparent().getpipeline().getfirst().setprefix()
而jdk9新增了module类,class下有这个类,这个类下有classloader类,所以利用链才是class.module.classLoader.resources.context.parent.pipeline.first.prefix
很明显:if (Class.class == beanClass &&("classLoader".equals(pd.getName()) || "protectionDomain".equals(pd.getName()
是原来存在漏洞的语句
很多人对此句的解释是:只验证了字符串class.module.classLoader.resources.context.parent.pipeline.first.prefix
的第二个字符是否为classloader
,让我一度以为getName()
方法会自动取我传入类的第二层。
实际上确实是取第二层,但并不是因为getName()
,而是因为Class.class == beanClass
这个函数整体的工作原理是:
1 | 比如我传入的内容是department.name1 |
断点设置如下图:(其中断点打到org.springframework.beans.CachedIntrospectionResults
的CachedIntrospectionResults
方法(init方法)if (Class.class == beanClass &&
可以查看本轮的pd,和相应的beanclass。断点打到this.propertyDescriptorCache.put(pd.getName(), pd);
可以查看本轮的白名单(this.propertyDescriptorCache),第一个断点用于区分大轮)
第二个问题是:我传入的内容到底有什么用?
直接看链子的最后org.apache.catalina.valves.AccessLogValve
,所有参数都被set进了这里(这里要换另一个项目)https://github.com/BobTheShoplifter/Spring4Shell-POC
发现backgroundProcess方法
这个方法是tomcat的日志功能,tomcat会定期进行调用,如果里面有东西就会写出文件,然后文件的内容,文件名,文件后缀,文件路径等都是可以通过set写入的,这就完成了后门的写入。
总结
总结,调用栈和利用方法都是十年前的,只是因为jdk9才更新了利用链,利用方法比大部分java漏洞都简单,直接写入木马,不用反序化,不用jndi……虽然网上一直流传着一个段子
但感觉漏洞的影响并没上次那么广泛。
首先必须要自己编写spring封成war包,然后导入tomcat的方法,而现在大部分人都在用直接打成jar包的方法(没有tomcat,调用链会乱掉,根本无从利用),特别是springmvc就本是配置地狱,和高版本的jdk/tomcat一起使用的时候动不动就404/500(因为javax和javata的关系),找了很久才完成复现,相比于傻瓜式的springboot,麻烦太多。
而且jdk并没有那么容易更改,因为公司很多自主开发和引入的模块可能都是依附于原jdk的,而且jdk8算是个分水岭,升级的话连javax都要换成javata,更不用提有些软件根本不支持更高版本,相比于要修所有其余代码因升级jdk所造成的bug,升级log4j2显然更加简单。如果不是去年有人给出解决方法是“升级jdk9+”,估计这个漏洞的影响力会更小。
看到一个挺有趣的紧急修复方式:class.module.classLoader.resources.context.parent.pipeline.first.enabled=false
——直接利用漏洞把这个日志功能关了,还不用改别的代码!
另外就是发现了一些神奇的绕过方法:https://mp.weixin.qq.com/s/plFLE8e0-Fc2tHJ4HaiSSw ,但是修复的代码也狠狠,原来是只有一个黑名单过滤classloader,现在是只留了白名单,而且只有一个name类允许(https://github.com/spring-projects/spring-framework/compare/v5.3.17...v5.3.18?diff=split#diff-de34d8ca8ee09e89bce68a491958bcc09197c49243aa2db6d981ecaa035b2447L289 )
