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

Комментарии (5)


  1. KadesVII
    27.09.2023 03:09
    +1

    Сдаётся мне, на Node JS такого бы не было. Потому что дело тут не в протоколе WebSocket, а в его реализации. Или, даже, в его правильном использовании. Если вы будете обрабатывать его в многопотоке, то, конечно, будут race conditions. Так используйте мьютексы или что-нибудь ещё. Думаю, это проблема не WebSocket, а многопоточной обработки.


    1. CatWithBlueHat Автор
      27.09.2023 03:09

      Согласен, но тут, скорее "показать концепцию" или того, как это реализовать. Пока нет большого исследования, чтобы продемонстрировать, что "race condition" может присутствовать много где. В первую очередь, помочь новым баунти хантерам с нахождением багов. Спасибо за понимание.


  1. yu_vl
    27.09.2023 03:09

    Атака, исследование, предупреждение о вмешательство в чужие сети...Причем здесь вебсокеты, если проблема совершенно не протоколе?


  1. username-ka
    27.09.2023 03:09

    С тем же успехом можно было всё то же самое сделать в одном сервисе, и запустить "onMessage" из нескольких конкурирующих потоков. Websocket тут не при чём и только сбивает с толку.

    Дожились, на Хабр пишет статьи ChatGPT.


    1. CatWithBlueHat Автор
      27.09.2023 03:09

      Статья написана без использования модного инструмента. Часть статьи - объяснение, другая - демонстрация концепции. Да, может это не тот уровень, что привыкли читать хабр-чане, но это мини-статья. Сейчас идет работа над обширной статьей про race condition, мне нужно было поделиться тем, что нашел. Спасибо за внимание.