OAuth от и до
Изучаем протокол и разбираем базовые атаки на OAuth
Думаю, не нужно объяснять, что такое аутентификация на сайте. Если ты хочешь искать работу, оформлять заказы в интернете или пользоваться государственными услугами, тебя попросят подтвердить, что именно ты владелец аккаунта. И даже когда это не особенно нужно, разработчики все равно заставят тебя авторизоваться — просто чтобы было удобнее собирать о тебе данные. Но статья не об этичности, поэтому перейдем к самой теме.
Чтобы каждый раз не приходилось вбивать свою электронную почту, дожидаться прихода на нее сообщения с кодом или ссылкой для подтверждения аккаунта и придумывать сложный пароль, достаточно однажды зарегистрироваться на одном из мегапопулярных сайтов и входить под созданной учетной записью на остальных.
Такая технология есть, она давно известна и называется OAuth. Именно ее используют соцсети и прочие крупные сервисы, когда предлагают авторизоваться через них.

Этот метод входа стал настолько обыденным, что многие не задумываются о том, как он устроен, — а зря, ведь дьявол, как известно, кроется как раз в деталях. Мелочей здесь настолько много, что пришлось разделить гигантский материал по этой теме на три части.
В этой и следующих статьях мы пошагово разберем, как работают технологии OAuth и OpenID Connect, в чем отличия разных версий протоколов и какие риски несут эти технологии при неправильной реализации. Мы также обязательно пройдем лаборатории PortSwigger, чтобы научиться эксплуатировать уязвимости на практике.
Начнем с базовых атак на OAuth.
Что такое OAuth и как он появился
На неофициальном сайте Аарона Парецкого (Aaron Parecki), который собирает информацию об OAuth, приведено краткое, простое, емкое и довольно понятное определение:
OAuth 2.0 — это способ авторизации, с помощью которого пользователи могут предоставлять веб‑сайтам или приложениям доступ к своей информации, не передавая пароли.
Не надо вводить ничего лишнего: нажал кнопку, выдал согласие — и у приложения тут же оказались твои данные. В современном вебе OAuth поддерживает практически каждый крупный сайт, и действительно многие пользователи выбирают его вместо классических механизмов.
Сам протокол существует уже давно. Идея создания OAuth возникла во время разработки Twitter OpenID (не стоит путать с OpenID Connect), в ноябре 2007 года, когда компания создавала интеграцию с Ma.gnolia. Это канувший в Лету социальный сервис закладок, где люди могли делиться сохраненными ссылками на сайты.

Целью интеграции Twitter и Ma.gnolia было дать возможность Ma.gnolia использовать виджеты информационной панели Twitter. Блейн Кук, на то время ведущий разработчик в Twitter, заметил, что открытого механизма, который делегировал бы доступ к API, просто не существует.
На тот момент уже были похожие решения, о которых ты, наверное, мог и не слышать: AuthSub (Google), BBAuth (Yahoo), OpenAuth (AOL), Windows Live ID Web Authentication (Microsoft) и Facebook Auth (Meta). Но они были проприетарными и потому не годились. Еще более древние методы вроде передачи паролей небезопасны и неудобны для таких сценариев. OAuth был предложен как решение этой проблемы.

Вот как лаконично описывают OAuth разработчики Джон Панзер и Эран Хаммер‑Лахав, которые участвовали в создании открытой спецификации:
OAuth — это своего рода ключ доступа ко всем вашим веб‑сервисам. Ключ парковщика позволяет вам дать парковщику возможность припарковать ваш автомобиль, но не дает ему возможности попасть в багажник, проехать более двух миль или ограничить обороты вашего дорогого немецкого автомобиля. Точно так же ключ OAuth позволяет вам дать веб‑агенту возможность проверять вашу веб‑почту, но не возможность притворяться вами и отправлять почту всем знакомым из вашей адресной книги.
Финальный вариант реализации версии OAuth 1.0 был утвержден 4 декабря 2007 года (анонс можно найти в веб‑архиве), а уже в 2008 году этот протокол поддержал у себя Google, и аудитория протокола начала расти в геометрической прогрессии. К 2010 году Twitter заставил все сторонние приложения использовать свою реализацию OAuth 1.0.
Но за год до этого события, в 2009 году, была обнаружена атака с фиксацией сессии, позволявшая злоумышленникам получать доступ к чужим ресурсам. В результате этого аудита была разработана немного улучшенная версия протокола — OAuth 1.0a, в которой добавили параметры oauth_callback
и oauth_verifier
для защиты от таких атак.
Однако это не решило остальные проблемы OAuth 1.0 — небезопасность и неудобство использования. Разработчики жаловались, что первый OAuth требовал слишком больших криптографических вычислений на стороне клиента — запросы нужно было подписывать с помощью HMAC. Многим такая криптография срывала сроки разработки и серьезно усложняла тестирование.
За три года с момента зарождения первой версии протокола разработчики придумали более простое на уровне реализации, но более совершенное функционально решение, которое привносило много нововведений и которое стали называть OAuth 2.0. Этот протокол появился в 2010 году, а его последняя версия, в качестве RFC 6749, была опубликована в октябре 2012 года.
Разбираем устройство протокола на реальном примере
Чтобы было понятнее, давай использовать реальный пример. OAuth поддерживает несколько Flow, или «потоков», которые определяют, как именно происходит обмен информацией между клиентом, ресурсным сервером и сервером авторизации (с этими понятиями мы познакомимся немного позже), и самый используемый в современном вебе на десктопных сайтах — это Authorization Code Flow. С него и начнем.
Существует известный сайт legacy.midjourney.com, который позволяет создавать изображения с помощью генеративной нейросети.
Чтобы пользоваться его функциями и иметь возможность создавать картинки, надо войти в свой аккаунт, а модель авторизации построена как раз с помощью технологии OAuth.
В OAuth есть четыре сущности:
- Resource Owner (владелец ресурса) — под ресурсом подразумеваются различные данные пользователя, это может быть почта, номер телефона, имя, никнейм и так далее. Владелец ресурса — тот пользователь, который владеет этими данными;
- Application (приложение) или Client (клиент) — само приложение, которое требует доступ к ресурсам пользователя. В нашем примере это сервис Midjourney, который хочет получить наш юзернейм, аватарку и адрес электронной почты, на что требуется согласие от нас;
- Authorization Server (авторизационный сервер) — сервер, который выдает OAuth-токены клиенту после согласия владельца ресурса. На сайте Midjourney в качестве авторизационного сервиса выступают Discord и Gmail;
- Resource Server (сервер ресурсов) — это сайт, где хранятся почта, номер телефона, имя пользователя и другие данные, которые хочет получить клиент. Мы будем входить через Discord (на котором мы уже зарегистрировались когда‑то), а значит, Discord и будет выступать в качестве этого сервера.
Схематично Authorization Code Flow в OAuth выглядит следующим образом.

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

Пугаться обилию информации не стоит, мы разберем каждый из пунктов по порядку.
Authorization Request
Первый этап называется Authorization Request — запрос на авторизацию.
- Пользовать нажимает на кнопку Sign In на главной странице сайта, чтобы войти.
-
Сайт показывает поп‑ап с выбором социальной сети, через которую пользователь хочет войти.
Вход на сайт Midjourney Мы выбираем Continue with Discord, приложение Midjourney генерирует ссылку на Discord и редиректит нас по ней. Там мы встречаем форму с просьбой войти в свой аккаунт.

При этом URL, по которому мы перешли, — это не сырая ссылка на вход, вроде /
. Она особенна тем, что содержит множество параметров, которые были сгенерированы приложением, и выглядит следующим образом:
<https://discord.com/login>?redirect_to=/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
Разберем каждый из параметров:
-
client_id
— случайное значение, которое генерирует Discord или любой другой сервер авторизации. Этот ID выдается сайту, в данном случае Midjourney. Для каждого из сервисов‑клиентов это значение уникально. Оно нужно для того, чтобы Discord понимал, что за приложение запрашивает данные его пользователя, то есть для идентификации; -
redirect_uri
— URI, на который Discord должен перенаправить пользователя после успешной авторизации. В данном случае он должен вернуть его обратно на сайт Midjourney. Несколько уязвимостей завязано на неправильной обработке этого параметра, их мы рассмотрим позже; -
state
— уникальное значение, которое генерируется на каждую сессию авторизации, чтобы защититься от CSRF-атак. В одной из лабораторий мы тоже научимся их эксплуатировать для кражи чужого аккаунта; -
response_type
— если OAuth — это протокол авторизации, то параметрresponse_type
говорит о том, какой флоу этого протокола будет использоваться: их несколько, и они немного отличаются друг от друга. Сейчас приложение подставляет сюда значениеcode
, но тут также может бытьtoken
илиid
;token -
scope
— это те данные или эндпоинты, на которые клиентское приложение Midjourney хочет получить у сервера ресурсов доступ.
В данном случае параметры следующие:
-
identify
открывает приложению возможность обращаться к эндпоинту/
Discord, чтобы получить информацию о пользователе, но без email;users/ @me -
email
— добавляет согласие на выдачу email. Предыдущий эндпоинт теперь будет возвращать и его тоже; -
guilds
— нужен, чтобы клиент мог обращаться к эндпоинту/
, который возвращает имя пользователя, его ID, баннер и другие параметры.users/ @me/ guilds
Помимо identify
, email
и guids
, есть и другие атрибуты для параметра scope
, c ними можно ознакомиться в документации Discord.
Consent Screen и Authorization Response
После того как мы вошли, нас встречает экран‑уведомление, который называется Consent Screen. В нем говорится о том, что клиент Midjourney хочет получить доступ к данным о нас.

Порой некоторые поля авторизационный сервер делает опциональными и пользователь может убирать данные, которые будут переданы. У Яндекса есть хороший пример.

Дальше есть две кнопки: отказаться от предоставления доступа или согласиться. Если пользователь соглашается, то сайт редиректит его обратно по пришедшему от приложения в параметре redirect_uri
URI.
В случае с Midjourney эта ссылка выглядит так:
https://midjourney.com/api/auth/callback/discord/
?code=<CENSORED>
&state=dLybVCqXiFHNLYcwiOLhrVKoQHMJwRKU9W7BxoDIGNo
Здесь code
— это код авторизации, который сгенерировал только что OAuth-провайдер, этот код здесь ключевой и будет использоваться в дальнейшем как подтверждение того, что пользователь предоставил доступ к своим данным, а state
— параметр из предыдущего шага, выполняющий роль CSRF-токена.
В момент, когда мы нажимаем OK, в базу данных сервера авторизации попадает тот самый grant
: записываются данные о том, что такой‑то пользователь дал такие‑то доступы такому‑то сервису. Клиент получает идентификатор успешной аутентификации, который ассоциируется с данными в базе данных. В нашем случае это code
.
Access token request
Итак, пользователь согласился предоставить свои данные, Discord выполнил редирект с кодом обратно на Midjourney. Теперь Midjourney, получив код авторизации, должен обменять его на OAuth-токен, чтобы иметь возможность им воспользоваться и запросить сведения о пользователе.
Такой флоу может показаться кому‑то необычным: почему бы сразу не отдать токен? Это нужно для того, чтобы злоумышленник, вставший посередине между пользователем и сервером, не мог перехватить OAuth-токен.
Приложение Midjourney, получив code
от только что пришедшего пользователя, где‑то у себя на бэкенде делает следующий запрос на сервер авторизации (пользователь его не увидит):
POST /api/v10/oauth2/token HTTP/1.1
Host: discord.com
…
client_id=12345
&client_secret=SECRET
&redirect_uri=https://midjourney.com/callback
&grant_type=authorization_code
&code=<CENSORED>
Снова рассмотрим все параметры по порядку, пусть даже некоторые из них повторяются:
-
client_id
— уникальное значение, которое Discord заранее выдал Midjourney, чтобы тот однозначно себя идентифицировал и Discord понимал, что к нему пришел именно Midjourney; -
client_secret
— как я уже говорил, злоумышленник мог бы перехватитьcode
пользователя и от имени Midjourney (с егоclient_id
) обменять код на OAuth-токен, но параметрclient_secret
дает некоторые гарантии, что он не сможет это сделать. Приложение должно держатьclient_secret
в секрете и использовать только при запросах на сервер ресурсов, а сервер должен сверятьclient_id
иclient_secret
; -
redirect_uri
— говорит серверу ресурсов, куда он должен вернуть токен; -
grant_type
— тип используемой авторизации, в данном случае это знакомый намauthorization_code
, следующий далее; -
code
— код, который ранее передал пользователь, обозначает его согласие на передачу данных.
Пример реализации такого запроса на Python:
import requestsAPI_ENDPOINT = 'https://discord.com/api/v10'CLIENT_ID = '332269999912132097'CLIENT_SECRET = '937it3ow87i4ery69876wqire'REDIRECT_URI = 'https://xakep.ru'def exchange_code(code): data = { 'grant_type': 'authorization_code', 'code': code, 'redirect_uri': REDIRECT_URI } headers = { 'Content-Type': 'application/x-www-form-urlencoded' } r = requests.post('%s/oauth2/token' % API_ENDPOINT, data=data, headers=headers, auth=(CLIENT_ID, CLIENT_SECRET)) r.raise_for_status() return r.json()
Access token grant
Сервер авторизации валидирует client_id
и client_secret
, чтобы убедиться, что к нему пришел настоящий Midjourney. И сверяет code
с тем, который он выдал пользователю. Если все эти данные верны, то он выписывает access_token
(он же OAuth-токен) и отдает его в ответе, с которым приложение Midjourney уже сможет получить настоящие данные пользователя.
{ "access_token": "6qrZcUqja7812RVdnEKjpzOL4CvHBFG", "token_type": "Bearer", "expires_in": 604800, "refresh_token": "D43f5y0ahjqew82jZ4NViEr2YafMKhue", "scope": "identify"}
OAuth-токен уникальный, живет определенное время, и на каждого пользователя он свой. Помимо самого токена, приложение возвращает некоторые другие атрибуты, вроде типа токена, срока жизни и скоупа данных, на которые он выдается.
API Call и Resource Grant
Midjourney наконец получает этот токен и должен сохранить его где‑то у себя, чаще всего хранилищем выступает база данных. Теперь он может ходить с этим токеном в API Discord и получать актуальные данные о пользователе.
Вот пример запроса на получение информации о пользователе по OAuth-токену. Пользователь указан в заголовке Authorization:
GET /api/v10/users/@me HTTP/1.1
Host: discord.com
Authorization: Bearer z0y9x8w7v6u5
На этом этапе флоу заканчивается. Midjourney получает почту пользователя, его имя и другие данные, указанные в скоупе.
{"username":"carlos","[email":"carlos@carlos-montoya.net](mailto:email%22:%22carlos@carlos-montoya.net)",…}
Midjourney сохраняет эти данные у себя в базе и регистрирует пользователя. Далее пользователь перенаправляется в личный кабинет, и уже там отображается его юзернейм, который был получен по токену из API Discord.

Напоследок для закрепления знаний оставлю еще одну схему — на этот раз на примере аутентификации через Google на все том же сайте Midjourney.

Implicit Grant Flow
В зависимости от цели разработчики могут использовать разные флоу OAuth. Второй поток называется Implicit Grant Flow, и он проще по сравнению с тем, который мы только что разобрали.

Помнишь параметр response_type
, который указывало приложение? В прошлом флоу туда подставлялся code
, теперь же это token
. После этапа с Authorization Code Grant приложение вместо кода будет возвращать сразу токен — минуя этап с обменом code
на token
.
Давай вкратце пройдемся по каждому шагу.
Шаг 1. Пользователь нажимает «Войти через Midjourney», приложение формирует ссылку и редиректит пользователя на сервер авторизации.
Ссылка та же, что и раньше, но, как видишь, response_type
изменился:
<https://discord.com/login>?redirect_to=/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
Значение response_type
равно token
— это говорит о том, что используется Implicit Grant.
Шаг 2. Пользователь вводит свои данные и дает согласие на передачу информации о себе в приложение, страница с согласием выглядит, как и раньше. Для разнообразия просто приведем ту, которую показывает Facebook при попытке входа на AliExpress.

Шаг 3. Сервер авторизации формирует ссылку, в которую зашивает токен OAuth, и редиректит пользователя обратно.
Ссылка выглядит примерно так:
#access_token=g0ZGZmNj4mOWIjNTk2Pw1Tk4ZTYyZGI3&token_type=Bearer&expires_in=600&state=xcoVv98y2kd44vuqwye3kcq
Вместо code
, который был в прошлом флоу, возвращается сразу access_token
.
Шаг 4. Клиентская часть приложения извлекает токен и начинает использовать его в запросах к серверу ресурсов.
Поэтому схема и называется упрощенной, теперь токен напрямую возвращается в браузер пользователя. Implicit Grant может использоваться в SPA-приложениях (Single Page Application) и храниться на стороне пользователя. Либо в мобильных приложениях, у которых отсутствует серверная часть.
В настоящее время такой флоу не рекомендуется использовать, но мы увидим его в лабораториях.
OAuth и аутентификация
Прежде чем мы приступим к решению лабораторных, сделаю важное замечание: изначально OAuth не предназначался для аутентификации. Это протокол для авторизации, цель которого — выдать доступ к определенным данным владельца ресурса сторонним приложениям, а не аутентифицировать его.
Вернемся к примеру с машиной. Если ты взял напрокат, то есть воспользовался услугами каршеринга, BMW и получил от него ключи, это не значит, что машина теперь твоя. Так и в OAuth — если ты принес токен сервису, тот не знает, как у тебя появился этот токен, и предоставляет лишь на право на использование некоторых ресурсов.
Но со временем многие начали использовать этот протокол не по назначению. Есть сайты, которые придумали делать на основе OAuth аутентификацию, что противоречит изначальной концепции и порой влечет за собой критические уязвимости с компрометацией аккаунтов.
Флоу OAuth в таком случае остается привычным. Но здесь важно то, как приложение использует полученные данные. Сейчас мы найдем уязвимость в лаборатории со следующими вводными:
- OAuth используется для аутентификации в режиме Implicit Grant;
- после получения токена доступа браузер пользователя обращается к серверу ресурсов и использует полученные данные для входа в систему;
- веб‑приложение доверяет пользовательскому вводу и принимает токен доступа в качестве пароля.
Лаба: Authentication bypass via OAuth implicit flow
Implicit flow — это тот сценарий, когда токен возвращается напрямую пользователю. Сейчас мы на практике увидим, как неправильная настройка может привести к компрометации любого аккаунта на сайте.
Запускаем лабораторию и попадаем на главную страницу блога. Так как мы имеем дело с OAuth, нам нужно перейти в раздел My account, чтобы проследовать полному флоу и иметь возможность проанализировать его.

Нажимаем My account, и вот мы на странице входа.

Вводим выданные нам перед началом решения данные для входа (логин wiener
, пользователь peter
), подтверждаем выдачу доступа к профилю и электронной почте.

Теперь мы попали в собственный личный кабинет, но по заданию надо попасть в чужой.

Давай внимательно посмотрим на запросы и разберемся, как устроен процесс входа.
Первый запрос — на страницу My account. Поскольку мы не были аутентифицированы, нас перенаправило на страницу входа через социальную сеть, без возможности использовать стандартную аутентификацию и авторизацию (на сайте ее просто нет).

Следующий запрос — это Authorization Request. Основное приложение создало HTML-код с редиректом с помощью тега meta
, который ведет на сервер авторизации.
<meta http-equiv=refresh content='3;url=https://oauth-0aeb00740370fc9c81e39155023700ad.oauth-server.net/auth?client_id=corfmvitwnpodbwncbim3&redirect_uri=https://0afc0017030dfc668162939300c10098.web-security-academy.net/oauth-callback&response_type=token&nonce=1924369299&scope=openid%20profile%20email'>
Этот же редирект можно увидеть на вкладке Proxy History, в ответе на запрос /
.

Сервер авторизации редиректит нас на страницу входа.

Вводим логин и пароль, следует еще один редирект. На этот раз на Consent Screen, где мы должны подтвердить свое согласие на выдачу доступа к собственным данным.

Соглашаемся простым POST-запросом.

Сервер OAuth еще раз перенаправляет нас — на этот раз наконец на клиентское приложение.

В ссылке возврата можно найти наш access_token
, срок его жизни и тип. Учитывая, что это упрощенный флоу, токен возвращается в браузер пользователя.
[https://0afc0017030dfc668162939300c10098.web-security-academy.net/oauth-callback
#access_token=0Ue2cGVe8U7e4xzKc_EFAxxipYPbDaRfP9YTXtdKxGV
&expires_in=3600
&token_type=Bearer
&scope=openid profile email](https://0afc0017030dfc668162939300c10098.web-security-academy.net/oauth-callback#access_token=0Ue2cGVe8U7e4xzKc_EFAxxipYPbDaRfP9YTXtdKxGV&expires_in=3600&token_type=Bearer&scope=openid%20profile%20email)
Итак, мы вернулись на страницу /
основного приложения, где его JavaScript говорит нам сделать запрос к приложению, используя почту, имя пользователя и токен.

Этот запрос — запрос на аутентификацию, где
- в качестве
email
иusername
используются полученные данные из сервера ресурсов; - в качестве пароля используется
access_token
, который мы получили от сервера авторизации.
В ответ сервер возвращает нам свою куку, с которой мы будем ходить по сайту.

При этом походы из браузера выглядят следующим образом: наш браузер → сервер (валидация токена) → Resource Owner.
Если токен валидный, то сервер считает, что мы успешно аутентифицировались с почтой, которая была указана в запросе. И здесь мы натыкаемся на главную проблему — сервер доверяет любым данным, которые мы указали в качестве email
и username
, выписывая куку на того пользователя, который приходит в параметре.
Чтобы пройти лабораторию, мы должны зайти от имени пользователя carlos@carlos-montoya.
. Все, что для этого нужно сделать, — заменить наш email адресом того пользователя, от имени которого мы хотим зайти. И отправить запрос, чтобы получить куку.
Что мы и делаем.

Если ты все сделал правильно, сервер должен выдать новую куку. Эта кука принадлежит пользователю carlos@carlos-montoya.
. Сервер принимает от нас email
и username
и безоговорочно доверяет им, в этом и заключается уязвимость. После того как мы отправили данные, он просто валидирует токен и, если он корректный, направляет в личный кабинет того пользователя, которого мы указали в запросе.

Открываем в браузере страницу с помощью опции Request in browser → In original session и убеждаемся в том, что мы успешно прошли первое испытание.

CSRF-атаки на OAuth

OAuth — это очень гибкий протокол, который предоставляет много возможностей для разработчиков. Именно по этой причине в нем и возникают уязвимости — разработчики хотят написать свой сервис как можно быстрее и пользуются кодом из первых же ссылок в Google, порой написанным с грубейшими ошибками.
В лаборатории, которую мы сейчас рассмотрим, нужно сначала зарегистрироваться классическим способом. А после регистрации пользователь имеет возможность прилинковать в настройках аккаунт своей социальной сети, чтобы входить через него в тот, который был зарегистрирован на первом этапе.
Звучит необычно, но такое действительно встречается (хотя и реже, чем раньше). Камнем преткновения здесь становится параметр state
, который служит защитой от CSRF-атак, чтобы хакер не мог выполнить действие от имени другого пользователя.

Работает он очень просто:
- Клиентское приложение (например, Midjourney) генерирует уникальную строку
state
и подставляет ее в ссылку, которая на этапе Authorization Request направляет пользователя на сервер авторизации. - После аутентификации пользователя сервер авторизации перенаправляет его обратно на указанный
redirect_uri
с тем же включенным в URL параметромstate
. - После получения ответа клиентское приложение сравнивает полученное значение
state
с ранее сгенерированным и сохраненным значением. Если значения совпадают, это подтверждает, что запрос подлинный, и можно продолжать аутентификацию.
Злоумышленник не может взять свою ссылку с собственным state
и прислать какому‑то случайному пользователю, потому что она привязывается к его куке, которая генерируется точно так же на первом шаге. И CSRF-атака становится невозможной.
Но параметр state
— необязательный, и многие разработчики им пренебрегают. В этой лаборатории его не будет, и мы увидим на реальном примере, как можно заполучить контроль над аккаунтом администратора сайта благодаря возникшей уязвимости.
Лаба: Forced OAuth profile linking
Открываем главную страницу уже знакомого нам блога, но с новыми постами.

Переходим в My account. Теперь нам выдали сразу две учетные записи: первую — классическую для входа через сайт, а вторую — для входа через социальную сеть. Поскольку к нашему аккаунту социальная сеть еще не привязана, мы входим классическим способом.

Вводим данные пользователя wiener
и попадаем в личный кабинет. Помимо уже привычных полей username
и email
, которые были в прошлых лабораториях, мы можем заметить тут свой ключ API и возможность прилинковать к текущему аккаунту социальный профиль, чтобы заходить через него в уже созданный личный кабинет без ввода логина и пароля.

Давай нажмем Attach a social profile и проследим за полным флоу, точно так же, как мы это уже делали раньше. Поскольку это неотъемлемая часть поиска уязвимостей.
Сначала нас редиректит на страницу входа с помощью социальной сети.

Здесь мы вводим пароль от второго выданного аккаунта peter.
и подтверждаем выдачу доступа к своему профилю и электронной почте.

Получаем уведомление о том, что профиль привязан.

Флоу не сильно отличается от предыдущего, но теперь он используется в новом сценарии. Наша задача, как и раньше, состоит в том, чтобы скомпрометировать администратора. В этом нам поможет уязвимость, которую мы сейчас обнаружим, а также Exploit Server, с помощью которого мы можем доставить полезную нагрузку до администратора.
Раньше его не было в заданиях, но в этом сценарии требуется взаимодействие с пользователем, поэтому он просто необходим. На Exploit Server можно разместить собственную полезную нагрузку и заманить администратора. Все CSRF-атаки заключаются в том, чтобы от имени пользователя отправить какой‑либо запрос, а без верстки это не сделать.

Снова посмотрим HTTP History в Burp Suite. Здесь нас интересует Authorization Response — тот редирект в само приложение, который происходит, когда мы соглашаемся выдать доступ к нашим данным.

Как можно заметить, здесь нет параметра, который бы защищал от CSRF-атак. Только code
, который является фактором согласия пользователя. Что будет, если по этой ссылке перейдет администратор, который уже залогинен в собственный аккаунт на сайте?
Произойдет неприятная ситуация:
- Администратор перейдет по ссылке, которая линкует социальный аккаунт хакера к аккаунту того, кто перешел, то есть самого админа.
- Хакер сможет зайти через собственный аккаунт социальной сети и попасть не в свой аккаунт, а в аккаунт того, кого он атаковал, в данном случае это администратор.
Для реализации этой атаки нам нужно написать немного HTML, который будет выполнять этот редирект. Если у тебя есть Pro-версия Burp Suite, то он умеет автоматически генерировать форму для CSRF-атаки: кликай правой кнопкой мыши на запросе → Engagement Tools → Generate CSRF PoC. Но мы сделаем это вручную, здесь это будет очень просто.
Пишем HTML, который выполнит редирект с нашим code
. Здесь мы используем тег meta
с атрибутом http-equiv
, чтобы направить пользователя по указанному в content
урлу. 0
— это задержка перед редиректом, то есть нам нужно сделать его сразу, как пользователь попадает на страницу.
<meta http-equiv="refresh" content="0; url=https://<поддомен лаборатории>.web-security-academy.net/oauth-linking?code=<code>" />
Копируем разметку выше и вставляем в него ссылку с линковкой. Кликай правой кнопкой мыши на запросе и выбирай Copy URL.

Код получился такой:
<meta http-equiv="refresh" content="0; url=https://0aa000b304cfe7de81ffa71800960086.web-security-academy.net/oauth-linking?code=UV_ah4kbQkY7GdedaCFP6pwFxcM_z9CW9Hv0ePlfSNM" />
Переходим в Exploit Server и вставляем этот код в поле Body. Это будет содержимым страницы по адресу /
.

Можно проверить, правильно ли все отработает: жми Store, чтобы сохранить, а потом зайди на саму страницу. А если ты уверен, что с первой попытки все сделал правильно, можешь сразу позвать администратора. Для этого нажимаем на Deliver exploit to victim.
Далее мы переходим в Access log, чтобы посмотреть логи Nginx, который показывает все посещения страниц. Серым я замазал собственный IP, но, кроме него, можно заметить и другой — это IP администратора. Если он заходил на страницу, значит, бот сработал.

Теперь возвращаемся в блог и логинимся через социальную сеть.

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

Заходим в админку и удаляем Карлоса (это нужно сделать по условию задачи).

И получаем уведомление об успешном прохождении.

Выводы
В этой статье мы познакомились с технологией OAuth, немного погрузились в историю и узнали, как появился этот протокол авторизации, разобрали, как он работает, на примере Midjourney, познакомились с Authorization Code Flow и его упрощенным вариантом Implicit Grant Flow и решили две лабораторные работы с базовыми атаками на протокол, которые довольно просто реализовать.
В следующих статьях мы узнаем о еще нескольких технологиях, без которых не обходится современный веб, а также научимся комбинировать разные уязвимости в цепочки, чтобы осуществлять атаки на более сложные и защищенные сайты.