Впервые об уязвимости Cross-Site WebSocket Hijacking (CSWSH) я узнал из статьи Кристиана Шнайдера и выступления Михаила Егорова, но не обратил на нее внимания. Позже, читая репорт на HackerOne, оцененный в 800 долларов, понял, что хочу разобраться. На просторах Рунета подробного описания CSWSH не нашлось, и я решил написать его самостоятельно.
В этой статье мы разберем протокол WebSocket, подробно остановимся на уязвимости CSWSH — насколько она распространена в открытом интернете. Для тех, кто дочитает до конца, я приготовил бонус в виде утилиты cswsh-scanner, с помощью которой ты можешь проверить свои приложения, работающие с WebSocket, либо попытать удачи на баг‑баунти.
Вся информация предоставлена исключительно в ознакомительных целях. Ни редакция, ни автор не несут ответственности за любой возможный вред, причиненный материалами данной статьи.
Итак, что такое WebSocket? Википедия дает следующее определение: «WebSocket — протокол связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб‑сервером в режиме реального времени». В отличие от синхронного протокола HTTP, построенного по модели «запрос — ответ», WebSocket полностью асинхронный и симметричный. Он применяется для организации чатов, онлайн‑табло и создает постоянное соединение между клиентом и сервером, которое обе стороны могут использовать для отправки данных.
Протокол WebSocket определен в RFC 6455. Для протокола зарезервированы две URI-схемы:
ws://host[:port]path[?query]
;wss://host[:port]path[?query]
.WebSocket достаточно распространен в современной веб‑разработке, есть поддержка во всех популярных языках программирования и браузерах. Его используют в онлайн‑чатах, досках объявлений, веб‑консолях, приложениях трейдеров. С помощью поисковика shodan.io можно с легкостью найти приложения на WebSocket, доступные из интернета. Достаточно сформировать простой запрос. Я не поленился и сделал:
Search for Sec-WebSocket-Version HTTP/1.1 400 Bad Request returned 55,461 results on 10-05-2020
В результате нашлось 55 тысяч адресов с обширной географией.
Разберем теперь, как работает WebSocket. Взаимодействие между клиентом и сервером начинается с рукопожатия. Для рукопожатия клиент и сервер используют протокол HTTP, но с некоторыми отличиями в формате передаваемых сообщений. Не соблюдаются все требования к HTTP-сообщениям. Например, отсутствует заголовок Content-Length
.
Для начала клиент инициирует соединение и отправляет запрос серверу:
GET /echo HTTP/1.1Host: localhost:8081Sec-WebSocket-Version: 13Origin: http://localhost:8081Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==Connection: keep-alive, UpgradeUpgrade: websocket
Заголовки Sec-WebSocket-Version
, Sec-WebSocket-Key
, Connection: Upgrade и Upgrade: websocket
обязательны, иначе сервер возвращает статус HTTP/1.1 400 Bad Request
. Сервер отвечает на запрос клиента следующим образом:
HTTP/1.1 101 Switching ProtocolsUpgrade: websocketConnection: UpgradeSec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Заголовок Sec-WebSocket-Key
формируется клиентом как случайное 16-байтовое значение, закодированное в Base64. Вариант формирования заголовка на Go:
func generateChallengeKey() (string, error) { p := make([]byte, 16) if _, err := io.ReadFull(rand.Reader, p); err != nil { return "", err } return base64.StdEncoding.EncodeToString(p), nil}
Заголовок Sec-WebSocket-Accept
в ответе формируется по следующему алгоритму. Берется строковое значение из заголовка Sec-WebSocket-Key
и объединяется с GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11
. Далее вычисляется хеш SHA-1 от полученной в первом пункте строки. Хеш кодируется в Base64. Вариант формирования заголовка на Go:
const GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"func computeAcceptKey(challengeKey string) string { h := sha1.New() h.Write([]byte(challengeKey + GUID)) return base64.StdEncoding.EncodeToString(h.Sum(nil))}
Заголовки Sec-WebSocket-Key
и Sec-WebSocket-Accept
не используются для авторизации и поддержки сессий, они служат для того, чтобы стороны убедились, что запрос и ответ относятся к протоколу WebSocket. Это помогает гарантировать, что сервер не принимает от клиентов запросы, не относящиеся к WebSocket.
Также RFC 6455 предполагает, что Sec-WebSocket-Key
должен быть выбран случайным образом для каждого соединения. Это означает, что любой кешированный результат от прокси‑сервера будет содержать невалидный Sec-WebSocket-Accept
и, следовательно, рукопожатие провалится вместо непреднамеренного чтения кешированных данных. Для успешного завершения рукопожатия клиент проверяет значение Sec-WebSocket-Accept
и ожидает статус‑код 101 Switching Protocols
. После того как рукопожатие выполнено, первоначальное соединение HTTP заменяется соединением по WebSocket, которое использует то же соединение TCP/IP. На этом этапе любая из сторон может начать отправку данных.
Для мониторинга трафика WebSocket удобно использовать «Инструменты разработчика», доступные, к примеру, в Chrome.
Как в WebSocket передаются сообщения? Данные по протоколу WebSocket передаются как последовательность фреймов. Фрейм имеет заголовок, в котором содержится следующая информация:
Формат фрейма представлен на рисунке.
Все сообщения, посылаемые клиентом, должны маскироваться. Пример отправки тестового сообщения «Hello world!» клиентом (данные из tcpdump):
Fin: TrueReserved: 0x0Opcode: Text (1)Mask: TruePayload length: 12Masking-Key: a2929b01Payload: eaf7f76dcdb2ec6ed0feff20
Маскировка производится обычным XOR с ключом маски. Клиент должен менять ключ для каждого переданного фрейма. Сервер не должен маскировать свои сообщения. Пример отправки тестового сообщения «Hello world!» сервером:
Fin: TrueReserved: 0x0Opcode: Text (1)Mask: FalsePayload length: 12Payload: 48656c6c6f20776f726c6421
Маскировка передаваемых сообщений некриптостойкая, чтобы обеспечить конфиденциальность, для WebSocket следует использовать протокол TLS и схему WSS.
С протоколом разобрались, самое время перейти к CSWSH. Протокол WebSocket использует Origin-based модель безопасности при работе с браузерами. Другие механизмы безопасности, например SOP (Same-origin policy), для WebSocket не применяются. RFC 6455 указывает, что при установке соединения сервер может проверять Origin, а может и нет:
Поле заголовка Origin в рукопожатии клиента означает происхождение скрипта, который устанавливает соединение. Origin сериализуется через ASCII и конвертируется в нижний регистр. Сервер МОЖЕТ использовать эту информацию при принятии решения о том, принимать ли входящее соединение. Если сервер не проверяет Origin, он будет принимать соединение откуда угодно. Если сервер решает не принимать соединение, он ОБЯЗАН вернуть соответствующий номер ошибки HTTP (то есть 403 Forbidden) и отменить рукопожатие по WebSocket, описанное в этой секции.
Уязвимость CSWSH связана со слабой или невыполненной проверкой заголовка Origin в рукопожатии клиента. Это разновидность уязвимости подделки межсайтовых запросов (CSRF), только для WebSocket. Если приложение WebSocket использует файлы cookie для управления сеансами пользователя, злоумышленник может подделать запрос на рукопожатие с помощью атаки CSRF и контролировать сообщения, отправляемые и получаемые через соединение WebSocket.
Материалы из последних выпусков становятся доступны по отдельности только через два месяца после публикации. Чтобы продолжить чтение, необходимо стать участником сообщества «Xakep.ru».
Членство в сообществе в течение указанного срока откроет тебе доступ ко ВСЕМ материалам «Хакера», позволит скачивать выпуски в PDF, отключит рекламу на сайте и увеличит личную накопительную скидку! Подробнее
1 год8460 р. |
1 месяц790 р. |
Я уже участник «Xakep.ru»
Читайте также
Последние новости