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

info

Это зак­лючитель­ная часть цик­ла ста­тей о про­токо­ле авто­риза­ции OAuth. Читай так­же статьи «OAuth от и до. Изу­чаем про­токол и раз­бира­ем базовые ата­ки на OAuth» и «OAuth от и до. Ищем цепоч­ки уяз­вимос­тей при ата­ках на авто­риза­цию».

Разница между OAuth 1.0 и OAuth 2.0

Преж­де чем мы перей­дем к OpenID Connect, давай нем­ного отка­тим­ся назад и выяс­ним, в чем же все‑таки раз­ница меж­ду OAuth 1.0 и OAuth 2.0 и почему нам при­ходит­ся раз­бирать нес­коль­ко ите­раций про­токо­ла. Для это­го при­выч­ным обра­зом пос­мотрим флоу, на этот раз уста­рев­шего OAuth 1.0 — того самого, с которо­го все и началось в Twitter.

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

Схема работы OAuth 1.0
Схе­ма работы OAuth 1.0

Здесь у нас сно­ва нем­ного изме­нил­ся ней­минг, обоз­начим все сто­роны:

  1. User — это поль­зователь.
  2. Consumer (пот­ребитель) — при­ложе­ние, которое хочет получить дос­туп к ресур­сам поль­зовате­ля.
  3. Service Provider (пос­тавщик услуг) — при­ложе­ние, которое пре­дос­тавля­ет дос­туп к ресур­сам поль­зовате­ля.

Как мож­но с ходу заметить, сер­вера авто­риза­ции, который есть в OAuth 2.0, здесь нет.

Получение Request Token

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

Пос­коль­ку Twitter на момент написа­ния статьи все еще под­держи­вает этот про­токол, рас­смот­рим имен­но на при­мере логина в этот сер­вис. При­мер зап­роса на получе­ние рек­вест‑токена:

POST /oauth/request_token HTTP/1.1
Host: [api.twitter.com](http://api.twitter.com/)
Authorization: OAuth oauth_callback="https%3A%2F%[2Fwww.example.com](http://2fwww.example.com/)%2Foauth%2Fcallback%2Ftwitter",
oauth_consumer_key="cChZNFj6T5R0TigYB9yd1w",
oauth_nonce="ea9ec8429b68d6b77cd5600adbbb0456",
oauth_signature_method="HMAC-SHA1",
oauth_timestamp="1318467427",
oauth_version="1.0",
oauth_signature="qCMIWvfCvmP0ZsfWGTPnuaPXsQE%3D"

Все эти парамет­ры (за исклю­чени­ем oauth_callback) мы видим впер­вые. Кро­ме того, они переда­ются в Authorization-заголов­ке вмес­то при­выч­ных GET- и POST-парамет­ров. Вот за что они отве­чают:

Сам механизм под­писи доволь­но прост.

К каким данным применяется HMAC
К каким дан­ным при­меня­ется HMAC

Ес­ли ты хоть нем­ного зна­ком с HMAC, то зна­ешь, что это крип­тогра­фичес­ки безопас­ная (то есть необ­ратимая) ком­бинация опре­делен­ной стро­ки с общим сек­ретом. Все отправ­ляемые дан­ные кон­катени­руют­ся, и к ним при­меня­ется HMAC с сек­ретом, который известен пот­ребите­лю и сер­вис‑про­вай­деру (oauth_consumer_secret).

В качес­тве фун­кции может исполь­зовать­ся HMAC-SHA-1, RSA-SHA-1 или любая дру­гая, которую заранее обго­ворят пот­ребитель и сер­вис‑про­вай­дер, спе­цифи­кация это не рег­ламен­тиру­ет.

Выдача неавторизованного токена OAuth

Пос­ле того как Twitter получил зап­рос, он дол­жен аутен­тифици­ровать при­ложе­ние. Для это­го он про­веря­ет, что под­пись была соз­дана соот­ветс­тву­ющим клю­чом пот­ребите­ля и его сек­ретом. Если все сош­лось, Twitter генери­рует токен OAuth вмес­те с сек­ретом и отда­ет их в отве­те:

HTTP/1.1 200 OK
Content-Type: application/x-www-form-urlencoded
oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0&
oauth_callback_confirmed=true

Здесь все­го два парамет­ра:

Редирект пользователя на service-провайдер

Пот­ребитель не может исполь­зовать токен OAuth до тех пор, пока тот не авто­ризо­ван. Что­бы его авто­ризо­вать, он нап­равля­ет поль­зовате­ля по ссыл­ке на сер­вис‑про­вай­дер, где единс­твен­ный параметр — тот самый токен из пре­дыду­щего шага.

HTTP/1.1 302 Found
Location: https://api.twitter.com/oauth/authenticate?oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0

Бра­узер поль­зовате­ля сле­дует редирек­ту и отправ­ляет при­мер­но такой зап­рос на стра­ницу Twitter:

GET /oauth/authenticate?oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0 HTTP/1.1
Host: api.twitter.com

Аутентификация пользователя и получение согласия

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

Пос­ле того как поль­зователь под­твержда­ет дос­туп, сер­вис‑про­вай­дер воз­вра­щает его на сайт пот­ребите­ля, вмес­те с тем же токеном и парамет­ром oauth_verifier. Этот параметр появил­ся в OAuth 1.0a и явля­ется кодом верифи­кации, который мы уже зна­ем по OAuth 2.0.

HTTP/1.1 302 Found
Location: [https://www.example.com/oauth/callback/twitter?oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0&oauth_verifier=uw7NjWHT6OJ1MpJOXsHfNxoAhPKpgI8BlYDhxEjIBY](https://www.example.com/oauth/callback/twitter?oauth_token=NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0&oauth_verifier=uw7NjWHT6OJ1MpJOXsHfNxoAhPKpgI8BlYDhxEjIBY)

Получение access-токена

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

Суть это­го зап­роса — обме­нять авто­ризо­ван­ный oauth_token на access_token.

В зап­росе отправ­ляют­ся:

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

POST /oauth/get_access_token HTTP/1.1
Host: [api.twitter.com](http://api.twitter.com/)
Authorization: OAuth oauth_consumer_key="cChZNFj6T5R0TigYB9yd1w",
oauth_signature_method="HMAC-SHA1",
oauth_signature="nPPh4sLZaCrSAD2moyG6%2Bp8lPuM%3D"
oauth_timestamp="1340653420",
oauth_token="NPcudxy0yU5T3tBzho7iCotZ3cnetKwcTIRlX0iwRl0",
oauth_nonce="OT1DI4X0Wer1ezbuhCnoqCFr9qjrmQZ6",
oauth_version="1.0"

В отве­те воз­вра­щает­ся access_token — токен, с помощью которо­го мож­но получать дос­туп к ресур­сам.

Вот так:

HTTP/1.1 200 OK
Content-Type: application/x-www-form-urlencoded
access_token=6cccgqOBPO4d3MikG5BkSv2ONp1rBZRtSZW9E9OAppr

Получение доступа к защищенным ресурсам

Пос­ле получе­ния access-токена пот­ребитель может получить дос­туп к защищен­ным ресур­сам от име­ни поль­зовате­ля. Зап­рос дол­жен быть под­писан и дол­жен содер­жать сле­дующие парамет­ры:

На этом флоу окон­чен.

Еще одна обоб­щающая схе­ма.

Схема работы OAuth 1.0
Схе­ма работы OAuth 1.0

Проблемы OAuth

Сравнение первой и второй версии OAuth
Срав­нение пер­вой и вто­рой вер­сии OAuth

Глав­ные проб­лемы OAuth 1.0 и OAuth 1.0a были в слож­ности реали­зации это­го про­токо­ла, имен­но на нее и жалова­лись раз­работ­чики. Ты сам толь­ко что про­читал весь флоу и убе­дил­ся, что на всех эта­пах исполь­зует­ся мно­го крип­тогра­фии.

Кро­ме это­го, поль­зовате­ли отме­чали сле­дующее:

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

Как мы уже узна­ли, на сме­ну OAuth 1.0a при­шел OAuth 2.0, став­ший пол­ноцен­ным фрей­мвор­ком. Но есть еще один про­токол, который в пер­спек­тиве выг­лядит гораз­до инте­рес­нее пре­дыду­щих, пос­коль­ку еще более стан­дарти­зиро­ван и реша­ет некото­рые проб­лемы OAuth 2.0. Его сей­час мы и рас­смот­рим более деталь­но.

Знакомство с OpenID Connect

Как OpenID связан с OAuth

Логотип OpenID Connect
Ло­готип OpenID Connect

OpenID Connect — это стан­дарт, который рас­ширя­ет исходный про­токол авто­риза­ции OAuth и опи­сыва­ет, как пос­тро­ить поверх него логику с иден­тифика­цией и аутен­тифика­цией поль­зовате­ля.

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

OpenID Connect в решении с Hybrid Flow может гаран­тировать как авто­риза­цию, так и аутен­тифика­цию, что дела­ет его уни­вер­саль­ным и более совер­шенным ана­логом клас­сичес­кого OAuth, OAuth 1.0a и OAuth 2.0.

OAuth не решает задачу аутентификации

Аутентификация и псевдоаутентификация
Аутен­тифика­ция и псев­доаутен­тифика­ция

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

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

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

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

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

Аутентификация и авторизация
Аутен­тифика­ция и авто­риза­ция

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

Мо­гут воз­никнуть воп­росы, нап­ример: «Может быть, спро­сить у поль­зовате­ля, какой email был ука­зан у вла­дель­ца токена? Или нуж­но уточ­нить его номер телефо­на с кодовым сло­вом, которые знал бы толь­ко соз­датель акка­унта на сер­вере ресур­сов?» Но все это не поз­воля­ет точ­но аутен­тифици­ровать поль­зовате­ля.

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

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

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

Принцип работы OpenID Connect

Для начала уточ­ним тер­миноло­гию. OpenID Connect вво­дит нес­коль­ко клю­чевых тер­минов, нез­накомых по обыч­ному OAuth. Вот они:

Hybrid Flow

Для начала прос­тая кар­тинка.

Простая схема OpenID Connect
Прос­тая схе­ма OpenID Connect

А теперь более слож­ная и более под­робная.

Сложная схема OpenID Connect
Слож­ная схе­ма OpenID Connect

OpenID под­держи­вает Client Credentials Grant, Resource Owner Password Grant, Implicit Flow и Hybrid Flow. Рас­смот­рим флоу аутен­тифика­ции Hybrid Flow, который ком­биниру­ет эле­мен­ты Authorization Code Flow и Implicit Flow из OAuth 2.0:

  1. Authentication Request. Про­веря­ющая сто­рона (Relying Party) ини­циирует зап­рос аутен­тифика­ции, нап­равляя бра­узер поль­зовате­ля OpenID-про­вай­деру (OpenID Provider).
  2. Authentication. Про­вай­дер OpenID аутен­тифици­рует поль­зовате­ля. Этот про­цесс может раз­личать­ся в зависи­мос­ти от про­вай­дера, но обыч­но он вклю­чает зап­рос учет­ных дан­ных поль­зовате­ля и их про­вер­ку.
  3. Authorization Code. Пос­ле аутен­тифика­ции поль­зовате­ля про­вай­дер OpenID перенап­равля­ет бра­узер поль­зовате­ля обратно про­веря­ющей сто­роне с кодом авто­риза­ции.
  4. Token Request. Про­веря­ющая сто­рона обме­нива­ет этот код авто­риза­ции на identity-токен и access-токен.
  5. Token Verification. Identity Token — это JWT-токен, в котором хра­нит­ся информа­ция об аутен­тифика­ции конеч­ного поль­зовате­ля. Про­веря­ющая сто­рона про­веря­ет этот токен, что­бы убе­дить­ся в его дей­стви­тель­нос­ти и получить информа­цию о поль­зовате­ле.

Как мож­но заметить, бег­ло про­читав эти пун­кты, в обыч­ном OAuth воз­вра­щает­ся толь­ко access-токен, в OpenID еще воз­вра­щает­ся identity-токен, наличие которо­го под­твержда­ет, что поль­зователь аутен­тифици­ровал­ся.

Вто­рое отли­чие зак­люча­ется в ско­упах. На эта­пе Authentication Request в парамет­ре scope всег­да отправ­ляет­ся зна­чение openid, а пос­ле него сле­дуют области дан­ных, к которым при­ложе­ние хочет получить дос­туп, нап­ример scope=openid profile email phone. Это не обыч­ные ско­упы, а клей­мы, за каж­дым из которых может быть зак­репле­но нес­коль­ко атри­бутов.

Кро­ме того, если в базовом OAuth каж­дый сер­вер авто­риза­ции ука­зывал собс­твен­ный набор полей, то в OpenID спи­сок полей у всех про­вай­деров уни­фици­рован и опи­сан в спе­цифи­кации.

И третье отли­чие зак­люча­ется в наличии трех обя­затель­ных эндпо­интов:

А так­же опци­ональ­ных:

Один из них мы рас­смот­рим далее более под­робно.

Что зашито в Identity Token

Наг­лядное срав­нение identity-токена и access-токена мож­но пос­мотреть на кар­тинке ниже.

Сравнение identity- и access-токенов
Срав­нение identity- и access-токенов

Мы не будем под­робно раз­бирать access-токен, потому что и так о нем уже дос­таточ­но зна­ем и работа­ли с ним на прак­тике. Важ­но раз­личать токены и знать, что access-токен исполь­зует­ся для получе­ния дан­ных о поль­зовате­ле от про­вай­дера OpenID, а ID — для его аутен­тифика­ции про­веря­ющей сто­роной. Поэто­му прис­таль­нее раз­берем Identity Token.

Identity Token мож­но вос­при­нимать как удос­товере­ние лич­ности, которое под­писано про­вай­дером OpenID. Что­бы его получить, кли­ент дол­жен быть аутен­тифици­рован.

Осо­бен­ности identity-токена:

Identity-токен может быть упа­кован в прос­той объ­ект JSON:

{
"sub" : "alice",
"iss" : "https://openid.c2id.com",
"aud" : "client-12345",
"nonce" : "n-0S6_WzA2Mj",
"auth_time" : 1311280969,
"acr" : "https://load.c2id.com/high",
"iat" : 1311280970,
"exp" : 1311281970
}

Этот объ­ект зашит в JWT, который закоди­рован в Base64.

eyJhbGciOiJSUzI1NiIsImtpZCI6IjFlOWdkazcifQ.ewogImlzcyI6ICJodHRw
Oi8vc2VydmVyLmV4YW1wbGUuY29tIiwKICJzdWIiOiAiMjQ4Mjg5NzYxMDAxIiw
KICJhdWQiOiAiczZCaGRSa3F0MyIsCiAibm9uY2UiOiAibi0wUzZfV3pBMk1qIi
wKICJleHAiOiAxMzExMjgxOTcwLAogImlhdCI6IDEzMTEyODA5NzAKfQ.ggW8hZ
1EuVLuxNuuIJKX_V8a_OMXzR0EHR9R6jgdqrOOF4daGU96Sr_P6qJp6IcmD3HP9
9Obi1PRs-cwh3LO-p146waJ8IhehcwL7F09JdijmBqkvPeB2T9CJNqeGpe-gccM
g4vfKjkM8FcGvnzZUN4_KSP0aAp1tOJ1zZwgjxqGByKHiOtX7TpdQyHE5lcMiKP
XfEIQILVq0pc_E2DzL7emopWoaoZTF_m0_N0YzFC6g6EJbOEoRoSK5hoDalrcvR
YLSrQAZZKflyuVCyixEoV9GfNQC3_osjzw2PAithfubEEBLuVVk4XUVrWOLrLl0
nx7RkKU8NXNHq-rvKMzqg

Под­робнее о струк­туре дан­ных JWT и ее кодиров­ке мож­но про­читать в RFC 7519, а об ата­ках на JWT — в мо­ей пре­дыду­щей статье.

OpenID Connect Discovery

OpenID Connect Discovery (он же WebFinger) — это механизм, который поз­воля­ет узнать всю необ­ходимую информа­цию о про­вай­дере OpenID, что дает воз­можность интегри­ровать с ним целевое при­ложе­ние, или Relying Party, как оно называ­ется в OpenID.

Механизм WebFinger
Ме­ханизм WebFinger

OpenID Connect Discovery пре­дос­тавля­ет метадан­ные кон­фигура­ции про­вай­дера, вклю­чая:

Рас­полага­ется обыч­но по такому адре­су:

https://server.com/.well-known/openid-configuration

И воз­вра­щает JSON со всей необ­ходимой информа­цией:

{
"issuer": "https://example.com/",
"authorization_endpoint": "https://example.com/authorize",
"token_endpoint": "https://example.com/token",
"userinfo_endpoint": "https://example.com/userinfo",
"jwks_uri": "https://example.com/.well-known/jwks.json",
"scopes_supported": [
"pets_read",
"pets_write",
"admin"
],
"response_types_supported": [
"code",
"id_token",
"token id_token"
],
"token_endpoint_auth_methods_supported": [
"client_secret_basic"
],
...
}

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

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

Незащищенная динамическая регистрация клиента

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

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

Ес­ли под­держи­вает­ся динами­чес­кая регис­тра­ция кли­ента, про­веря­ющая сто­рона может зарегис­три­ровать­ся, отпра­вив спе­циаль­но сфор­мирован­ный POST-зап­рос на выделен­ный registration_endpoint, нап­ример /registration. Путь может отли­чать­ся, и обыч­но он ука­зыва­ется в фай­ле с нас­трой­ками или в докумен­тации.

Взаимодействие между RP и OpenID-провайдером
Вза­имо­дей­ствие меж­ду RP и OpenID-про­вай­дером

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

POST /openid/register HTTP/1.1
Content-Type: application/json
Accept: application/json
Host: oauth-authorization-server.com
Authorization: Bearer ab12cd34ef56gh89
{
"application_type": "web",
"redirect_uris": [
"https://client-app.com/callback",
"https://client-app.com/callback2"
],
"client_name": "My Application",
"logo_uri": "https://client-app.com/logo.png",
"token_endpoint_auth_method": "client_secret_basic",
"jwks_uri": "https://client-app.com/my_public_keys.jwks",
"userinfo_encrypted_response_alg": "RSA1_5",
"userinfo_encrypted_response_enc": "A128CBC-HS256",
}

Про­вай­дер OpenID дол­жен пот­ребовать от про­веря­ющей сто­роны прой­ти аутен­тифика­цию. В при­мере выше для это­го исполь­зует­ся bearer-токен ab12cd34ef56gh89. Одна­ко некото­рые про­вай­деры раз­реша­ют динами­чес­кую регис­тра­цию кли­ентов без какой‑либо аутен­тифика­ции, что поз­воля­ет зло­умыш­ленни­ку регис­три­ровать свое вре­донос­ное при­ложе­ние.

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

Лаба: SSRF via OpenID dynamic client registration

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

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

Ло­гиним­ся, как мы делали это и рань­ше. Здесь сто­ит упо­мянуть инте­рес­ный факт, на который ты мог обра­тить вни­мание при дол­жном изу­чении деталей: во всех лабора­тори­ях, которые мы решали до это­го, исполь­зовал­ся OpenID.

Успешно вошли в аккаунт
Ус­пешно вош­ли в акка­унт

Пос­коль­ку и здесь исполь­зует­ся OpenID, а лабора­тория пос­вящена поис­ку уяз­вимос­тей с помощью механиз­ма Discovery, про­веря­ем все­воз­можные дирек­тории на наличие кон­фигов. Спус­тя некото­рое вре­мя находим такую пап­ку у про­вай­дера OpenID:

http://oauth-0a67000b04d8701081b36eab026900ea.oauth-server.net/.well-known/openid-configuration
Конфигурация WebFinger
Кон­фигура­ция WebFinger

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

Эндпоинт для регистрации
Эн­дпо­инт для регис­тра­ции

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

Попытка отправить запрос
По­пыт­ка отпра­вить зап­рос

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

Пе­рехо­дим в Burp Suite и меня­ем метод с GET на POST. Вмес­то того что­бы перепи­сывать руками, мож­но прос­то клик­нуть пра­вой кноп­кой мыши и выб­рать Change request method.

Как быстро поменять тип запроса
Как быс­тро поменять тип зап­роса

От­прав­ляем зап­рос и встре­чаем новую ошиб­ку. Сер­вер ожи­дает от нас Content-Type: application/json вмес­то application/x-www-form-urlencoded, который мы отпра­вили.

Неправильный тип контента
Неп­равиль­ный тип кон­тента

Это JSON API. Меня­ем Accept на application/json. И Content-Type, что­бы ска­зать сер­веру о том, что мы отпра­вим ему объ­ект JSON. Пос­коль­ку пока что мы прос­то тес­тиру­ем этот эндпо­инт, что­бы изба­вить­ся от оши­бок, дос­таточ­но будет не ука­зывать парамет­ры и отпра­вить зап­рос пус­тым.

Не хватает параметра redirect_uri
Не хва­тает парамет­ра redirect_uri

Встре­чаем ошиб­ку invalid_redirect_uri. Как ты пом­нишь, мы дол­жны ука­зать допус­тимые урлы, на которые сер­вер OpenID дол­жен уметь перенап­равлять поль­зовате­ля. На один параметр redirect_uri сер­вер выда­ет ошиб­ку, поэто­му отправ­ляем спи­сок из того же самого тес­тового урла с помощью redirect_uris.

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

Успешно зарегистрировали приложение
Ус­пешно зарегис­три­рова­ли при­ложе­ние

На сам webhook.site мож­но не обра­щать вни­мания, я пыта­юсь ука­зывать его вез­де. Но в этот раз ожи­даемо отстук, конеч­но же, не при­шел на этот URL, поэто­му нуж­но искать дру­гие парамет­ры, которые помог­ли бы нам про­экс­плу­ати­ровать SSRF.

От­прав­ляем­ся на стра­ницу Consent Screen (что­бы най­ти уяз­вимость, мне пот­ребова­лось вре­мя, поэто­му тут я решил перей­ти сра­зу к сути).

В коде при­меча­телен логотип при­ложе­ния, в которое мы вхо­дим.

Логотип на странице входа
Ло­готип на стра­нице вхо­да

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

Откуда загружается логотип
От­куда заг­ружа­ется логотип

Ло­го зап­рашива­ется через спе­циаль­ный ID и, что самое глав­ное, хра­нит­ся на сер­вере про­вай­дера OpenID. Это стран­но, ведь если кли­ент­ское при­ложе­ние регис­три­рова­лось с собс­твен­ным URL, то тут дол­жен был бы ока­зать­ся он, а не ссыл­ка на про­вай­дер.

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

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

Запрос на получение логотипа
Зап­рос на получе­ние логоти­па

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

logo_uri
OPTIONAL. URL that references a logo for the Client application. If present, the server SHOULD display this image to the End-User during approval. The value of this field MUST point to a valid image file.

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

http://169.254.169.254/latest/meta-data/iam/security-credentials/admin/
Прокидываем пейлоад для SSRF
Про­киды­ваем пей­лоад для SSRF

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

Об­раща­емся к кар­тинке:

GET /client/aw5wi0v3hxmbrf60b0sdg/logo HTTP/2
Host: [oauth-0a94003704d0a6a7c9162297020a00c6.oauth-server.net](http://oauth-0a94003704d0a6a7c9162297020a00c6.oauth-server.net/)

Та часть, которая находит­ся меж­ду /client/ и /logo, — это ID при­ложе­ния. Поэто­му, что­бы уви­деть наше лого, в качес­тве которо­го мы ука­зали внут­ренний сер­вер, нуж­но заменить этот ID иден­тифика­тором того при­ложе­ния, которое мы бук­валь­но толь­ко что зарегис­три­рова­ли.

Успешно эксплуатировали SSRF и получили данные админа
Ус­пешно экс­плу­ати­рова­ли SSRF и получи­ли дан­ные адми­на

Вот мы и получи­ли токен адми­нис­тра­тора. Про­вай­дер OpenID попытал­ся заг­рузить нашу кар­тинку, отпра­вил зап­рос в свою внут­реннюю сеть (на тот URL, который мы ука­зали) и вер­нул содер­жимое пос­ле того, как мы зап­росили его.

Те­перь оста­ется толь­ко сдать его и при­нять поз­драв­ления. Мы всё прош­ли!

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

Чек-лист проверок

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

Этот драфт называ­ется OAuth 2.0 Security Best Current Practice, он был опуб­ликован 3 июня 2024 года и содер­жит меры по пос­тро­ению защиты при исполь­зовании это­го про­токо­ла.

Чек-лист багов, которые можно найти в OAuth
Чек‑лист багов, которые мож­но най­ти в OAuth

Здесь есть мно­го момен­тов, которые мы не рас­смот­рели, поэто­му советую озна­комить­ся с докумен­том OAuth 2.0 Security Best Current Practice.

Риски

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

Проверки

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

Автоматизация

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

Расширение для Burp, которое поможет искать баги
Рас­ширение для Burp, которое поможет искать баги

OAUTHScan выпол­няет про­вер­ки безопас­ности OAuth и OpenID на осно­ве информа­ции, которая опи­сана в спе­цифи­каци­ях и стать­ях, ука­зан­ных ниже:

Вот непол­ный спи­сок про­верок, которые выпол­няет рас­ширение:

Что­бы уста­новить OAUTHScan, надо кло­ниро­вать ре­пози­торий:

git clone https://github.com/PortSwigger/oauth-scan.git

Те­перь перехо­дим в пап­ку oauth-scan и собира­ем про­ект с помощью gradlew build fatJar.

За­тем нуж­но перей­ти в Burp → Extensions, нажать на Add и выб­рать соб­ранный JAR.

Как загрузить расширение в Burp
Как заг­рузить рас­ширение в Burp

Кро­ме того, его мож­но най­ти на вклад­ке BApp Store.

Как установить расширение из магазина
Как уста­новить рас­ширение из магази­на

Уч­ти, что для при­мене­ния пла­гина нуж­на куп­ленная вер­сия Burp, в которой работа­ет Scanner, а не бес­плат­ная Community Edition без него.

Как защититься

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

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

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

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

Сервис-провайдерам

Ес­ли ты пре­дос­тавля­ешь авто­риза­цию OAuth или OpenID, вот нес­коль­ко полез­ных советов, которые защитят тво­их поль­зовате­лей:

Клиентским приложениям

А вот советы на слу­чай, если ты пишешь кли­ент­ское при­ложе­ние:

Выводы

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

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

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