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

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

  • Описание протокола
  • Установка соединения
  • Передача данных
  • Как работает уязвимость
  • 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, дос­тупные из интерне­та. Дос­таточ­но сфор­мировать прос­той зап­рос. Я не поленил­ся и сде­лал:

В резуль­тате наш­лось 55 тысяч адре­сов с обширной геог­рафи­ей.

WebSocket в мире 

Установка соединения

Раз­берем теперь, как работа­ет 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 переда­ются сооб­щения? Дан­ные по про­токо­лу WebSocket переда­ются как пос­ледова­тель­ность фрей­мов. Фрейм име­ет заголо­вок, в котором содер­жится сле­дующая информа­ция:

  • фраг­менти­рова­но ли сооб­щение;
  • тип переда­ваемых дан­ных — all code;
  • под­верга­лось ли сооб­щение мас­киров­ке — флаг мас­ки;
  • раз­мер дан­ных;
  • ключ мас­ки (32 бита);
  • дру­гие управля­ющие дан­ные (ping, pong…).

Фор­мат фрей­ма пред­став­лен на рисун­ке.

Фор­мат фрей­ма 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, а может и нет:

Уяз­вимость CSWSH свя­зана со сла­бой или невыпол­ненной про­вер­кой заголов­ка Origin в рукопо­жатии кли­ента. Это раз­новид­ность уяз­вимос­ти под­делки меж­сай­товых зап­росов (CSRF), толь­ко для WebSocket. Если при­ложе­ние WebSocket исполь­зует фай­лы cookie для управле­ния сеан­сами поль­зовате­ля, зло­умыш­ленник может под­делать зап­рос на рукопо­жатие с помощью ата­ки CSRF и кон­тро­лиро­вать сооб­щения, отправ­ляемые и получа­емые через соеди­нение WebSocket.

Источник: xakep.ru

Ответить

Ваш адрес email не будет опубликован. Обязательные поля помечены *