Содержание статьи

Дата релиза: 6 марта 2017 года
Автор: Найк Чжэн (Nike Zheng)
CVE: CVE-2017-5638
 

BRIEF

Причина уязвимости — в некорректной логике обработки сообщений об ошибках. Если текст ошибки содержит языковые конструкции OGNL, то они будут выполнены. Таким образом, атакующий может отправить специально сформированный запрос, который повлечет за собой исполнение произвольного кода.

 

EXPLOIT

Если ты следишь за новостями, то уже наверняка слышал про эту нашумевшую проблему. Предлагаю еще раз взглянуть на причины ее возникновения и методы эксплуатации.

В качестве стенда я установил Tomcat 7.0.76 и последнюю уязвимую на данный момент версию Struts 2 из ветки 2.5.x под номером 2.5.10. Как приложение для теста я взял Showcase из коробки.

Первый взгляд

Эксплоитов для этой уязвимости написано уже великое множество, я буду опираться на этот.

Для начала протестируем его работоспособность. Выполняем:

python27 41570.py "http://127.0.0.1:8080/struts2-showcase/showcase.action" "ls"

На сервер отправляется запрос POST, содержащий некорректный заголовок Content-Type, в котором и находится вредоносный код.

Успешная эксплуатация. Выполнена команда ls
Успешная эксплуатация. Выполнена команда ls

Эксплоит отработал успешно, команда выполнилась, а в консоль Tomcat упало исключение. В нем можно наблюдать переданный эксплоитом хидер.

Исключение, вызванное работой эксплоита
Исключение, вызванное работой эксплоита

Детали уязвимости

Заглядываем в исходнички. Наш путь начинается с файла Dispatcher.java.

/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java:

794:     public HttpServletRequest wrapRequest(HttpServletRequest request) throws IOException {
...
800:         String content_type = request.getContentType();
801:         if (content_type != null && content_type.contains("multipart/form-data")) {
802:             MultiPartRequest mpr = getMultiPartRequest();
803:             LocaleProvider provider = getContainer().getInstance(LocaleProvider.class);
804:             request = new MultiPartRequestWrapper(mpr, request, getSaveDir(), provider, disableRequestAttributeValueStackLookup);
...
818:     protected MultiPartRequest getMultiPartRequest() {
819:         MultiPartRequest mpr = null;
...
827:         if (mpr == null ) {
828:             mpr = getContainer().getInstance(MultiPartRequest.class);
829:         }
830:         return mpr;

Обертка wrapRequest выполняет проверку на наличие строки multipart/form-data в хидере Content-Type. Далее обработка запроса продолжается в функцию MultiPartRequestWrapper, которая передает управление парсеру (MultiPartRequest.class), указанному в настройке struts.multipart.parser.

/core/src/main/java/org/apache/struts2/dispatcher/Dispatcher.java:

077: public class Dispatcher {
...
262:     @Inject(StrutsConstants.STRUTS_MULTIPART_PARSER)
263:     public void setMultipartHandler(String val) {
264:         multipartHandlerName = val;
265:     }

/core/src/main/java/org/apache/struts2/config/DefaultBeanSelectionProvider.java:

361: public class DefaultBeanSelectionProvider extends AbstractBeanSelectionProvider {
...
395:         alias(MultiPartRequest.class, StrutsConstants.STRUTS_MULTIPART_PARSER, builder, props, Scope.PROTOTYPE);

/core/src/main/java/org/apache/struts2/StrutsConstants.java:

136:     /**
137:      * The org.apache.struts2.dispatcher.multipart.MultiPartRequest parser implementation
138:      * for a multipart request (file upload)
139:      */
140:     public static final String STRUTS_MULTIPART_PARSER = "struts.multipart.parser";

/plugins/config-browser/src/main/java/org/apache/struts2/config_browser/ShowBeansAction.java:

47: public class ShowBeansAction extends ActionNamesAction {
...
51:     @Inject
52:     public void setContainer(Container container) {
...
61:         bindings.put(MultiPartRequest.class.getName(), addBindings(container, MultiPartRequest.class, StrutsConstants.STRUTS_MULTIPART_PARSER));

По дефолту используется парсер Jakarta.

/core/src/main/resources/org/apache/struts2/default.properties:

65: ### Parser to handle HTTP POST requests, encoded using the MIME-type multipart/form-data
66: # struts.multipart.parser=cos
67: # struts.multipart.parser=pell
68: # struts.multipart.parser=jakarta-stream
69: struts.multipart.parser=jakarta

Поэтому для парсинга юзерских POST-запросов используется класс JakartaMultiPartRequest. Посмотрим на часть кода, которая обрабатывает исключения.

/core/src/main/java/org/apache/struts2/dispatcher/multipart/JakartaMultiPartRequest.java:

34: import org.apache.struts2.dispatcher.LocalizedMessage;
...
64:     public void parse(HttpServletRequest request, String saveDir) throws IOException {
...
68:         } catch (FileUploadException e) {
69:             LOG.warn("Request exceeded size limit!", e);
70:             LocalizedMessage errorMessage;
...
75:                 errorMessage = buildErrorMessage(e, new Object[]{});
...
79:                 errors.add(errorMessage);

Метод buildErrorMessage класса LocalizedMessage нужен для того, чтобы сохранять сообщения об ошибках на языке текущей локали.

/core/src/main/java/org/apache/struts2/dispatcher/multipart/AbstractMultiPartRequest.java:

019: public abstract class AbstractMultiPartRequest implements MultiPartRequest {
...
091:     /**
092:      * Build error message.
...
098:     protected LocalizedMessage buildErrorMessage(Throwable e, Object[] args) {
...
102:         return new LocalizedMessage(this.getClass(), errorKey, e.getMessage(), args);
103:     }

Сам процесс загрузки файлов контролируется классом FileUploadInterceptor. И если обработчик запроса возвращает какие-то ошибки, то он пытается их отобразить.

/core/src/main/java/org/apache/struts2/interceptor/FileUploadInterceptor.java:

183: public class FileUploadInterceptor extends AbstractInterceptor {
...
237:     public String intercept(ActionInvocation invocation) throws Exception {
...
259:         MultiPartRequestWrapper multiWrapper = (MultiPartRequestWrapper) request;
260:
261:         if (multiWrapper.hasErrors()) {
262:             for (LocalizedMessage error : multiWrapper.getErrors()) {
263:                 if (validation != null) {
264:                     validation.addActionError(LocalizedTextUtil.findText(error.getClazz(), error.getTextKey(), ActionContext.getContext().getLocale(), error.getDefaultMessage(), error.getArgs()));
265:                 }
266:             }
267:         }

Метод LocalizedTextUtil.findText ищет сохраненный текст ошибки по ее типу.

Допустим, текст найден. Теперь дело за его отображением. Вот тут и происходит магия. Если в тексте ошибки имеются конструкции типа ${...} или %{...}, то они попадают в парсер выражений OGNL и выполняются. А как ты помнишь, сообщение об ошибке, которую вызывает эксплоит, содержит в себе текст из хидера Content-Type. Благодаря гибкости языка OGNL мы можем выполнять произвольный код, изменяя заголовок.

Обычно эту проблему преподносят как «ошибку в парсере Jakarta».
Однако это не так. Проблема гораздо шире, и ее можно эксплуатировать через любые другие парсеры. Нужно только найти способы вызвать сообщение об ошибке, текст в которой можно контролировать.

Про новый вектор эксплуатации этой уязвимости ты можешь прочитать здесь.

 

TARGETS

  • Apache Struts 2.3.5–2.3.31;
  • Apache Struts 2.5–2.5.10.
 

SOLUTION

Обновляйся до последних версий дистрибутива. Начиная со Struts 2.3.32 и Struts 2.5.10.1 уязвимость успешно запатчена. Патч можно посмотреть тут.

1 комментарий

  1. Il

    05.06.2017 at 08:35

    Спасибо, познавательно

Оставить мнение

Check Also

SHA 2017. Репортаж с самого яркого хакерского ивента этого лета

Still Hacking Anyway 2017 — это фестиваль, на котором собрались четыре тысячи хакеров со в…