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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Это мини-статья для ознакомления с атакой на Websocket. Для начала разберёмся, что такое Websocket шучу, это вы можете прочесть в прошлой статье "WebSocket. Краткий экскурс в пентест ping-pong протокола", но тут я только затрону основное. Нужно разобраться с другим - "Что такое Race condition(состояние гонки)?". Начнем именно с этого, но пока не больше основ, т.к. сейчас готовиться крупная статья по данной теме, думаю управиться за пару недель.

Приятного чтения

Помните, что использование полученных знаний и навыков должно быть ограничено законными и этическими рамками, и вмешательство в чужие сети без разрешения является неприемлемым и незаконным действием.

Оглавление

  • Что такое Race condition?

  • Что такое Websocket?

  • Демонстрация или доказательство концепции

  • Заключение


Что такое 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/


Интересные статьи

Интересные статьи

Сегодня мы с вами (и с маленькой улиточкой) научимся поражать сотрудниц бухгалтерии во славу Алены Апиной, посчитаем свою чистую стоимость, определим является ли ваш кот Тимофей активом или пассивом, ...
Близится завершение большого этапа реализации первого продукта, в котором, с некоторого момента времени, я являюсь техническим лидом, или, как мне больше нравится - техническим владельцем продукта. И ...
Я знаю отличную шутку про UDP, но не факт, что она до вас дойдёт. Все, кто хоть раз в жизни, по работе открывал файл /etc/services знают, что одни сетевые службы используют транспортный протокол TC...
Тепловая карта с клавиатуры высокоинтеллектуальных программистов, источник: r/ProgrammerHumor/ Много говорилось об удовольствии, которое испытываешь при работе в консоли. Это не случ...
Однажды мне потребовалось забирать регулярно относительно большие объемы данных в MS SQL из PostgreSQL. Неожиданно выяснилось, что самый очевидный способ, через Linked Se...