В этой статье мы мак­сималь­но глу­боко изу­чим тех­нологию JSON Web Tokens (JWT): где она исполь­зует­ся, в чем ее плю­сы и минусы и какие опас­ности она может таить для все­го веб‑при­ложе­ния, если прог­раммист видит ее впер­вые. Мы так­же рас­смот­рим типич­ные уяз­вимос­ти JWT, научим­ся их экс­плу­ати­ровать и исправ­лять.

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

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

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

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

info

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

Высокоуровневая схема аутентификации
Вы­соко­уров­невая схе­ма аутен­тифика­ции

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

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

info

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

Высокоуровневая схема авторизации
Вы­соко­уров­невая схе­ма авто­риза­ции

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

Куки и токены

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

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

Куки

Взаимодействие браузера и сервера с куками
Вза­имо­дей­ствие бра­узе­ра и сер­вера с куками

HTTP Cookie — это неболь­шие записи, которы­ми обме­нива­ются кли­ент (твой бра­узер) и сер­вер. Вся­кий раз при обра­щении к соот­ветс­тву­юще­му сай­ту эти дан­ные пересы­лают­ся сер­веру в сос­таве HTTP-зап­роса.

Ку­ки при­дума­ны дав­но, в начале девянос­тых годов. В июне 1994 года Лу Мон­тулли, сот­рудни­ку Netscape Communications, приш­ла идея исполь­зовать их при веб‑соеди­нении.

Ку­ки исполь­зуют­ся в веб‑при­ложе­ниях:

Про­цесс исполь­зования кук выг­лядит сле­дующим обра­зом:

  1. Бра­узер отправ­ляет при­ложе­нию зап­рос с логином и паролем.
  2. При­ложе­ние про­веря­ет логин и пароль поль­зовате­ля. Если они сов­пада­ют с теми, что хра­нят­ся в базе (вмес­то паролей срав­нива­ются их хеши), то при­ложе­ние фор­миру­ет сес­сию, генери­рует сес­сион­ную куку и отправ­ляет ее бра­узе­ру поль­зовате­ля.
  3. Бра­узер сох­раня­ет куку и отправ­ляет ее вмес­те с каж­дым зап­росом в при­ложе­ние.
  4. При­ложе­ние получа­ет куку, рас­шифро­выва­ет ее и, если она валид­ная, выпол­няет дей­ствие от име­ни авто­ризо­ван­ного поль­зовате­ля.

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

info

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

Для прис­воения куки поль­зовате­лю сер­вер отправ­ляет такой заголо­вок:

Set-Cookie: <имя cookie>=<значение cookie>

Бра­узер в свою оче­редь — такой:

Cookie: <имя cookie>=<значение cookie>

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

Токены

В сов­ремен­ном вебе исполь­зуют­ся не толь­ко куки, но и токены. Это аль­тер­натив­ный и более сов­ремен­ный механизм, который име­ет свои плю­сы и минусы по срав­нению с при­выч­ными «печень­ками».

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

OAuth — не единс­твен­ные токены, сущес­тву­ет мно­го дру­гих фор­матов:

Что такое JWT

Три основополагающие части JWT
Три осно­вопо­лага­ющие час­ти JWT

Аб­бре­виату­ра JWT рас­шифро­выва­ется как JSON Web Token. Стан­дарт RFC 7519 опи­сыва­ет отправ­ку крип­тогра­фичес­ки под­писан­ных JSON дан­ных (зна­чений key-value) меж­ду сис­темами. Теоре­тичес­ки стан­дарт RFC 7519 допус­кает отправ­ку любых дан­ных, но в сов­ремен­ном вебе чаще исполь­зует­ся, что­бы переда­вать информа­цию о поль­зовате­лях для аутен­тифика­ции, обра­бот­ки сеан­сов и кон­тро­ля дос­тупа.

info

JWT (JSON Web Token) — это спе­циаль­ный фор­мат токена, который поз­воля­ет безопас­но переда­вать дан­ные меж­ду кли­ентом и сер­вером. В качес­тве кли­ента может выс­тупать бра­узер поль­зовате­ля или мобиль­ное при­ложе­ние, сер­вера — вир­туаль­ная машина или выделен­ный компь­ютер с запущен­ным веб‑при­ложе­нием.

JWT-токены были при­дума­ны гораз­до поз­же, чем куки. В 2011 году была сфор­мирова­на груп­па JOSE (JSON Object Signing and Encryption group), приз­ванная стан­дарти­зиро­вать механизм защиты целос­тнос­ти, шиф­рования, а так­же фор­мат клю­чей и алго­рит­мов иден­тифика­ции для обес­печения сов­мести­мос­ти служб безопас­ности, исполь­зующих фор­мат JSON. К 2013 году в откры­том дос­тупе появи­лись неофи­циаль­ные наб­роски и при­меры исполь­зования идей этой груп­пы. Офи­циаль­но был стан­дарти­зиро­ван груп­пой IETF в мае 2015 года.

То­кены, как и сес­сион­ные куки, соз­дают­ся сер­вером либо в начале вза­имо­дей­ствия поль­зовате­ля с сай­том или при­ложе­нием, либо пос­ле аутен­тифика­ции поль­зовате­ля в при­ложе­нии. Затем они отправ­ляют­ся поль­зовате­лю как часть отве­та сер­вера — либо в HTTP-заголов­ке Set-Cookie, либо в заголов­ке Bearer. От того, где будет записан токен, зависит, как стро­ится даль­нейшая модель безопас­ности.

Взаимодействие браузера и сервера с JWT
Вза­имо­дей­ствие бра­узе­ра и сер­вера с JWT

Аутен­тифика­ция и авто­риза­ция с исполь­зовани­ем JWT-токена устро­ена сле­дующим обра­зом:

  1. Бра­узер отправ­ляет зап­рос при­ложе­нию с логином и паролем.
  2. При­ложе­ние про­веря­ет логин и пароль и, если они вер­ны, генери­рует JWT-токен, который затем отправ­ляет бра­узе­ру. При генера­ции JWT-токена веб‑при­ложе­ние ста­вит под­пись сек­ретным клю­чом, который хра­нит­ся толь­ко в при­ложе­нии. Может исполь­зовать­ся как сим­метрич­ное шиф­рование, так и асим­метрич­ное.
  3. Бра­узер сох­раня­ет JWT-токен и отправ­ляет его вмес­те с каж­дым зап­росом в при­ложе­ние.
  4. При­ложе­ние про­веря­ет JWT-токен и, если он валид­ный (то есть он пра­виль­но сфор­мирован и дан­ные не были изме­нены), выпол­няет дей­ствие от име­ни авто­ризо­ван­ного поль­зовате­ля.

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

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

При под­писи токена исполь­зует­ся шиф­рование. С помощью под­писи веб‑при­ложе­ние про­веря­ет, что токен дей­стви­тель­но был сге­нери­рован им. Алго­рит­мы шиф­рования могут быть раз­ными, нап­ример HS256 — HMAC с SHA-256.

Формат JWT

JWT-токен сос­тоит из трех час­тей, которые раз­делены точ­кой:

Фор­мат токена:

header.payload.signature

Каж­дая из этих час­тей обыч­но кодиру­ется в Base64 для переда­чи без пов­режде­ний по сети. Помимо это­го, исполь­зует­ся облегчен­ный вари­ант URL-кодиро­вания. Свой­ствен­ный Base64 знак равенс­тва усе­кает­ся. Плюс заменя­ется минусом, а слеш (/) — под­черки­вани­ем, что­бы не воз­никло кол­лизий.

При­мер токена:

eyJraWQiOiI5MTM2ZGRiMy1jYjBhLTRhMTktYTA3ZS1lYWRmNWE0NGM4YjUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTY0ODAzNzE2NCwibmFtZSI6IkNhcmxvcyBNb250b3lhIiwic3ViIjoiY2FybG9zIiwicm9sZSI6ImJsb2dfYXV0aG9yIiwiZW1haWwiOiJjYXJsb3NAY2FybG9zLW1vbnRveWEubmV0IiwiaWF0IjoxNTE2MjM5MDIyfQ.SYZBPIBg2CRjXAJ8vCER0LA_ENjII1JakvNQoP-Hw6GG1zfl4JyngsZReIfqRvIAEi5L4HV0q7_9qGhQZvy9ZdxEJbwTxRs_6Lb-fZTDpW6lKYNdMyjw45_alSCZ1fypsMWz_2mTpQzil0lOtps5Ei_z7mM7M8gCwe_AGpI53JxduQOaB5HkT5gVrv9cKu9CsW5MS6ZbqYXpGyOG5ehoxqm8DL5tFYaW3lB50ELxi0KsuTKEbD0t5BCl0aCR2MBJWAbN-xeLwEenaqBiwPVvKixYleeDQiBEIylFdNNIMviKRgXiYuAvMziVPbwSgkZVHeEdF5MQP1Oe2Spac-6IfA

Header

За­голо­вок обыч­но сос­тоит из JSON-объ­екта с дву­мя свой­ства­ми:

Да­лее этот JSON-объ­ект хеширу­ется с помощью Base64URL-кодиро­вания, что­бы пред­ста­вить его в виде ком­пак­тной стро­ки.

Та­ким обра­зом, в нашем при­мере заголо­вок JWT-токена име­ет сле­дующее зна­чение:

{
"kid": "9136ddb3-cb0a-4a19-a07e-eadf5a44c8b5",
"alg": "RS256"
}

Так­же напом­ню, что это не шиф­рование, поэто­му его мож­но рас­кодиро­вать из кон­соли Linux:

$ echo eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 | base64 -d
{"typ":"JWT","alg":"HS256"}

Или из кон­соли JavaScript в бра­узе­ре:

>> atob("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9")
"{"typ":"JWT","alg":"HS256"}"

Ва­риант рас­шифров­ки через PowerShell:

PS C:\> [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String("eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9"))
{"typ":"JWT","alg":"HS256"}

Мож­но даже из CMD, но чуть слож­нее:

C:\>echo eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 > file.txt && certutil -decode -f file.txt outfile.txt && type outfile.txt
Input Length = 40
Output Length = 27
CertUtil: -decode command completed successfully.
{"typ":"JWT","alg":"HS256"}

Payload

Вто­рая часть токена — это полез­ная наг­рузка в виде JSON-объ­екта. Она содер­жит дан­ные об авто­ризо­ван­ном поль­зовате­ле. Зна­чение этой час­ти JWT-токена раз­ное в раз­ных веб‑при­ложе­ниях. Мы можем записать здесь любые пуб­личные дан­ные, которые могут быть полез­ны при авто­риза­ции.

Как и заголо­вок JWT-токена, полез­ная наг­рузка хеширу­ется с помощью Base64URL-кодиро­вания для пред­став­ления в виде ком­пак­тной стро­ки.

В нашем при­мере полез­ная наг­рузка JWT-токена име­ет сле­дующее зна­чение:

{
"iss": "portswigger",
"exp": 1648037164,
"name": "Carlos Montoya",
"sub": "carlos",
"role": "blog_author",
"email": "carlos@carlos-montoya.net",
"iat": 1516239022
}

Наз­вания некото­рых полей могут показать­ся непонят­ными с пер­вого взгля­да. Здесь говорит­ся о том, кто выписал токен (iss), на кого он выписан (sub и name) и каков его срок жиз­ни (exp), по про­шес­твии которо­го сер­вер дол­жен счи­тать его невалид­ным. Эти дан­ные может изме­нить любой человек, затем закоди­ровать в Base64 и вста­вить на мес­то изна­чаль­ных. Поэто­му вся безопас­ность зависит нап­рямую от крип­тогра­фичес­кой под­писи.

www

При сос­тавле­нии полей полез­ной наг­рузки раз­работ­чики ста­рают­ся учи­тывать име­на из докумен­тации IANA (Internet Assigned Numbers Authority), что­бы избе­жать кон­флик­тов имен с общепри­няты­ми нор­мами.

Ос­новная при­чина, почему наз­вания полей в полез­ной наг­рузке JWT-токена пишут­ся сок­ращен­но, — это умень­шение раз­мера токена пос­ле шиф­рования.

Signature

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

Вот при­мер для HS256:

HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)

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

www

jwt.io — онлайн‑дебаг­гер, который авто­мати­зиру­ет декоди­рова­ние, про­вер­ку и генера­цию JWT-токенов. Его мож­но исполь­зовать для собс­твен­ных экспе­римен­тов.

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

JWT vs JWS vs JWE

Ис­ходная спе­цифи­кация JWT очень огра­ничен­на. Она опре­деля­ет толь­ко фор­мат пред­став­ления информа­ции в виде объ­екта JSON, который может быть передан меж­ду дву­мя сто­рона­ми. На прак­тике JWT поч­ти не исполь­зует­ся как отдель­ная сущ­ность.

Спе­цифи­кация JWT была рас­ширена спе­цифи­каци­ями JSON Web Signature (JWS) — RFC 7515 и JSON Web Encryption (JWE) — RFC 7516, которые опре­деля­ют кон­крет­ные спо­собы реали­зации JWT.

Дру­гими сло­вами, ког­да мы говорим про JWT-токены в кон­тек­сте веба, мы на самом деле име­ем в виду либо JWS-токены, либо токены JWE. Токены JWE и JWS очень похожи, пер­вые зашиф­рованы, а вто­рые закоди­рова­ны.

Атаки на JWT-токены

Зачем атаковать

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

Последствия атак

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

Почему токены уязвимы

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

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

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

Burp Suite и JWT

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

info

PortSwigger WebSecurity Academy — бес­плат­ная ака­демия раз­работ­чиков Burp Suite (популяр­ного инс­тру­мен­та, исполь­зуемо­го пен­тесте­рами) для обу­чения безопас­ности.

Кро­ме того, качай Burp Suite и ставь пла­гин JWT Editor, который поможет нам под­писывать JWT-токены и реали­зовать некото­рые ата­ки при про­хож­дении.

Неправильная проверка подписей

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

Вот при­мер из JavaScript. Для работы с JWT-токена­ми в Node.js сущес­тву­ет биб­лиоте­ка jsonwebtoken. Прог­раммист может выз­вать:

Их лег­ко спу­тать и передать токен в метод decode() вмес­то пра­виль­ного verify(). В таком слу­чае при­ложе­ние вооб­ще не будет про­верять под­пись и будет счи­тать ее валид­ной. При этом раз­работ­чик заметит такую недора­бот­ку не сра­зу, осо­бен­но если на сер­вере не ведут­ся логи.

Лаба: JWT authentication bypass via unverified signature

Эта лабора­тория поз­волит нам про­экс­плу­ати­ровать такую уяз­вимость на прак­тике. Как ска­зано в задании к ней, нам нуж­но изме­нить сеан­совый токен и получить дос­туп к панели адми­нис­тра­тора по адре­су /admin. Из‑за недос­татков реали­зации сер­вер не про­веря­ет под­пись получа­емых JWT. Мы в роли зло­умыш­ленни­ка дол­жны взло­мать при­ложе­ние.

От­кро­ем лабора­торию. Перед нами — блог с лич­ным кабине­том.

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

Нам нуж­но аутен­тифици­ровать­ся, что­бы получить токен. Перей­дем в My account.

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

Тес­товые дан­ные были ука­заны в опи­сании к заданию:

Вве­дем их, наж­мем Log in и взгля­нем на зап­рос в Burp Suite.

Перехваченный запрос на вход
Пе­рех­вачен­ный зап­рос на вход

В теле зап­роса отпра­вились сле­дующие дан­ные:

В отве­те при­шел JWT-токен. Это говорит о том, что мы успешно вош­ли на сайт.

eyJraWQiOiJhYzhjMWM5Yy0yMWU5LTRkODYtODlkYi02M2M2NWJiYTUxNTciLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTcyMDcyODY2Nywic3ViIjoid2llbmVyIn0.BMsKR6H2eFhSahX97sPW9WoXpSd5FZRDNC12am1_K8vBwEuc_PdpnOjJIHtYY1q-dOi93c5A90xhUaUWi8z3QwgU6zTsVPbgJK6q7_6EU8KsSSNF_saayykHxNsN4YwZlwl3qwaB3TjyriEPtFKHB-fFS04cJQURLSAGCx39Y5FXzK4BUa3cLdN5-TV3GwDYGCd29oTAXqfGmvkuJ8DxRrjrWLlEYa1_IzzLY7XTMJAdfIeiR7zTwlkixndRUGO1-hZEHq5Cjojg-3MJ0RuieH42qJQKqgFVzSWslEM-3jtTV3rCHVK_nwM0llPOaOe0VUgYoMt-yq57rx8_Ucx7Fg

В таком виде про­читать его мы не можем. Перей­дем на вклад­ку JSON Web Token. Она появи­лась бла­года­ря рас­ширению JWT Editor, которое мы уста­нови­ли ранее.

Расширение JSON Web Token умеет парсить JWT-токены
Рас­ширение JSON Web Token уме­ет пар­сить JWT-токены

На этой вклад­ке мож­но уви­деть исходный токен JWT, а так­же декоди­рован­ный из Base64 Header и Payload, который рас­полага­ется под ним. Еще ниже — под­пись.

За­голо­вок:

По­лез­ная наг­рузка:

Нам необ­ходимо зай­ти от име­ни адми­нис­тра­тора. Это зна­чит, что нуж­но изме­нить subject.

Най­дем GET-зап­рос в лич­ный кабинет — это /my-account?id=wiener. Отпра­вим его в Repeater и выделим ту часть JWT-токена, в которой находит­ся полез­ная наг­рузка.

Полезная нагрузка JWT-токена
По­лез­ная наг­рузка JWT-токена

В пра­вом углу в окне Inspector мож­но уви­деть декоди­рован­ные дан­ные. Burp «из короб­ки» уме­ет декоди­ровать Base64, но в даль­нейшем мы будем исполь­зовать JWT Editor, который поз­воля­ет кодиро­вать дан­ные обратно и под­писывать. Заменим subject с wiener логином адми­нис­тра­тора, то есть administrator. И наж­мем Apply changes.

Как должен выглядеть токен после редактирования
Как дол­жен выг­лядеть токен пос­ле редак­тирова­ния

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

Убе­рем из пути ?id=wiener и отпра­вим зап­рос. В отве­те появи­лась ссыл­ка на админ­скую панель. Эта ссыл­ка отоб­ража­ется толь­ко для адми­нис­тра­тора, сле­дова­тель­но, мы заш­ли как админ, а не как wiener.

Ссылка на панель в личном кабинете администратора
Ссыл­ка на панель в лич­ном кабине­те адми­нис­тра­тора

Что­бы убе­дить­ся в этом, клик­нем пра­вой кноп­кой мыши, выберем Request in browser → In original session.

Контекстное меню, которое позволяет увидеть отрендеренный ответ
Кон­текс­тное меню, которое поз­воля­ет уви­деть отренде­рен­ный ответ

Ско­пиру­ем зна­чение.

Ссылка, по которой нужно перейти, чтобы увидеть ответ в браузере
Ссыл­ка, по которой нуж­но перей­ти, что­бы уви­деть ответ в бра­узе­ре

И вста­вим его в нашем бра­узе­ре.

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

Your username is administrator

Мы дей­стви­тель­но ста­ли адми­нис­тра­тором.

Те­перь нам нуж­но зай­ти в саму админ­скую панель. Мы мог­ли бы вновь изме­нить Path в самом Burp Suite и пов­торно открыть новую ссыл­ку, но это неудоб­но. Гораз­до про­ще заменить токен в самом бра­узе­ре, что­бы выпол­нять дей­ствия от нашего нового поль­зовате­ля.

Кли­каем пра­вой кноп­кой и выбира­ем Inspect, что­бы открыть меню раз­работ­чика.

Инструменты разработчика
Инс­тру­мен­ты раз­работ­чика

За­тем выберем Storage — это хра­нили­ще бра­узе­ра, где лежит наш токен.

Storage — вкладка, на которой отображается браузерное хранилище
Storage — вклад­ка, на которой отоб­ража­ется бра­узер­ное хра­нили­ще

Он находит­ся в Cookies, кука единс­твен­ная, и она называ­ется session.

Кука пользователя с записанным в нее токеном JWT
Ку­ка поль­зовате­ля с записан­ным в нее токеном JWT

Сей­час в ней лежит ста­рый токен JWT поль­зовате­ля wiener. Заменим его тем, что мы соз­дали. И наж­мем Admin panel. Если ты все сде­лал пра­виль­но, дол­жна открыть­ся админка.

Личный кабинет после замены куки и обновления страницы
Лич­ный кабинет пос­ле замены куки и обновле­ния стра­ницы

По заданию нам надо уда­лить поль­зовате­ля carlos, что­бы лабора­тория счи­талась выпол­ненной. Нажима­ем Delete нап­ротив него и получа­ем поз­драв­ления.

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

Мы успешно прош­ли пер­вую лабора­торию!

Алгоритм подписи None

Пред­ста­вим, что на этот раз сер­вер кор­рек­тно про­веря­ет под­пись. Это зна­чит, что если мы изме­ним тело, то ста­рая под­пись ста­нет невалид­ной и сер­вер не при­мет токен.

Еще раз взгля­нем на заголо­вок из пер­вой лабора­тор­ной работы.

{
"kid": "a2b8c974-3de8-4bf1-8abc-b8753873b25c",
"alg": "RS256"
}

Па­раметр alg ука­зыва­ет, какой алго­ритм необ­ходимо исполь­зовать сер­веру при про­вер­ке под­писи. В дан­ном слу­чае это RS256, но какие алго­рит­мы еще сущес­тву­ют?

По­мимо RS256, есть мно­жес­тво вари­антов: HS256 (384/512), ES256 (384/512) и none. Пос­ледний осо­бен­но при­меча­телен, пос­коль­ку говорит сер­веру, что под­писи быть не дол­жно и дан­ные счи­тают­ся валид­ными «как есть».

{
"kid": "a2b8c974-3de8-4bf1-8abc-b8753873b25c",
"alg": "none"
}

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

Но если и в production-сре­де сер­вер безого­вороч­но доверя­ет ука­зан­ному поль­зовате­лем зна­чению alg, которое хакер может изме­нить на none, это зна­чит, что его очень и очень прос­то ском­про­мети­ровать.

info

Да­же если под­пись не исполь­зует­ся, Payload дол­жен закан­чивать­ся точ­кой.

Лаба: JWT authentication bypass via flawed signature verification

От­кро­ем лабора­торию.

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

Аутен­тифици­руем­ся от име­ни выдан­ной учет­ки wiener.

Личный кабинет пользователя wiener
Лич­ный кабинет поль­зовате­ля wiener

Най­дем зап­рос на откры­тие лич­ного кабине­та и отпра­вим его в Repeater.

Выделяем запрос в History и отправляем в Repeater
Вы­деля­ем зап­рос в History и отправ­ляем в Repeater

Сде­лаем то же, что делали в прош­лой лабора­тории:

Как должен выглядеть JWT-токен после редактирования
Как дол­жен выг­лядеть JWT-токен пос­ле редак­тирова­ния

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

Сервер не принял наш токен, поскольку подпись невалидна
Сер­вер не при­нял наш токен, пос­коль­ку под­пись невалид­на

Вы­делим заголо­вок. В нем ука­зан алго­ритм HS256.

Алгоритм HS256 в заголовке токена
Ал­горитм HS256 в заголов­ке токена

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

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

HS256 нужно заменить none
HS256 нуж­но заменить none

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

Ссылка на панель администратора
Ссыл­ка на панель адми­нис­тра­тора

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

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

Сра­бота­ло! Заходим в админ­скую панель и уда­ляем поль­зовате­ля carlos.

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

Мы успешно прош­ли вто­рую лабора­торию.

Брутфорс секретов

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

Как ты понима­ешь, прис­тавка в исполь­зуемом алго­рит­ме под­писи озна­чает, что исполь­зует­ся HMAC (алго­ритм аутен­тифика­ции сооб­щений) с при­мене­нием хеш‑фун­кции SHA-256.

Как вычис­ляет­ся HMAC-SHA256 — не сек­рет. Выг­лядит это сле­дующим обра­зом:

HMAC_SHA256(key, message) = SHA256((key opad) SHA256((key ipad) message))

Здесь

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

info

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

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

Лаба: JWT authentication bypass via weak signing key

Итак, вся безопас­ность JWT с алго­рит­мом HS256 дер­жится на том, нас­коль­ко надеж­ный был выб­ран сим­метрич­ный ключ. Давай для начала добудем JWT-токен, что­бы иметь воз­можность его подоб­рать.

От­кро­ем лабора­торию.

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

Зай­дем в лич­ный кабинет под тес­товым акка­унтом и получим JWT-токен. Если открыть вклад­ку JSON Web Token, мож­но убе­дить­ся, что исполь­зует­ся алго­ритм с сим­метрич­ным клю­чом.

JSON Web Token показывает декодированное содержимое JWT-токена
JSON Web Token показы­вает декоди­рован­ное содер­жимое JWT-токена

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

Нам нужен пер­вый вари­ант, то есть ата­ка по сло­варю. В качес­тве сло­варя будем исполь­зовать jwt.secrets.list. Это набор стан­дар­тных сек­ретов, которые исполь­зуют­ся по умол­чанию в раз­ных биб­лиоте­ках.

Ути­лита hashcat поможет взять все извес­тные клю­чи из это­го спис­ка и опре­делить, какой из них исполь­зовал­ся для под­писи нашего JWT-токена, пос­ледова­тель­но под­писывая дан­ные до тех пор, пока мы не получим такую же под­пись, какую нам выдал сер­вер.

Ско­пиру­ем JWT-токен и под­ста­вим его в коман­ду:

hashcat -a 0 -m 16500 <jwt> <wordlist>

Раз­берем ее по поряд­ку:

Ре­зуль­тат выпол­нения — на скрин­шоте ниже.

Результат выполнения hashcat
Ре­зуль­тат выпол­нения hashcat

Спус­тя две секун­ды hashcat закон­чил свою работу. Видим вот такую стро­ку:

Recovered........: 1/1 (100.00%) Digests (total), 1/1 (100.00%) Digests (new)

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

eyJraWQiOiI5YWUwMTFkMi1mNTdkLTQ5ZTYtYWMyYS0xM2FlNmU0MWYzYzciLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTcyMDc3MTM5Niwic3ViIjoid2llbmVyIn0.Zt_AWlB6_9nOdJA8JIEAUTYEukICAqTj8WTP-vyt9go:secret1

Это secret1. Теперь мы можем под­писывать дан­ные тем же клю­чом, что и сер­вер.

Вер­немся в Burp и сме­ним логин на administrator.

Логин wiener нужно сменить на administrator в теле токена JWT
Ло­гин wiener нуж­но сме­нить на administrator в теле токена JWT

Те­перь наш JWT нуж­но под­писать. Перей­дем на вклад­ку JWT Editor, наж­мем на кноп­ку New Symmetric Key, затем Generate. Появив­ший­ся параметр k — это слу­чай­ный ключ, который сге­нери­рова­ло нам рас­ширение. Нам же нужен наш собс­твен­ный, заменим его secret1. Дол­жно получить­ся, как на скрин­шоте (за исклю­чени­ем парамет­ра kid).

Создание симметричного секрета с секретом, который мы набрутили
Соз­дание сим­метрич­ного сек­рета с сек­ретом, который мы наб­рутили

Сох­раня­ем.

Вкладка JWT Editor после сохранения секрета
Вклад­ка JWT Editor пос­ле сох­ранения сек­рета

Те­перь мы можем исполь­зовать этот ключ, что­бы под­писывать JWT как сер­вер. Для это­го воз­вра­щаем­ся в Repeater. Нажима­ем кноп­ку Sign, оставля­ем все как есть и нажима­ем OK.

Процесс подписания JWT-токена
Про­цесс под­писания JWT-токена

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

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

echo -n <HEADER.PAYLOAD> | openssl dgst -sha256 -hmac secret1 -binary | openssl base64 -e -A | sed s/\+/-/ | sed -E s/=+$//

Вмес­то <HEADER.PAYLOAD> встав­ляем наш JWT-токен. Тот, где мы сме­нили wiener на administrator.

В резуль­тате в кон­соли выводит­ся готовая под­пись.

Подписание токена JWT из консоли
Под­писание токена JWT из кон­соли

Встав­ляем ее на мес­то невалид­ной и отправ­ляем зап­рос.

Ссылка на панель администратора
Ссыл­ка на панель адми­нис­тра­тора

Дос­туп к админ­ской панели открыт!

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

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

Мы успешно прош­ли третью лабора­торию.

Инъекции в параметры заголовка

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

Сер­вер под­писыва­ет сво­им при­ват­ным клю­чом (который он никому не переда­ет) токены, а мы рас­шифро­выва­ем их с помощью пуб­лично­го клю­ча и убеж­даем­ся в том, что они были под­писаны под­линным сер­вером.

Схематичное изображение асимметричного шифрования
Схе­матич­ное изоб­ражение асим­метрич­ного шиф­рования

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

Нап­ример, есть проб­лема: отку­да поль­зователь или дру­гое при­ложе­ние, которое будет исполь­зовать токен, возь­мет пуб­личный ключ, что­бы самому убе­дить­ся в том, что JWT-токен под­писал сер­вер, а не кто‑то дру­гой? Есть два вари­анта:

  1. При­ложе­ние отда­ет в отве­те параметр kid (иден­тифика­тор клю­ча Key ID), по которо­му мы можем отпра­вить зап­рос в реестр это­го же при­ложе­ния и получить пуб­личный ключ. Тре­бует выдать дос­туп ко внут­ренне­му реес­тру, а это соз­дает допол­нитель­ные рис­ки.
  2. При­ложе­ние отда­ет JWK — параметр, в который сра­зу записан пуб­личный ключ. На пер­вый взгляд, удоб­нее и про­ще реали­зовать, но тут тоже мож­но допус­тить ошиб­ку.

За­голов­ки JWT-токенов (которые так­же извес­тны как JOSE-заголов­ки) час­то содер­жат ряд дру­гих парамет­ров. Вот какие парамет­ры обыч­но могут быть нам инте­рес­ны:

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

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

Инъекция через параметр jwk

Итак, пред­положим, сер­вер прис­лал нам параметр jwk. Он так­же может не при­сылать его, но по‑преж­нему под­держи­вать — с этим мы стол­кнем­ся даль­ше.

Вот при­мер заголов­ка JWT-токена с парамет­рами jwk:

{
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"typ": "JWT",
"alg": "RS256",
"jwk": {
"kty": "RSA",
"e": "AQAB",
"kid": "ed2Nf8sb-sD6ng0-scs5390g-fFD8sfxG",
"n": "yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9m"
}
}

C kid и alg мы уже зна­комы, вот что озна­чают осталь­ные парамет­ры:

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

Ата­кующий может зло­упот­ребить этим поведе­нием, под­писав модифи­циро­ван­ный JWT с помощью собс­твен­ного зак­рытого клю­ча RSA, а затем вста­вить соот­ветс­тву­ющий откры­тый ключ в заголо­вок jwk.

Лаба: JWT authentication bypass via jwk header injection

На­чина­ем про­хож­дение. Нас сно­ва при­ветс­тву­ет при­выч­ный блог.

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

За­ходим в акка­унт под тес­товой учет­кой и смот­рим на токен. Пер­вое, что бро­сает­ся в гла­за, — это новый алго­ритм. Если во всех пре­дыду­щих задани­ях нас встре­чал HS256, то на этот раз это RS256.

Новый алгоритм RS256 — он предполагает использование асимметричного шифрования
Но­вый алго­ритм RS256 — он пред­полага­ет исполь­зование асим­метрич­ного шиф­рования

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

Пе­рехо­дим в JWT Editor, нажима­ем New RSA Key и генери­руем собс­твен­ный ключ.

Процесс создания ключа RSA на вкладке JWT Editor
Про­цесс соз­дания клю­ча RSA на вклад­ке JWT Editor

На­жима­ем OK. Теперь воз­вра­щаем­ся в Proxy → History, выбира­ем зап­рос в лич­ный кабинет и отправ­ляем его в Repeater. Выделя­ем полез­ную наг­рузку в клю­че JWT и заменя­ем поль­зовате­ля wiener поль­зовате­лем administrator.

В теле токена JWT нужно сменить wiener на administrator
В теле токена JWT нуж­но сме­нить wiener на administrator

Дан­ные замени­ли, теперь дело за под­писью. Откры­ваем JSON Web Token, нажима­ем Attack. Выбира­ем Embedded JWT и выбира­ем наш толь­ко что соз­данный RSA-ключ. Нажима­ем ОK.

Процесс подписания JWT-токена асимметричным ключом
Про­цесс под­писания JWT-токена асим­метрич­ным клю­чом

Как мож­но заметить на кар­тинке ниже, рас­ширение помог­ло вста­вить в Header новый параметр jwk с нашим собс­твен­ным пуб­личным клю­чом. Дан­ные тоже под­писались — нашим при­ват­ным клю­чом, который свя­зан с пуб­личным. Отправ­ляем зап­рос и видим завет­ную над­пись Admin panel.

Ссылка на панель администратора
Ссыл­ка на панель адми­нис­тра­тора

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

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

Ла­бора­тория успешно прой­дена!

Инъекция через параметр jku

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

info

jku (JWK Set URL) — это URL, по которо­му мы про­сим уяз­вимый сер­вер заг­рузить наши пуб­личные клю­чи и исполь­зовать их для про­вер­ки под­писи.

На самом сер­вере дол­жны лежать клю­чи в фор­мате JWK Set. Это объ­ект JSON, сос­тоящий из одно­го парамет­ра keys. В этом парамет­ре может быть записа­но мно­го пуб­личных клю­чей в фор­мате обыч­ного jwk.

Вот при­мер:

{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "75d0ef47-af89-47a9-9061-7c02a610d5ab",
"n": "o-yy1wpYmffgXBxhAUJzHHocCuJolwDqql75ZWuCQ_cb33K2vh9mk6GPM9gNN4Y_qTVX67WhsN3JvaFYw-fhvsWQ"
},
{
"kty": "RSA",
"e": "AQAB",
"kid": "d8fDFo-fS9-faS14a9-ASf99sa-7c1Ad5abA",
"n": "fc3f-yy1wpYmffgXBxhAUJzHql79gNNQ_cb33HocCuJolwDqmk6GPM4Y_qTVX67WhsN3JvaFYw-dfg6DH-asAScw"
}
]
}

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

Для таких клю­чей так­же есть общепри­нятый путь: /.well-known/jwks.json.

info

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

Лаба: JWT authentication bypass via jku header injection

Для это­го задания нам пре­дос­тавили Exploit Server, на котором мы смо­жем раз­местить клю­чи. Давай заранее изме­ним file с /exploit на /.well-known/jwks.json. Клю­чей у нас пока нет, поэто­му перей­дем в Burp Suite, а сюда вер­немся поз­же.

Exploit Server с настроенным путем к файлу
Exploit Server с нас­тро­енным путем к фай­лу

По уже отра­ботан­ному алго­рит­му вой­дем в акка­унт, перей­дем к зап­росу и сме­ним wiener на administrator в теле JWT-токена.

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

К сожале­нию, в JWT Editor не авто­мати­зиро­вана эта ата­ка, так что зай­мем­ся ею вруч­ную. Для это­го в заголо­вок добавим ключ jwk и ука­жем ссыл­ку на наш Exploit Server.

В заголовке JWT-токена нужно указать параметр jwk
В заголов­ке JWT-токена нуж­но ука­зать параметр jwk

У меня получил­ся сле­дующий заголо­вок:

{
"kid": "54ab4e74-d36c-4d67-850f-2dbfd8009e58",
"alg": "RS256",
"jwk": "https://exploit-0ac500c503396ee5810229a401520050.exploit-server.net/.well-known/jwks.json"
}

Под­пишем его. Для это­го наж­мем Sign, выберем пос­ледний пункт с обновле­нием заголов­ка и наж­мем ОK. Ключ RSA у меня оста­ется из прош­лого задания. Ты можешь сге­нери­ровать новый на вклад­ке JWK Editor, если у тебя его не ока­залось.

Процесс подписания токена
Про­цесс под­писания токена

Те­перь нуж­но раз­местить клю­чи на сер­вере. Для это­го перей­дем в JWT Editor и дваж­ды щел­кнем на нашем единс­твен­ном клю­че.

Нужно извлечь атрибуты, необходимые для jwk
Нуж­но извлечь атри­буты, необ­ходимые для jwk

Здесь мно­го атри­бутов, ско­пиру­ем все поле и обер­нем в фор­мат JWK Set. Вста­вим меж­ду квад­ратны­ми скоб­ками ско­пиро­ван­ное зна­чение.

{
"keys": []
}

Вот что у меня получи­лось:

{
"keys": [
{
"p": "3IRsQoVyh0-7hPpptHQ36Tge4cpOwwDxA8iZjXQpOBBQqTf5WpVFJc28UpipC5RoAZ-DDYUJMZRhqpRA1nG8So8CVJEJTGeu1iO3gC21iaK7okzEUWGspLOxgBPit5KtpnqaQtzeNmRPZeMLDaJ7rBf6RydUg-No4nFH_KYYRAM",
"kty": "RSA",
"q": "vS_wzSWysTQGVkxvMev8jIf_RtFW-B5JMyPpkHov0u9fAnGSqkJTqDe2dcgK5jdlArzf6EdbJzqkRZFlLC4i7_LP7Wg71KvWkF7eE2oC7X8F1nD9bbtI-Xeuz3gOtH8rcJdbYdg43eXoCVmR6-WQ95j32sFe7i8eIt0n__4kvK8",
"d": "R74TV7cS9c_SA5x4p13bdJx1-7p45yoxMlF8iCIUfLyZX4hqdVGPWygW11P-p8g0YyXKvLfGUcSRWeHVLQZEYSUT3u8Op734Ycg1f8nn00GZj3ajqK-y1auCbaiZSyjScl7oKTCzL-xcU--drR4KhSA9PQRdAduxCWqc816L-lRmpSuBnZ6c9vSETFkU9GQbkTJNorwKuO8pDe5lOnrt8ULilKfoZ-6BcyMWptsFNXR05afpjtXsNgsl2a136KRLSC2JNyseROipi1f4jK-LoEumTO_0MuvJ7t_f94Xy3LFsn-SL4yS7G99hW8K9GqmcUddibbqcQTqNQ2rDCWIcAw",
"e": "AQAB",
"kid": "45a3ae3f-c28f-42db-80f5-deb3256dd93f",
"qi": "i91AYPzed-J6FojbvsugxbMOH5RBJIQ-4sV1MyqGhiiPByCcRU-bTzWqEl_bJmq_rKefh-yeHvBhmWG7xCj1Zl_MCEUqMvy-B2AYhGXNDlFYQK7fTMyD4ifLEbbpc05Yz8iEVwECuUOHrKPhbTmvCwOgvrqucOS8qg0kdO5g-7E",
"dp": "WCrOCi7G4tj7NajVeKP68tFQb6Buq0MGGigrVMY78MF9LptFpHUIJ5xBjpihBhM5HmUDhfVJ_ru_7O7HmbXxvbG-EcfHevf9jHrNVH9yFOyurq6Y050E5Pk_n-DThegsa-KbKN6cLg0fPbJwaewsHFud4rMT9IOJHPiD-r0B-Lc",
"dq": "LyPk8pk0H2eBNLuy8VOGCFZSy4iaDRUu1Clcp31qsTqoB-nYy8ffJIlNU6fW32pqJvZ9LFmrYuj_yb3i4dFVL0jnepaAYgu3WR3qZBgERr1h7P8WhuMl2dNyoYuezmbpohJ02LqR4OjKmpnQ_GClcKyTBdUtHFhtP-6vauAes00",
"n": "ovcPqdi-LKozZXjExEaZ3wk2BmtTJnqMa4wODS6OzQDJRtzOGabuAeccdH4RWENdkJC3zODQ640WJ85WlOk2lgd0Fx0T4xjk9Q90cfErdQIEWSz3Ensymgej-riF379iFqoyulmYIQoD830Q9VqzhT1Whw2AIgHzfXByVm16eknqUi4xJumrk4G3OXYUTWUI8p4SFGkpzuvLb9iESOFzoZKuVVwPjzJZ8zpbeEMmtu7W9Wi1E4tpKnHz7y4I0Z_0T_iXuFBIM7vnDboKDacahvNZBoAHmsde0CWDIi0GkWsBXpsxvKRjiSli2e2kgl9fDtVFWnCtC6UJwGKQ5vSyDQ"
}
]
}

Убе­рем отсю­да сле­дующие парамет­ры:

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

Ито­говый резуль­тат:

{
"keys": [
{
"kty": "RSA",
"e": "AQAB",
"kid": "45a3ae3f-c28f-42db-80f5-deb3256dd93f",
"n": "ovcPqdi-LKozZXjExEaZ3wk2BmtTJnqMa4wODS6OzQDJRtzOGabuAeccdH4RWENdkJC3zODQ640WJ85WlOk2lgd0Fx0T4xjk9Q90cfErdQIEWSz3Ensymgej-riF379iFqoyulmYIQoD830Q9VqzhT1Whw2AIgHzfXByVm16eknqUi4xJumrk4G3OXYUTWUI8p4SFGkpzuvLb9iESOFzoZKuVVwPjzJZ8zpbeEMmtu7W9Wi1E4tpKnHz7y4I0Z_0T_iXuFBIM7vnDboKDacahvNZBoAHmsde0CWDIi0GkWsBXpsxvKRjiSli2e2kgl9fDtVFWnCtC6UJwGKQ5vSyDQ"
}
]
}

По­мес­тим его на наш сер­вер. Вста­вим в поле Body, а в Head сме­ним Content-Type с text/html; charset=utf-8 на application/json, пос­коль­ку ответ мы отда­ем в фор­мате JSON.

Страница Exploit Server с нашими публичными ключами
Стра­ница Exploit Server с нашими пуб­личны­ми клю­чами

Не забудь нажать Store, что­бы сох­ранить изме­нения.

Вер­немся в Burp и отпра­вим под­писан­ный ранее зап­рос.

Ссылка на панель администратора
Ссыл­ка на панель адми­нис­тра­тора

По­яви­лась ссыл­ка на Admin panel, а это зна­чит, что мы ста­ли адми­нис­тра­тором. Воз­вра­щаем­ся в лич­ный кабинет и уда­ляем Кар­лоса.

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

Ла­бора­тория успешно прой­дена!

Инъекция через параметр kid

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

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

Ес­ли неп­равиль­но работать с этой стро­кой, то, как и при любой работе с базой дан­ных, может воз­никнуть уяз­вимость SQL injection, бла­года­ря которой зло­умыш­ленник смо­жет извлечь содер­жимое базы. Если же клю­чи хра­нят­ся на дис­ке, это может при­вес­ти к обхо­ду катало­гов и уяз­вимос­ти path traversal с чте­нием про­изволь­ных фай­лов на дис­ке сер­вера.

{
"kid": "../../path/to/file",
"typ": "JWT",
"alg": "HS256",
"k": "asGsADas3421-dfh9DGN-AFDFDbasfd8-anfjkvc"
}

Ес­ли сер­вер уяз­вим к path traversal, то мож­но в качес­тве kid ука­зать путь к пус­тому фай­лу /dev/null. В качес­тве под­писи будет взя­то его содер­жимое, а пос­коль­ку в этом фай­ле ничего нет, то и для генера­ции под­писи пот­ребу­ется толь­ко пус­тая стро­ка вмес­то пол­ноцен­ного клю­ча. Этим может вос­поль­зовать­ся зло­умыш­ленник или мы — в новой лабора­тории.

Лаба: JWT authentication bypass via kid header path traversal

От­кры­ваем лабора­торию, аутен­тифици­руем­ся от име­ни тес­тового поль­зовате­ля и отправ­ляем зап­рос на откры­тие лич­ного кабине­та в Repeater. Выделя­ем заголо­вок и про­буем экс­плу­ати­ровать path traversal. Для это­го выходим из дирек­тории, в которой находит­ся при­ложе­ние, к самому кор­ню и ука­зыва­ем путь к /dev/null. Сох­раня­ем изме­нения.

Path traversal через параметр kid
Path traversal через параметр kid

Те­перь нам нуж­но под­писать токен. Наше рас­ширение JWT Editor не поз­воля­ет под­писывать JWT с помощью пус­той стро­ки. Одна­ко из‑за ошиб­ки это мож­но обой­ти, исполь­зуя нулевой байт в кодиров­ке Base64.

Пе­рей­дем на вклад­ку JWT Editor, выберем New Symmetric Key, в появив­шемся окне наж­мем Generate и заменим содер­жимое перемен­ной k нулевым бай­том в Base64. Это будет AA==. Сох­раним резуль­тат.

info

Па­раметр k на вклад­ке JWT Editor обоз­нача­ет сим­метрич­ный ключ, закоди­рован­ный в Base64, который будет исполь­зован для под­писи.

Нулевой байт в Base64 в качестве подписи
Ну­левой байт в Base64 в качес­тве под­писи

Вер­немся в Repeater и перей­дем на JSON Web Token:

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

Процесс подписания токена JWT
Про­цесс под­писания токена JWT

От­пра­вим зап­рос.

Ссылка на панель администратора
Ссыл­ка на панель адми­нис­тра­тора

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

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

Мы прош­ли пос­леднюю прос­тую лабора­торию!

Ос­тались толь­ко экспертные.

Атаки algorithm confusion

Ата­ки algorithm confusion, или ата­ки с путани­цей в алго­рит­мах (еще их иног­да называ­ют ата­ками с путани­цей в клю­чах), воз­ника­ют, ког­да зло­умыш­ленни­ку уда­ется зас­тавить сер­вер про­верить под­пись токена JWT с помощью алго­рит­ма, отлично­го от пре­дус­мотрен­ного раз­работ­чиками веб‑при­ложе­ния. При этом это не None, как было в одном из пре­дыду­щих кей­сов. Если такой слу­чай не обра­ботать дол­жным обра­зом, зло­умыш­ленни­ки могут под­делать токен JWT собс­твен­ными зна­чени­ями, не зная сек­ретный ключ под­писи сер­вера.

Уяз­вимос­ти, свя­зан­ные с путани­цей алго­рит­мов, обыч­но воз­ника­ют из‑за несовер­шенс­тва биб­лиотек, которые поз­воля­ют работать с токена­ми JWT. Хотя фак­тичес­кий про­цесс про­вер­ки отли­чает­ся в зависи­мос­ти от исполь­зуемо­го алго­рит­ма, мно­гие биб­лиоте­ки пре­дос­тавля­ют еди­ный, не завися­щий от алго­рит­ма метод про­вер­ки под­писей. Эти методы полага­ются на параметр alg в заголов­ке токена, что­бы опре­делить тип про­вер­ки, которую они дол­жны выпол­нить.

Да­вай раз­берем на при­мере, что­бы было понят­нее. Пред­положим, исполь­зует­ся метод verify() для про­вер­ки валид­ности под­писи. Он при­нима­ет не толь­ко тот алго­ритм, который исполь­зуют раз­работ­чики, но и дру­гие. Вот его псев­докод:

function verify(token, secretOrPublicKey){
algorithm = token.getAlgHeader();
if(algorithm == "RS256"){
// Use the provided key as an RSA public key
} else if (algorithm == "HS256"){
// Use the provided key as an HMAC secret key
}
}

Проб­лемы воз­ника­ют, ког­да раз­работ­чики сай­тов, исполь­зующие этот метод в сво­ем коде, дума­ют, что он будет работать исклю­читель­но с токена­ми JWT, под­писан­ными асим­метрич­ным алго­рит­мом, нап­ример RS256. Хотя по фак­ту это не так. Этот метод будет работать с любыми алго­рит­мами. Из‑за это­го оши­боч­ного пред­положе­ния они могут всег­да переда­вать методу фик­сирован­ный откры­тый ключ сле­дующим обра­зом:

publicKey = <public-key-of-server>;
token = request.getCookie("session");
verify(token, publicKey);

В этом слу­чае, если сер­вер получит токен JWT, в котором мы ука­жем сим­метрич­ный алго­ритм шиф­рования, нап­ример HS256, метод verify() будет рас­смат­ривать откры­тый ключ как сек­рет для HMAC-фун­кции. Это озна­чает, что зло­умыш­ленник может под­писать токен, исполь­зуя HS256 в связ­ке с откры­тым клю­чом, который сер­вер не скры­вает.

info

От­кры­тый ключ, который ты будешь исполь­зовать для под­писи JWT-токена, дол­жен быть иден­тичен тому откры­тому клю­чу, который хра­нит­ся на сер­вере. Под этим под­разуме­вает­ся исполь­зование того же фор­мата (нап­ример, X.509 PEM) и сох­ранение любых непечат­ных сим­волов, таких как новые стро­ки. На прак­тике при­дет­ся мно­го экспе­римен­тировать, что­бы эту ата­ку уда­лось реали­зовать.

Лаба: JWT authentication bypass via algorithm confusion

За­ходим в лабора­торию как тес­товый поль­зователь и смот­рим на токен JWT.

JWT-токен, выписанный сервером, в исходном виде
JWT-токен, выписан­ный сер­вером, в исходном виде

Под­меча­ем алго­ритм — RS256. Это зна­чит, что сер­вер исполь­зует при­ват­ный ключ для под­писи токенов, а пуб­личный — для ее про­вер­ки. Нам нуж­но научить­ся изме­нять и под­писывать JWT, не имея при­ват­ного клю­ча. Для это­го мы экс­плу­ати­руем уяз­вимость с путани­цей в алго­рит­мах, что­бы под­писать JWT с помощью пуб­лично­го клю­ча и зас­тавить сер­вер исполь­зовать этот же ключ для ее про­вер­ки.

Про­верим дирек­тории /jwks.json и /.well-known/jwks.json: не оста­вил ли в них сер­вер пуб­личные клю­чи для нас? Оста­вил!

Сервер оставил публичные ключи в директории /jwks.json
Сер­вер оста­вил пуб­личные клю­чи в дирек­тории /jwks.json

По­мимо этих, мож­но про­верять еще сле­дующие пути:

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

openssl s_client -connect http://example.com:443/ 2>&1 < /dev/null | sed -n '/-----BEGIN/,/-----END/p' > certificatechain.pem
openssl x509 -pubkey -in certificatechain.pem -noout > pubkey.pem

Итак, ключ, который мы наш­ли в дирек­тории, исполь­зует­ся сер­вером для про­вер­ки под­писи. В опи­сании к заданию было ска­зано, что он исполь­зует собс­твен­ную копию, которая лежит на самом хос­те в фор­мате X.509 PEM. Зна­чит, нам нуж­но пре­обра­зовать этот же ключ в исполь­зуемый сер­вером фор­мат, да так, что­бы фай­лы сов­падали.

Пе­рехо­дим на вклад­ку JWT Editor и соз­даем новый ключ RSA. Встав­ляем JWK, который мы наш­ли на сай­те.

Публичный ключ на странице создания JWT Editor
Пуб­личный ключ на стра­нице соз­дания JWT Editor

Пе­рек­люча­ем на PEM. JWT Editor пре­обра­зует встав­ленный ранее ключ в этот фор­мат.

Конвертация из JWK в PEM
Кон­верта­ция из JWK в PEM

Сох­раня­ем и перек­люча­емся на вклад­ку Decoder. Кодиру­ем ключ в Base64.

PEM, закодированный в Base64
PEM, закоди­рован­ный в Base64

Ко­пиру­ем получив­шееся зна­чение и воз­вра­щаем­ся в JWT Editor. Нажима­ем «Новый сим­метрич­ный ключ» и генери­руем его зна­чение соот­ветс­тву­ющей кноп­кой.

Процесс создания нового симметричного ключа
Про­цесс соз­дания нового сим­метрич­ного клю­ча

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

Параметр k — наш публичный ключ, закодированный в Base64
Па­раметр k — наш пуб­личный ключ, закоди­рован­ный в Base64

Воз­вра­щаем­ся к зап­росу. Меня­ем поль­зовате­ля и алго­ритм с асим­метрич­ного на сим­метрич­ный HS256.

Асимметричный RS256 нужно заменить симметричным HS256
Асим­метрич­ный RS256 нуж­но заменить сим­метрич­ным HS256

Под­писыва­ем кноп­кой Sign, Header мож­но не модифи­циро­вать.

Подписание токена JWT
Под­писание токена JWT

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

Ссылка на панель администратора
Ссыл­ка на панель адми­нис­тра­тора

Пе­рехо­дим в бра­узер с новым токеном, откры­ваем админку и уда­ляем Кар­лоса.

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

Ла­бора­тория успешно прой­дена!

Лаба: JWT authentication bypass via algorithm confusion with no exposed key

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

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

Доб­рые люди из Cryptography Stack Exchange уже всё при­дума­ли за нас и пред­ста­вили прос­тое и гени­аль­ное решение: най­ти наиболь­ший общий делитель (НОД) раз­ности всех дос­тупных пар сооб­щение — под­пись. Не вда­ваясь в под­робнос­ти того, почему это работа­ет (пол­ное объ­ясне­ние можешь про­читать в ком­мента­рии на Stack Exchange), отме­тим нес­коль­ко момен­тов:

Итак, прис­тупа­ем к выпол­нению. Заходим один раз от име­ни wiener, получа­ем токен и копиру­ем его куда‑нибудь к себе в тек­сто­вый документ. Вот что у меня получи­лось:

eyJraWQiOiJlYTc5ODI1ZC0wZGE1LTQ0M2YtODRhOC1mYjU3YjIwOWQ1ZmUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTcyMDgyMjQ3Nywic3ViIjoid2llbmVyIn0.sO3PBnZQ0CVk-ict3vvRsPnZFsrPIW4Rsptfl0sviLLI8JNDoqzyrZNYLJoigkhdNTTtI8Svf3AJvVhHO3b0VIQigd52P8KYwXEIHNEGsbcY7Robg9gTn7kXulaksKb1CYcwXGqUC17xOm2BRD9Z0D_HU_hDufxyk4xGS3nR7aIxZvEHJcJoyyD6BcPxvs1PuPCFrOYyqjlGZqcd3rVpc9ZGdk3hco1amFOGuG33pBF8EjrSXzrX6jONuEF31D6M5Abr58-_ip30-_RqsRf4P6tLA-6_mKRS5xzIi_ZypuszdU2kPlO_npFTqILgdO_OScS6KgaSW8EU2ghLIo8JGA

Вы­ходим, аутен­тифици­руем­ся вто­рой раз и копиру­ем вто­рой JWT-токен:

eyJraWQiOiJlYTc5ODI1ZC0wZGE1LTQ0M2YtODRhOC1mYjU3YjIwOWQ1ZmUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTcyMDgyMjU2OCwic3ViIjoid2llbmVyIn0.UlNcTGk8SfBv_AyBEXYqClWbRl1jY985uSs2OjcW6nKFwyRnnutcKRX1Zoj8zqn-51TN12-C1yf-hlQ5nZESdpCifDBDJVQ8DUKmJ1Ke7ao7WKHxVaOV0VNbk5vOyTpiCccp4p4ihVOn6BNW9bll28Fl9L10PpPMnC1DRO--9snEZEFQ_Xvmnt3xfG9h6IIytApT57LgNk7GHQHsQvspVRXSAeGSCgxz3FRkRC5KZDV2juLxi6iojPbhIX83fpkT6L_XBv-_a7-dw68OEKCM0Qnau8d-Kj4xqwndQ9c1ZK0vVwUnzpsag1F-eEsvhzUzczZYtCOd3NKUh5crVoIhgg

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

Он так­же есть в Docker Registry, поэто­му вос­поль­зуем­ся докером:

docker run --rm -it portswigger/sig2n <token1> <token2>

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

Извлечение публичного ключа на основе подписанных токенов JWT
Из­вле­чение пуб­лично­го клю­ча на осно­ве под­писан­ных токенов JWT

Са­ми клю­чи сге­нери­рова­лись в фор­матах X.509 и PKCS1 и были заэн­кожены в Base64. Как мы пом­ним, на сер­вере они хра­нят­ся в X.509, поэто­му мы будем исполь­зовать имен­но этот фор­мат.

Но сна­чала нам нуж­но опре­делить, какой из двух сге­нери­рован­ных клю­чей соот­ветс­тву­ет сер­верно­му. Копиру­ем под­писан­ные токены Tampered JWT и про­буем по одно­му в Repeater.

Ес­ли в отве­те не было редирек­та на стра­ницу логина, зна­чит, пуб­личный ключ успешно подоб­ран.

Страница должна открываться от имени пользователя wiener
Стра­ница дол­жна откры­вать­ся от име­ни поль­зовате­ля wiener

Пе­рехо­дим в JWT Editor, генери­руем новый сим­метрич­ный ключ и заменя­ем k тем, которым был под­писан подошед­ший JWT-токен.

Процесс создания нового симметричного ключа
Про­цесс соз­дания нового сим­метрич­ного клю­ча

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

Подписание токена JWT с помощью публичного ключа
Под­писание токена JWT с помощью пуб­лично­го клю­ча

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

Ссылка на панель администратора в личном кабинете
Ссыл­ка на панель адми­нис­тра­тора в лич­ном кабине­те

Уда­ляем Кар­лоса.

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

Ла­бора­тория успешно прой­дена! Пос­ледняя!

Другие интересные векторы

Сле­дующие парамет­ры тоже могут пред­став­лять инте­рес:

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

На осно­ве все­го перечис­ленно­го я сос­тавил чек‑лист про­верок. Исполь­зуй его при поис­ке мис­конфи­гура­ций, свя­зан­ных с JWT-токена­ми.

  1. Пер­вый взгляд:
    • най­ти токены JWT;
    • по­искать под­ходящую для тес­тов стра­ницу.
  2. Прос­тые про­вер­ки:
    • обя­зате­лен ли токен JWT;
    • вы­дает ли сайт сер­висные ошиб­ки из‑за неп­равиль­ных токенов;
    • ис­поль­зует­ся ли для HMAC сла­бый сек­рет;
    • про­веря­ется ли срок дей­ствия токена.
  3. Ба­зовые экс­пло­иты:
    • ал­горитм none (CVE-2015-9235);
    • ата­ки с путани­цей клю­чей (CVE-2016-5431);
    • JWKS-инъ­екция (CVE-2018-0114);
    • под­пись нулевым бай­том (CVE-2020-28042).
  4. До­пол­нитель­ные про­вер­ки:
    • path traversal через kid;
    • SQL injection через kid;
    • ата­ки URL tampering (SSRF через параметр jku);
    • JWKS-спу­финг.
  5. Рас­ширен­ные про­вер­ки:
    • меж­сервис­ные релей‑ата­ки.

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

Страница jwt_tool на GitHub
Стра­ница jwt_tool на GitHub

На руч­ную про­вер­ку всех проб­лем, свя­зан­ных с JWT-токена­ми, может уйти мно­го вре­мени. Боль­шинс­тво атак, которые мы рас­смот­рели, нес­ложно авто­мати­зиро­вать и быс­тро про­верить с помощью готово­го тул­кита. Имен­но такой тул­кит раз­работал Энди Тай­лер: jwt_tool.py — это набор инс­тру­мен­тов для валида­ции, ска­ниро­вания и под­делыва­ния JWT (JSON Web Tokens).

Кло­ниру­ем репози­торий и ста­вим зависи­мос­ти:

git clone https://github.com/ticarpi/jwt_tool
python3 -m pip install -r requirements.txt

За­пус­каем jwt_tool в режиме Playbook с базовы­ми про­вер­ками (не забудь сме­нить ticarpi.com на тот сайт, где ты тес­тиру­ешь токены):

python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -M pb -np

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

python3 jwt_tool.py -t https://www.ticarpi.com/ -rc "jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJsb2dpbiI6InRpY2FycGkifQ.bsSwqj2c2uI9n7-ajmi3ixVGhPUiY7jO9SUn9dm15Po;anothercookie=test" -X i -I -pc name -pv admin

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

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

Так­же советую соб­людать луч­шие прак­тики:

Выводы

Кру­то, если ты про­читал всю статью и дошел досюда! Теперь ты зна­ешь обо всех ошиб­ках, которые мож­но допус­тить при работе с JWT-токена­ми, — а опас­ных мест дей­стви­тель­но мно­го. Кро­ме того, мы научи­лись находить такие ошиб­ки, и ты уже можешь исполь­зовать это на прак­тике. Более того, эти зна­ния помогут тебе сдать некото­рые сер­тифика­ции, осо­бен­но свя­зан­ные с вебом, — нап­ример, тот же Burp Suite Certified Practitioner.