Это мини-статья для ознакомления с атакой на Websocket. Для начала разберёмся, что такое Websocket шучу, это вы можете прочесть в прошлой статье "WebSocket. Краткий экскурс в пентест ping-pong протокола", но тут я только затрону основное. Нужно разобраться с другим - "Что такое Race condition(состояние гонки)?". Начнем именно с этого, но пока не больше основ, т.к. сейчас готовиться крупная статья по данной теме, думаю управиться за пару недель.
Приятного чтения
Помните, что использование полученных знаний и навыков должно быть ограничено законными и этическими рамками, и вмешательство в чужие сети без разрешения является неприемлемым и незаконным действием.
Оглавление
Что такое Race condition?
Race condition - это ситуация, при которой несколько потоков (или процессов) одновременно пытаются выполнить операции чтения или записи к общим ресурсам без должной синхронизации. Представить можно в формате очереди, один за одним.
Сложно? Я Вас понимаю, при разборе тем, всегда так, но представьте площадку наподобие Я.маркет, где присутствуют купоны на скидку в 10% «best10». Два потока могут одновременно запросить базу данных и подтвердить, что код скидки «best10» не был применен к корзине, затем оба попытаются применить скидку, в результате чего она будет применена дважды. Обратите внимание на то, что «гоночные» условия не ограничиваются конкретной архитектурой веб‑приложений. Проще всего рассуждать о многопоточном приложении с одной базой данных, но в более сложных системах состояние обычно хранится в еще большем количестве мест. Однопоточные системы, такие как NodeJS, чуть менее уязвимы, но все равно, есть вероятность возникновения подобных проблем.
Что такое Websocket?
WebSocket (веб-сокет) - это протокол для двусторонней связи между клиентом и сервером через веб-соединение. Он предоставляет возможность передавать данные в режиме реального времени без необходимости постоянного запроса к серверу. WebSocket обеспечивает более эффективное соединение и не такие накладные расходы на его организацию, чем традиционные методы - например, HTTP-запросы и ответы.
Протокол имеет две схемы URI:
ws: / host [: port] path [? query]
для обычных соединений.wss: / host [: port] path [? query]
для туннельных соединений TLS.
Вот основные характеристики и особенности WebSocket:
Установка соединения: WebSocket начинается с установки соединения через HTTP (обычно используется стандартный порт 80 или защищенный порт 443). После успешной установки соединения клиент и сервер могут обмениваться данными в реальном времени;
Двусторонняя связь: WebSocket поддерживает как отправку данных от клиента к серверу, так и от сервера к клиенту. Это позволяет строить интерактивные веб-приложения, где клиент и сервер могут обмениваться информацией без задержек;
Низкая задержка: WebSocket обеспечивает низкую задержку (лаг) по сравнению с традиционными методами долгого опроса (long polling) или периодическими запросами;
Протокол на основе кадров (frame-based protocol): Данные в WebSocket упаковываются в кадры (frames), что делает их эффективными для передачи и обработки;
Поддержка защиты (Security): WebSocket может использовать шифрование для обеспечения безопасности передаваемых данных, используя
wss://
вместоws://
в URL;Поддержка разных типов данных: WebSocket позволяет передавать различные типы данных, включая текст, бинарные данные и даже произвольные объекты;
Событийная модель: WebSocket использует событийную модель для обработки входящих данных. Это означает, что Вы можете реагировать на события, такие как открытие соединения, получение сообщения или закрытие соединения.
Пример использования WebSocket:
-
Установка соединения:
Клиент отправляет HTTP-запрос на сервер с заголовком "Upgrade: websocket".
Если сервер поддерживает WebSocket, он возвращает HTTP-ответ с заголовком
Upgrade: websocket
, и соединение переключается на WebSocket.
-
Обмен данными:
Клиент и сервер могут отправлять друг другу текстовые или бинарные кадры через установленное соединение.
-
Закрытие соединения:
Клиент или сервер могут закрыть соединение по желанию, отправив специальный кадр.
Для лучшего понимания, представьте настольный теннис. Сервер периодически присылает ответ по WS с просьбой о действии - послать запрос на сервер. Если клиент отвечает до истечения тайм-аута — он подключен, если нет, то происходит разрыв соединения до следующего рукопожатия. Как и говорилось в предисловии, вот ссылка на полною статью для глубокого изучения.
Демонстрация или доказательство концепции
Мне понравилось короткое исследование, которое я возьму за основу.
Для демонстрации этой концепции в данной статье приводится Java-код, представляющий собой WebSocket-сервер, взаимодействующий с базой данных PostgreSQL. Сервер использует библиотеку Java-WebSocket для обработки WebSocket-соединений и выполняет следующие задачи:
После запуска программы Java-код подключается к базе данных и проверяет, существует ли таблица "example". Если она не существует, то создается таблица и в нее вставляются произвольные данные:
RandomName0
RandomName1
...
RandomName9
Наиболее интересный код находится в функции "onMessage".
public static int a = 0;
@Override
public void onMessage(WebSocket conn, String message) {
if (a == 0) {
try {
int rowCount = getCountFromExampleTable();
} catch (SQLException e) {
System.out.println("Error executing query: " + e.getMessage());
}
conn.send("Echo: " + message);
a = a + 1;
System.out.println(a);
}
}
Имеется глобальная переменная "a
", инициализированная в 0. Когда клиент подключается к серверу и отправляет сообщение, он проверяет, что "id == 0
", указывая, была ли уже выполнена эта функция. Если нет, то выполняется простая SQL-команда для выбора количества строк из таблицы "example". Затем "a
" увеличивается на 1, и его значение выводится на печать. И теоретически функция не должна выполняться 2 раза.
Что касается клиента, то было создано два типа: "WebSocketParallel_Success"
package io.redrays.ws.concept.client;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class WebSocketParallel_Success {
public static void main(String[] args) {
// Определение URI сервера WebSocket
String serverUri = "ws://127.0.0.1:8080";
// Количество создаваемых WebSocket-клиентов
int numClients = 100;
// Создание ExecutorService для управления несколькими клиентскими потоками WebSocket
ExecutorService executor = Executors.newFixedThreadPool(numClients);
// Создание списка для хранения экземпляров клиентов WebSocket
List<WebSocketClient> clients = new ArrayList<>();
// Цикл для создания и настройки нескольких клиентов WebSocket
for (int i = 0; i < numClients; i++) {
int clientId = i + 1;
try {
// Создание WebSocket-клиент для каждого соединения
WebSocketClient webSocketClient = new WebSocketClient(new URI(serverUri)) {
@Override
public void onOpen(ServerHandshake handshakedata) {
// Обработка события открытия WebSocket-соединения
System.out.println("Client " + clientId + " connected to the WebSocket server");
this.send("Hello, WebSocket server! From client " + clientId);
}
@Override
public void onMessage(String message) {
// Обработка входящих сообщений WebSocket
System.out.println("Client " + clientId + " received message: " + message);
}
@Override
public void onClose(int code, String reason, boolean remote) {
// Обработка события закрытия WebSocket-соединения
System.out.println("Client " + clientId + " connection closed: " + reason);
}
@Override
public void onError(Exception ex) {
// Обработка ошибок WebSocket
System.out.println("Client " + clientId + " error occurred: " + ex.getMessage());
}
};
// Добавление WebSocket-клиента в список
clients.add(webSocketClient);
// Подключение клиента WebSocket в отдельном потоке
executor.submit(webSocketClient::connect);
} catch (URISyntaxException e) {
System.out.println("Invalid WebSocket server URI: " + e.getMessage());
}
}
// Выключение исполнителя после выполнения всех заданий
executor.shutdown();
// Ожидание завершения работы всех клиентских потоков WebSocket
try {
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
System.out.println("Interrupted while waiting for tasks to complete: " + e.getMessage());
}
}
}
и "WebSocketParallel_Failed".
package io.redrays.ws.concept.client;
import org.java_websocket.client.WebSocketClient;
import org.java_websocket.handshake.ServerHandshake;
import java.net.URI;
import java.net.URISyntaxException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class WebSocketParallel_Failed {
public static void main(String[] args) {
String serverUri = "ws://127.0.0.1:8080"; // URI сервера WebSocket
int numParallelRequests = 285; // Количество параллельных WebSocket-запросов
try {
WebSocketClient webSocketClient = new WebSocketClient(new URI(serverUri)) {
// Этот метод вызывается при успешном открытии WebSocket-соединения
@Override
public void onOpen(ServerHandshake handshakedata) {
System.out.println("Connected to the WebSocket server");
// Создание фиксированного пула потоков для управления параллельными запросами
ExecutorService executor = Executors.newFixedThreadPool(numParallelRequests);
for (int i = 0; i < numParallelRequests; i++) {
int messageId = i + 1;
executor.submit(() -> {
this.send("Hello, WebSocket server! Message ID: " + messageId);
System.out.println("Sent message with ID: " + messageId);
});
}
// Выключение исполнителя после выполнения всех заданий
executor.shutdown();
// Ожидание завершения выполнения заданий
try {
executor.awaitTermination(Long.MAX_VALUE, TimeUnit.NANOSECONDS);
} catch (InterruptedException e) {
System.out.println("Interrupted while waiting for tasks to complete: " + e.getMessage());
}
}
// Этот метод вызывается при получении сообщения WebSocket.
@Override
public void onMessage(String message) {
System.out.println("Received message: " + message);
}
// Этот метод вызывается при закрытии WebSocket-соединения.
@Override
public void onClose(int code, String reason, boolean remote) {
System.out.println("Connection closed: " + reason);
}
// Этот метод вызывается при возникновении ошибки в соединении WebSocket
@Override
public void onError(Exception ex) {
System.out.println("Error occurred: " + ex.getMessage());
}
};
// Подключение к серверу WebSocket
webSocketClient.connect();
} catch (URISyntaxException e) {
System.out.println("Invalid WebSocket server URI: " + e.getMessage());
}
}
}
Были предприняты попытки создать состояние гонки двумя различными методами. В первом файле параллельно создается несколько соединений, и данные отправляются на сервер, а во втором - устанавливается только одно соединение, но данные отправляются последовательно друг за другом.
Как видно из названий классов, при параллельном создании нескольких соединений и отправке данных возникнет состояние гонки. Однако во втором случае оно не возникнет, поскольку WebSockets передают данные последовательно в одном соединении.
Как видно из приведенного ниже скриншота и видео, условия гонки могут возникать.
Заключение
Сегодня Я и автор исследования попытались дать новые знания Вам - читателям блога. В перерывах между крупными исследованиями и написанием статей, постараюсь Вас радовать подобными мини-статьями.
Спасибо за внимание ^-^
P.S. Больше подобной информации и хороших мемов Вы сможете найти тут
Комментарии (5)
yu_vl
27.09.2023 03:09Атака, исследование, предупреждение о вмешательство в чужие сети...Причем здесь вебсокеты, если проблема совершенно не протоколе?
username-ka
27.09.2023 03:09С тем же успехом можно было всё то же самое сделать в одном сервисе, и запустить "onMessage" из нескольких конкурирующих потоков. Websocket тут не при чём и только сбивает с толку.
Дожились, на Хабр пишет статьи ChatGPT.
CatWithBlueHat Автор
27.09.2023 03:09Статья написана без использования модного инструмента. Часть статьи - объяснение, другая - демонстрация концепции. Да, может это не тот уровень, что привыкли читать хабр-чане, но это мини-статья. Сейчас идет работа над обширной статьей про race condition, мне нужно было поделиться тем, что нашел. Спасибо за внимание.
KadesVII
Сдаётся мне, на Node JS такого бы не было. Потому что дело тут не в протоколе WebSocket, а в его реализации. Или, даже, в его правильном использовании. Если вы будете обрабатывать его в многопотоке, то, конечно, будут race conditions. Так используйте мьютексы или что-нибудь ещё. Думаю, это проблема не WebSocket, а многопоточной обработки.
CatWithBlueHat Автор
Согласен, но тут, скорее "показать концепцию" или того, как это реализовать. Пока нет большого исследования, чтобы продемонстрировать, что "race condition" может присутствовать много где. В первую очередь, помочь новым баунти хантерам с нахождением багов. Спасибо за понимание.