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

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

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

Та­кая тех­нология есть, она дав­но извес­тна и называ­ется OAuth. Имен­но ее исполь­зуют соц­сети и про­чие круп­ные сер­висы, ког­да пред­лага­ют авто­ризо­вать­ся через них.

Страница входа в AliExpress
Стра­ница вхо­да в AliExpress

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

В этой и сле­дующих стать­ях мы пошаго­во раз­берем, как работа­ют тех­нологии OAuth и OpenID Connect, в чем отли­чия раз­ных вер­сий про­токо­лов и какие рис­ки несут эти тех­нологии при неп­равиль­ной реали­зации. Мы так­же обя­затель­но прой­дем лабора­тории PortSwigger, что­бы научить­ся экс­плу­ати­ровать уяз­вимос­ти на прак­тике.

Нач­нем с базовых атак на OAuth.

Что такое OAuth и как он появился

На неофи­циаль­ном сай­те Ааро­на Парец­кого (Aaron Parecki), который собира­ет информа­цию об OAuth, при­веде­но крат­кое, прос­тое, емкое и доволь­но понят­ное опре­деле­ние:

OAuth 2.0 — это спо­соб авто­риза­ции, с помощью которо­го поль­зовате­ли могут пре­дос­тавлять веб‑сай­там или при­ложе­ниям дос­туп к сво­ей информа­ции, не переда­вая пароли.

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

Сам про­токол сущес­тву­ет уже дав­но. Идея соз­дания OAuth воз­никла во вре­мя раз­работ­ки Twitter OpenID (не сто­ит путать с OpenID Connect), в нояб­ре 2007 года, ког­да ком­пания соз­давала интегра­цию с Ma.gnolia. Это канув­ший в Лету соци­аль­ный сер­вис зак­ладок, где люди мог­ли делить­ся сох­ранен­ными ссыл­ками на сай­ты.

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

Схе­матич­но Authorization Code Flow в OAuth выг­лядит сле­дующим обра­зом.

Схематичное изображение Authorization Code Flow
Схе­матич­ное изоб­ражение Authorization Code Flow

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

Это отлично показы­вает схе­ма из докумен­тации Microsoft.

Альтернативная схема от Microsoft
Аль­тер­натив­ная схе­ма от Microsoft

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

Authorization Request

Пер­вый этап называ­ется Authorization Request — зап­рос на авто­риза­цию.

  1. Поль­зовать нажима­ет на кноп­ку Sign In на глав­ной стра­нице сай­та, что­бы вой­ти.
  2. Сайт показы­вает поп‑ап с выбором соци­аль­ной сети, через которую поль­зователь хочет вой­ти.

    Вход на сайт Midjourney
    Вход на сайт Midjourney
  3. Мы выбира­ем Continue with Discord, при­ложе­ние Midjourney генери­рует ссыл­ку на Discord и редирек­тит нас по ней. Там мы встре­чаем фор­му с прось­бой вой­ти в свой акка­унт.

Страница входа в Discord
Стра­ница вхо­да в Discord

При этом URL, по которо­му мы переш­ли, — это не сырая ссыл­ка на вход, вро­де /login. Она осо­бен­на тем, что содер­жит мно­жес­тво парамет­ров, которые были сге­нери­рова­ны при­ложе­нием, и выг­лядит сле­дующим обра­зом:

<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

Раз­берем каж­дый из парамет­ров:

В дан­ном слу­чае парамет­ры сле­дующие:

По­мимо identify, email и guids, есть и дру­гие атри­буты для парамет­ра scope, c ними мож­но озна­комить­ся в до­кумен­тации Discord.

Consent Screen и Authorization Response

Пос­ле того как мы вош­ли, нас встре­чает экран‑уве­дом­ление, который называ­ется Consent Screen. В нем говорит­ся о том, что кли­ент Midjourney хочет получить дос­туп к дан­ным о нас.

Страница Consent Screen в Discord
Стра­ница Consent Screen в Discord

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

Страница Consent Screen в Яндексе
Стра­ница Consent Screen в Яндексе

Даль­ше есть две кноп­ки: отка­зать­ся от пре­дос­тавле­ния дос­тупа или сог­ласить­ся. Если поль­зователь сог­лаша­ется, то сайт редирек­тит его обратно по при­шед­шему от при­ложе­ния в парамет­ре 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>

Сно­ва рас­смот­рим все парамет­ры по поряд­ку, пусть даже некото­рые из них пов­торя­ются:

При­мер реали­зации такого зап­роса на Python:

import requests
API_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.

Сайт Midjourney после входа
Сайт Midjourney пос­ле вхо­да

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

Authorization Code Flow, если бы мы заходили на сайт через Google
Authorization Code Flow, если бы мы заходи­ли на сайт через Google

Implicit Grant Flow

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

Наглядное изображение Implicit Grant Flow
Наг­лядное изоб­ражение 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.

Пример окна согласия при входе через Facebook
При­мер окна сог­ласия при вхо­де через Facebook

Шаг 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 в таком слу­чае оста­ется при­выч­ным. Но здесь важ­но то, как при­ложе­ние исполь­зует получен­ные дан­ные. Сей­час мы най­дем уяз­вимость в лабора­тории со сле­дующи­ми ввод­ными:

Лаба: 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, в отве­те на зап­рос /social-login.

Еще один редирект, на сервер авторизации
Еще один редирект, на сер­вер авто­риза­ции

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

Authorization Request на реальном примере
Authorization Request на реаль­ном при­мере

Вво­дим логин и пароль, сле­дует еще один редирект. На этот раз на 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)

Итак, мы вер­нулись на стра­ницу /oauth-callback основно­го при­ложе­ния, где его JavaScript говорит нам сде­лать зап­рос к при­ложе­нию, исполь­зуя поч­ту, имя поль­зовате­ля и токен.

Скрипт, который выполняет дальнейшую логику
Скрипт, который выпол­няет даль­нейшую логику

Этот зап­рос — зап­рос на аутен­тифика­цию, где

В ответ сер­вер воз­вра­щает нам свою куку, с которой мы будем ходить по сай­ту.

Мы успешно вошли на сайт, сайт вернул куку
Мы успешно вош­ли на сайт, сайт вер­нул куку

При этом походы из бра­узе­ра выг­лядят сле­дующим обра­зом: наш бра­узер → сер­вер (валида­ция токена) → Resource Owner.

Ес­ли токен валид­ный, то сер­вер счи­тает, что мы успешно аутен­тифици­рова­лись с поч­той, которая была ука­зана в зап­росе. И здесь мы натыка­емся на глав­ную проб­лему — сер­вер доверя­ет любым дан­ным, которые мы ука­зали в качес­тве email и username, выписы­вая куку на того поль­зовате­ля, который при­ходит в парамет­ре.

Что­бы прой­ти лабора­торию, мы дол­жны зай­ти от име­ни поль­зовате­ля carlos@carlos-montoya.net. Все, что для это­го нуж­но сде­лать, — заменить наш email адре­сом того поль­зовате­ля, от име­ни которо­го мы хотим зай­ти. И отпра­вить зап­рос, что­бы получить куку.

Что мы и дела­ем.

Запрос к API с чужой почтой и логином
Зап­рос к API с чужой поч­той и логином

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

Способ открыть ответ в браузере
Спо­соб открыть ответ в бра­узе­ре

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

Уведомление о прохождении лаборатории
Уве­дом­ление о про­хож­дении лабора­тории

CSRF-атаки на OAuth

Эксплуатация CSRF
Экс­плу­ата­ция CSRF

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

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

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

Как работает CSRF-токен
Как работа­ет CSRF-токен

Ра­бота­ет он очень прос­то:

  1. Кли­ент­ское при­ложе­ние (нап­ример, Midjourney) генери­рует уни­каль­ную стро­ку state и под­став­ляет ее в ссыл­ку, которая на эта­пе Authorization Request нап­равля­ет поль­зовате­ля на сер­вер авто­риза­ции.
  2. Пос­ле аутен­тифика­ции поль­зовате­ля сер­вер авто­риза­ции перенап­равля­ет его обратно на ука­зан­ный redirect_uri с тем же вклю­чен­ным в URL парамет­ром state.
  3. Пос­ле получе­ния отве­та кли­ент­ское при­ложе­ние срав­нива­ет получен­ное зна­чение state с ранее сге­нери­рован­ным и сох­ранен­ным зна­чени­ем. Если зна­чения сов­пада­ют, это под­твержда­ет, что зап­рос под­линный, и мож­но про­дол­жать аутен­тифика­цию.

Зло­умыш­ленник не может взять свою ссыл­ку с собс­твен­ным state и прис­лать какому‑то слу­чай­ному поль­зовате­лю, потому что она при­вязы­вает­ся к его куке, которая генери­рует­ся точ­но так же на пер­вом шаге. И CSRF-ата­ка ста­новит­ся невоз­можной.

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

Лаба: Forced OAuth profile linking

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

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

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

Страница входа
Стра­ница вхо­да

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

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

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

Сна­чала нас редирек­тит на стра­ницу вхо­да с помощью соци­аль­ной сети.

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

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

Экран с подтверждением доступа
Эк­ран с под­твержде­нием дос­тупа

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

Уведомление об успешной линковке профиля
Уве­дом­ление об успешной лин­ковке про­филя

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

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

Эксплоит-сервер
Экс­пло­ит‑сер­вер

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

Запрос на линковку аккаунта с кодом авторизации
Зап­рос на лин­ковку акка­унта с кодом авто­риза­ции

Как мож­но заметить, здесь нет парамет­ра, который бы защищал от CSRF-атак. Толь­ко code, который явля­ется фак­тором сог­ласия поль­зовате­ля. Что будет, если по этой ссыл­ке перей­дет адми­нис­тра­тор, который уже залоги­нен в собс­твен­ный акка­унт на сай­те?

Про­изой­дет неп­рият­ная ситу­ация:

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

Для реали­зации этой ата­ки нам нуж­но написать нем­ного 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.

Как быстро скопировать URL из Burp
Как быс­тро ско­пиро­вать URL из Burp

Код получил­ся такой:

<meta http-equiv="refresh" content="0; url=https://0aa000b304cfe7de81ffa71800960086.web-security-academy.net/oauth-linking?code=UV_ah4kbQkY7GdedaCFP6pwFxcM_z9CW9Hv0ePlfSNM" />

Пе­рехо­дим в Exploit Server и встав­ляем этот код в поле Body. Это будет содер­жимым стра­ницы по адре­су /exploit.

Эксплоит-сервер с нашей полезной нагрузкой
Экс­пло­ит‑сер­вер с нашей полез­ной наг­рузкой

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

Да­лее мы перехо­дим в Access log, что­бы пос­мотреть логи Nginx, который показы­вает все посеще­ния стра­ниц. Серым я замазал собс­твен­ный IP, но, кро­ме него, мож­но заметить и дру­гой — это IP адми­нис­тра­тора. Если он заходил на стра­ницу, зна­чит, бот сра­ботал.

Логи Nginx, где видно администратора
Ло­ги Nginx, где вид­но адми­нис­тра­тора

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

Страница входа
Стра­ница вхо­да

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

Личный кабинет администратора
Лич­ный кабинет адми­нис­тра­тора

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

Админка, откуда можно удалять пользователей
Ад­минка, отку­да мож­но уда­лять поль­зовате­лей

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

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

Выводы

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

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