JWT-токены от и до
Разбираемся с JSON Web Tokens и атаками на них
Аутентификация и авторизация
Прежде чем мы перейдем непосредственно к JWT, давай повторим основы и вспомним главные термины, которые неразрывно связаны с современным вебом и информационной безопасностью, а именно — аутентификация и авторизация. Эти знания нам необходимы, чтобы понимать весь дальнейший материал.
Представим, что мы хотим войти в свой аккаунт Facebook. Веб‑приложению нужно понять, что мы владеем этим аккаунтом. Для этого оно просит нас предоставить корректные учетные данные. Обычно это логин и пароль, которые мы вводим на странице входа.
Приложение проверяет предоставленные данные, и, если они верны, мы входим в собственный аккаунт. Этот процесс называется аутентификацией.
info
Аутентификация — проверка подлинности предъявленного пользователем идентификатора, в процессе которой пользователь доказывает, что он действительно тот, за кого себя выдает. Сравнение пароля, введенного пользователем, с паролем, который сохранен в базе данных сервера, — один из примеров аутентификации.

После успешной аутентификации мы заходим на страницу со своим профилем. Веб‑приложение понимает, что пользователь — тот, за кого себя выдает, то есть мы успешно аутентифицированы.
Теперь мы можем отредактировать собственный профиль, изменить имя и фамилию или поменять возраст. Мы не можем редактировать чужой профиль, поэтому прежде, чем разрешить редактирование, приложению нужно убедиться, что запрос пришел от владельца профиля. Процесс проверки прав на осуществление действий называется авторизацией.
info
Авторизация — это процесс проверки, подтверждения или предоставления разрешений, прав доступа и привилегий на выполнение определенных действий. Пример авторизации: пользователь хочет прочитать документ на сайте, приложение проверяет, имеет ли он право читать его.

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

HTTP Cookie — это небольшие записи, которыми обмениваются клиент (твой браузер) и сервер. Всякий раз при обращении к соответствующему сайту эти данные пересылаются серверу в составе HTTP-запроса.
Куки придуманы давно, в начале девяностых годов. В июне 1994 года Лу Монтулли, сотруднику Netscape Communications, пришла идея использовать их при веб‑соединении.
Куки используются в веб‑приложениях:
- для управления сеансами пользователя и проверки его прав;
- для отслеживания активности и персонализации контента (специальные «рекламные» куки).
Процесс использования кук выглядит следующим образом:
- Браузер отправляет приложению запрос с логином и паролем.
- Приложение проверяет логин и пароль пользователя. Если они совпадают с теми, что хранятся в базе (вместо паролей сравниваются их хеши), то приложение формирует сессию, генерирует сессионную куку и отправляет ее браузеру пользователя.
- Браузер сохраняет куку и отправляет ее вместе с каждым запросом в приложение.
- Приложение получает куку, расшифровывает ее и, если она валидная, выполняет действие от имени авторизованного пользователя.
В итоге нам не нужно вводить пароль каждый раз. Благодаря кукам сервер понимает, что мы уже прошли этот процесс.
info
Сервер не должен хранить куки пользователей ни в базе данных, ни в файловой системе. Они должны использоваться только в рантайме, чтобы ими не могли воспользоваться злоумышленники, которые получат доступ к серверу.
Для присвоения куки пользователю сервер отправляет такой заголовок:
Set-Cookie: <имя cookie>=<значение cookie>
Браузер в свою очередь — такой:
Cookie: <имя cookie>=<значение cookie>
Каждый из этих заголовков поддерживает множество атрибутов, которые позволяют снизить риски при возникновении уязвимостей на стороне приложения.
Токены
В современном вебе используются не только куки, но и токены. Это альтернативный и более современный механизм, который имеет свои плюсы и минусы по сравнению с привычными «печеньками».
Например, когда ты хочешь аутентифицироваться на сайте через другой сайт, на котором ты уже аутентифицирован (например, войти в Facebook через Gmail), используются OAuth-токены для межсерверной аутентификации между приложением и провайдером данных.
OAuth — не единственные токены, существует много других форматов:
-
Simple Web Token — набор пар имя — значение в формате кодирования HTML Form, описывает стандартные ключи
Issuer
,Audience
,ExpiresOn
иHMACSHA256
; - Security Assertion Markup Language (SAML) — определяет токены в XML-формате, включающем информацию об эмитенте и субъекте, а также условия для валидации токена. Подпись осуществляется при помощи асимметричной криптографии. Содержат механизм для подтверждения владения токеном;
- JSON Web Token, 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-токена веб‑приложение ставит подпись секретным ключом, который хранится только в приложении. Может использоваться как симметричное шифрование, так и асимметричное.
- Браузер сохраняет JWT-токен и отправляет его вместе с каждым запросом в приложение.
- Приложение проверяет JWT-токен и, если он валидный (то есть он правильно сформирован и данные не были изменены), выполняет действие от имени авторизованного пользователя.
Все данные, которые нужны серверу, хранятся на стороне клиента в самом JWT. Это делает JWT популярным выбором для высокораспределенных веб‑сайтов, где пользователям необходимо беспрепятственно взаимодействовать с несколькими внутренними серверами.
Безопасность коммуникации между веб‑браузером и веб‑приложением строится на том, что токены генерируются и подписываются только со стороны веб‑приложения. Злоумышленник в теории не сможет подделать токен, так как не знает секретный ключ, который используется для подписи токена.
При подписи токена используется шифрование. С помощью подписи веб‑приложение проверяет, что токен действительно был сгенерирован им. Алгоритмы шифрования могут быть разными, например HS256 — HMAC с SHA-256.
Формат JWT
JWT-токен состоит из трех частей, которые разделены точкой:
- Header (заголовок) — информация о токене, тип токена и алгоритм шифрования;
- Payload (полезные данные) — данные, которые мы хотим передать в токене. Например, имя пользователя, его роль, истекает ли токен. Эти данные представлены в виде JSON-объекта;
- Signature (подпись) — подпись токена, которая позволяет проверить, что токен не был изменен.
Формат токена:
header.payload.signature
Каждая из этих частей обычно кодируется в Base64 для передачи без повреждений по сети. Помимо этого, используется облегченный вариант URL-кодирования. Свойственный Base64 знак равенства усекается. Плюс заменяется минусом, а слеш (/
) — подчеркиванием, чтобы не возникло коллизий.
Пример токена:
eyJraWQiOiI5MTM2ZGRiMy1jYjBhLTRhMTktYTA3ZS1lYWRmNWE0NGM4YjUiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTY0ODAzNzE2NCwibmFtZSI6IkNhcmxvcyBNb250b3lhIiwic3ViIjoiY2FybG9zIiwicm9sZSI6ImJsb2dfYXV0aG9yIiwiZW1haWwiOiJjYXJsb3NAY2FybG9zLW1vbnRveWEubmV0IiwiaWF0IjoxNTE2MjM5MDIyfQ.SYZBPIBg2CRjXAJ8vCER0LA_ENjII1JakvNQoP-Hw6GG1zfl4JyngsZReIfqRvIAEi5L4HV0q7_9qGhQZvy9ZdxEJbwTxRs_6Lb-fZTDpW6lKYNdMyjw45_alSCZ1fypsMWz_2mTpQzil0lOtps5Ei_z7mM7M8gCwe_AGpI53JxduQOaB5HkT5gVrv9cKu9CsW5MS6ZbqYXpGyOG5ehoxqm8DL5tFYaW3lB50ELxi0KsuTKEbD0t5BCl0aCR2MBJWAbN-xeLwEenaqBiwPVvKixYleeDQiBEIylFdNNIMviKRgXiYuAvMziVPbwSgkZVHeEdF5MQP1Oe2Spac-6IfA
Header
Заголовок обычно состоит из JSON-объекта с двумя свойствами:
- тип токена, в нашем случае — JWT;
- алгоритм шифрования, в нашем случае — HMAC SHA-256.
Далее этот 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
. Программист может вызвать:
- метод
verifiy(
, который проверяет подпись у токенов и возвращает) True
, если подпись валидна, иFalse
, если нет; - метод
decode(
из этой же библиотеки, который декодирует токен и всегда возвращает) True
.
Их легко спутать и передать токен в метод decode(
вместо правильного verify(
. В таком случае приложение вообще не будет проверять подпись и будет считать ее валидной. При этом разработчик заметит такую недоработку не сразу, особенно если на сервере не ведутся логи.
Лаба: JWT authentication bypass via unverified signature
Эта лаборатория позволит нам проэксплуатировать такую уязвимость на практике. Как сказано в задании к ней, нам нужно изменить сеансовый токен и получить доступ к панели администратора по адресу /
. Из‑за недостатков реализации сервер не проверяет подпись получаемых JWT. Мы в роли злоумышленника должны взломать приложение.
Откроем лабораторию. Перед нами — блог с личным кабинетом.

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

Тестовые данные были указаны в описании к заданию:
- логин —
wiener
; - пароль —
peter
.
Введем их, нажмем Log in и взглянем на запрос в Burp Suite.

В теле запроса отправились следующие данные:
-
csrf=VpPh0Tg2R6YvJjg4Vshrpeot5G76WUji
— токен CSRF, нас он не интересует; -
username=wiener
— пользователь; -
password=peter
— пароль.
В ответе пришел JWT-токен. Это говорит о том, что мы успешно вошли на сайт.
eyJraWQiOiJhYzhjMWM5Yy0yMWU5LTRkODYtODlkYi02M2M2NWJiYTUxNTciLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJwb3J0c3dpZ2dlciIsImV4cCI6MTcyMDcyODY2Nywic3ViIjoid2llbmVyIn0.BMsKR6H2eFhSahX97sPW9WoXpSd5FZRDNC12am1_K8vBwEuc_PdpnOjJIHtYY1q-dOi93c5A90xhUaUWi8z3QwgU6zTsVPbgJK6q7_6EU8KsSSNF_saayykHxNsN4YwZlwl3qwaB3TjyriEPtFKHB-fFS04cJQURLSAGCx39Y5FXzK4BUa3cLdN5-TV3GwDYGCd29oTAXqfGmvkuJ8DxRrjrWLlEYa1_IzzLY7XTMJAdfIeiR7zTwlkixndRUGO1-hZEHq5Cjojg-3MJ0RuieH42qJQKqgFVzSWslEM-3jtTV3rCHVK_nwM0llPOaOe0VUgYoMt-yq57rx8_Ucx7Fg
В таком виде прочитать его мы не можем. Перейдем на вкладку JSON Web Token. Она появилась благодаря расширению JWT Editor, которое мы установили ранее.

На этой вкладке можно увидеть исходный токен JWT, а также декодированный из Base64 Header и Payload, который располагается под ним. Еще ниже — подпись.
Заголовок:
-
kid
(Key ID) — уникальный идентификатор ключа в формате UUID4; -
alg
— алгоритм, которым были подписаны данные, в нашем случае это RS256.
Полезная нагрузка:
-
iss
(issuer) — тот, кто выписал JWT-токен, в лаборатории этоportswigger
; -
exp
(expires) — дата истечения токена в формате Unix Timestamp, после которой он станет невалидным. Если его декодировать c помощью unixtimestamp.com, можно узнать, что токен станет невалидным через час; -
sub
(subject) — тот, для кого выписан токен, здесь указан пользовательwiener
, от имени которого мы вошли.
Нам необходимо зайти от имени администратора. Это значит, что нужно изменить subject
.
Найдем GET-запрос в личный кабинет — это /
. Отправим его в Repeater и выделим ту часть 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 — это хранилище браузера, где лежит наш токен.

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

Сейчас в ней лежит старый токен 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.

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

Сделаем то же, что делали в прошлой лаборатории:
- уберем из запроса
?id=wiener
; - выделим полезную нагрузку и исправим wiener на administrator в теле.

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

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

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

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

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

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

Мы успешно прошли вторую лабораторию.
Брутфорс секретов
Представим, что разработчики наконец‑то научились правильно проверять подпись и перестали доверять пользовательскому вводу.
Как ты понимаешь, приставка в используемом алгоритме подписи означает, что используется HMAC (алгоритм аутентификации сообщений) с применением хеш‑функции SHA-256.
Как вычисляется HMAC-SHA256 — не секрет. Выглядит это следующим образом:
HMAC_SHA256(key, message) = SHA256((key ⊕ opad) ∥ SHA256((key ⊕ ipad) ∥ message))
Здесь
-
key
— секретный ключ, возможно дополненный или сокращенный до нужной длины; -
message
— сообщение для аутентификации; -
opad
иipad
— константыpadding
, определенные стандартом HMAC; -
⊕
— побитовый XOR; -
∥
— операция конкатенации.
Зная ключ, мы сможем генерировать собственную подпись этим же алгоритмом. Чтобы его найти и научиться подписывать произвольные данные, мы должны взять исходный заголовок и пейлоад и начать применять к ним функцию HS256 с разными ключами, пока не получим такую же подпись, которую отдал нам изначально сервер.
info
Очень важно выбирать надежный ключ, чтобы злоумышленник не мог его сбрутить (перебрать) и начать подписывать им собственные токены. Очень часто разработчики не задумываются об этом и используют очень простые комбинации или ключи по умолчанию, которые всем известны.
Если строки будут идентичны, это будет значить, что мы подобрали ключ шифрования.
Лаба: JWT authentication bypass via weak signing key
Итак, вся безопасность JWT с алгоритмом HS256 держится на том, насколько надежный был выбран симметричный ключ. Давай для начала добудем JWT-токен, чтобы иметь возможность его подобрать.
Откроем лабораторию.

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

Скопируем весь токен. Теперь нам нужно подобрать ключ, для этого мы будем использовать инструмент под названием hashcat. Эта утилита позволяет восстанавливать пароли из хешей с помощью различных атак: по словарю, по маске, на основе правил и так далее.
Нам нужен первый вариант, то есть атака по словарю. В качестве словаря будем использовать jwt.secrets.list. Это набор стандартных секретов, которые используются по умолчанию в разных библиотеках.
Утилита hashcat поможет взять все известные ключи из этого списка и определить, какой из них использовался для подписи нашего JWT-токена, последовательно подписывая данные до тех пор, пока мы не получим такую же подпись, какую нам выдал сервер.
Скопируем JWT-токен и подставим его в команду:
hashcat -a 0 -m 16500 <jwt> <wordlist>
Разберем ее по порядку:
-
-a
— флаг, который означает, что мы будем использовать атаку по словарю;0 -
-m
— указывает, что тип хеша, который будет взламываться, — это JSON Web Token (JWT). Код 16500 соответствует HMAC-SHA256 JSON Web Token;16500 -
<
— это наш JWT;jwt> -
<
— путь к словарю, который мы только что скачали.wordlist>
Результат выполнения — на скриншоте ниже.

Спустя две секунды 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
.

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

Сохраняем.

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

Как бы я ни пытался отправлять запрос после этого, у меня ничего не получилось. Расширение давно не обновлялось, либо оно плохо работает в Windows.
Поэтому мы научимся подписывать токены вручную, ведь мы знаем алгоритм. Переходим в Bash на Linux и выполняем следующий однострочник:
echo -n <HEADER.PAYLOAD> | openssl dgst -sha256 -hmac secret1 -binary | openssl base64 -e -A | sed s/\+/-/ | sed -E s/=+$//
Вместо <
вставляем наш JWT-токен. Тот, где мы сменили wiener
на administrator
.
В результате в консоли выводится готовая подпись.

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

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

Мы успешно прошли третью лабораторию.
Инъекции в параметры заголовка
Предыдущие уязвимости были связаны с симметричными алгоритмами шифрования, когда есть один‑единственный приватный ключ. Теперь мы переходим к разделу с асимметричным шифрованием в JWT-токенах, когда используются приватный и публичный ключи.
Сервер подписывает своим приватным ключом (который он никому не передает) токены, а мы расшифровываем их с помощью публичного ключа и убеждаемся в том, что они были подписаны подлинным сервером.

Согласно спецификации JWS, в заголовке существует только один обязательный параметр — это параметр alg
, означающий используемый алгоритм подписи, с которым мы уже сталкивались ранее. Однако на практике, когда ты будешь пентестить реальное веб‑приложение, их окажется куда больше.
Например, есть проблема: откуда пользователь или другое приложение, которое будет использовать токен, возьмет публичный ключ, чтобы самому убедиться в том, что JWT-токен подписал сервер, а не кто‑то другой? Есть два варианта:
- Приложение отдает в ответе параметр
kid
(идентификатор ключа Key ID), по которому мы можем отправить запрос в реестр этого же приложения и получить публичный ключ. Требует выдать доступ ко внутреннему реестру, а это создает дополнительные риски. - Приложение отдает JWK — параметр, в который сразу записан публичный ключ. На первый взгляд, удобнее и проще реализовать, но тут тоже можно допустить ошибку.
Заголовки JWT-токенов (которые также известны как JOSE-заголовки) часто содержат ряд других параметров. Вот какие параметры обычно могут быть нам интересны:
-
jwk
(JSON Web Key) — это структура данных в формате JSON, в которую записывается публичный ключ. Параметры этой структуры представляют основные свойства ключа, в том числе его значение; -
jku
(JSON Web Key Set URL) — если публичных ключей несколько, в этом параметре можно указать URL, по которому будет подгружаться JSON-объект с публичными ключами; -
kid
(Key ID) — уникальный идентификатор ключа, который серверы могут использовать для определения того ключа, который им необходимо использовать, если их несколько.
Эти параметры присылает сам сервер. Но если сервер безоговорочно доверяет пользователю и не использует механизм белых списков, то это может привести к тому, что злоумышленник сможет отправить собственный публичный ключ, который он сгенерирует сам. Это позволит ему подписать произвольные данные, а сервер действительно убедится в его подлинности с помощью публичного ключа, присланного злоумышленником.
Все три уязвимости, которые можно допустить с каждым из параметров и о которых мы будем говорить ниже, связаны с доверием к пользовательскому вводу.
Инъекция через параметр 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
мы уже знакомы, вот что означают остальные параметры:
-
kty
— определяет тип ключа (key type). В данном случаеRSA
указывает, что ключ является ключом RSA; -
e
— экспонента ключа (exponent). ЗначениеAQAB
— открытая экспонента RSA; -
n
— модуль ключа (modulus). Это длинное значение представляет модуль RSA, который используется в расчетах шифрования и дешифрования.
В идеале серверы для проверки подписей JWT-токенов должны использовать только те ключи, которые у них есть в белом списке. Однако неправильно настроенные серверы иногда будут использовать любой ключ, встроенный в параметр jwk
.
Атакующий может злоупотребить этим поведением, подписав модифицированный JWT с помощью собственного закрытого ключа RSA, а затем вставить соответствующий открытый ключ в заголовок jwk
.
Лаба: JWT authentication bypass via jwk header injection
Начинаем прохождение. Нас снова приветствует привычный блог.

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

Сервер использует асимметричную криптографию и подписывает данные приватным ключом, который лежит у него на сервере. Давай проверим, уязвим ли он к атаке с внедрением собственного публичного ключа.
Переходим в JWT Editor, нажимаем New RSA Key и генерируем собственный ключ.

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

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

Как можно заметить на картинке ниже, расширение помогло вставить в 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" } ]}
Здесь указано два публичных ключа, но может быть гораздо больше. Нам хватит и одного.
Для таких ключей также есть общепринятый путь: /.
.
info
Если сервер и поддерживает формат jku, на него тоже должны распространяться белые списки. Помимо внедрения публичных ключей злоумышленника, здесь также возможны атаки типа SSRF.
Лаба: JWT authentication bypass via jku header injection
Для этого задания нам предоставили Exploit Server, на котором мы сможем разместить ключи. Давай заранее изменим file
с /
на /.
. Ключей у нас пока нет, поэтому перейдем в Burp Suite, а сюда вернемся позже.

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

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

У меня получился следующий заголовок:
{ "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 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" } ]}
Уберем отсюда следующие параметры:
- p — один из двух простых множителей модуля;
- q — второй простой множитель модуля;
- d — правильная экспонента;
- qi — коэффициент обратного преобразования;
-
dp — приватная экспонента для простого множителя
p
; -
dq — приватная экспонента для простого множителя
q
.
Они должны быть строго конфиденциальными и никогда не должны публиковаться, иначе кто угодно сможет заполучить наш приватный ключ.
Итоговый результат:
{ "keys": [ { "kty": "RSA", "e": "AQAB", "kid": "45a3ae3f-c28f-42db-80f5-deb3256dd93f", "n": "ovcPqdi-LKozZXjExEaZ3wk2BmtTJnqMa4wODS6OzQDJRtzOGabuAeccdH4RWENdkJC3zODQ640WJ85WlOk2lgd0Fx0T4xjk9Q90cfErdQIEWSz3Ensymgej-riF379iFqoyulmYIQoD830Q9VqzhT1Whw2AIgHzfXByVm16eknqUi4xJumrk4G3OXYUTWUI8p4SFGkpzuvLb9iESOFzoZKuVVwPjzJZ8zpbeEMmtu7W9Wi1E4tpKnHz7y4I0Z_0T_iXuFBIM7vnDboKDacahvNZBoAHmsde0CWDIi0GkWsBXpsxvKRjiSli2e2kgl9fDtVFWnCtC6UJwGKQ5vSyDQ" } ]}
Поместим его на наш сервер. Вставим в поле Body
, а в Head
сменим Content-Type
с text/
на application/
, поскольку ответ мы отдаем в формате JSON.

Не забудь нажать 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
указать путь к пустому файлу /
. В качестве подписи будет взято его содержимое, а поскольку в этом файле ничего нет, то и для генерации подписи потребуется только пустая строка вместо полноценного ключа. Этим может воспользоваться злоумышленник или мы — в новой лаборатории.
Лаба: JWT authentication bypass via kid header path traversal
Открываем лабораторию, аутентифицируемся от имени тестового пользователя и отправляем запрос на открытие личного кабинета в Repeater. Выделяем заголовок и пробуем эксплуатировать path traversal. Для этого выходим из директории, в которой находится приложение, к самому корню и указываем путь к /
. Сохраняем изменения.

Теперь нам нужно подписать токен. Наше расширение JWT Editor не позволяет подписывать JWT с помощью пустой строки. Однако из‑за ошибки это можно обойти, используя нулевой байт в кодировке Base64.
Перейдем на вкладку JWT Editor, выберем New Symmetric Key, в появившемся окне нажмем Generate и заменим содержимое переменной k
нулевым байтом в Base64. Это будет AA==
. Сохраним результат.
info
Параметр k
на вкладке JWT Editor обозначает симметричный ключ, закодированный в Base64, который будет использован для подписи.

Вернемся в Repeater и перейдем на JSON Web Token:
- изменим
kid
на../../../../../../../../../
;dev/ null - сменим
wiener
наadministrator
.
И подпишем результат только что созданным ключом, «пустотой» или нулевым байтом.

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

Результат показывает, что мы стали администратором. Записываем токен в куку браузера и удаляем пользователя 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.

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

Помимо этих, можно проверять еще следующие пути:
-
/
;openid/ connect/ jwks. json -
/
;api/ keys -
/
.api/ v1/ keys
В некоторых случаях токены 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, который мы нашли на сайте.

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

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

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

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

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

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

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

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

Лаборатория успешно пройдена!
Лаба: JWT authentication bypass via algorithm confusion with no exposed key
Эта лаборатория отличается от предыдущей тем, что в этот раз владелец сервера не выложил публичные ключи. На практике сайты редко это делают, поэтому лаба ближе к реальности, чем предыдущие.
Как же эксплуатировать уязвимость? Здесь нам на помощь приходит обычная алгебра. Хотя криптосистемы с открытым ключом гарантируют, что закрытый ключ не может быть получен из открытого ключа, подписей, зашифрованных текстов и прочих источников, обычно таких гарантий для открытого ключа нет!
Добрые люди из Cryptography Stack Exchange уже всё придумали за нас и представили простое и гениальное решение: найти наибольший общий делитель (НОД) разности всех доступных пар сообщение — подпись. Не вдаваясь в подробности того, почему это работает (полное объяснение можешь прочитать в комментарии на Stack Exchange), отметим несколько моментов:
- Открытый ключ RSA представляет собой пару целых чисел, где
n
— модуль, аe
— открытая экспонента. Посколькуe
— это обычно некое захардкоженное малое число, нас интересует только нахождениеn
. - Несмотря на то что RSA включает в себя большие числа, по‑настоящему эффективные алгоритмы для нахождения НОД чисел существуют с древних времен (нам не нужно умножать методом перебора).
- Наши шансы растут с увеличением количества известных пар сообщение — подпись.
Итак, приступаем к выполнению. Заходим один раз от имени 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>
Результат выполнения команды — на скриншоте ниже. Скрипту удалось подобрать несколько возможных вариантов НОД, и он сгенерировал нам готовые токены, подписанные публичными ключами, которые он также сгенерировал.

Сами ключи сгенерировались в форматах X.509 и PKCS1 и были заэнкожены в Base64. Как мы помним, на сервере они хранятся в X.509, поэтому мы будем использовать именно этот формат.
Но сначала нам нужно определить, какой из двух сгенерированных ключей соответствует серверному. Копируем подписанные токены Tampered JWT и пробуем по одному в Repeater.
Если в ответе не было редиректа на страницу логина, значит, публичный ключ успешно подобран.

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

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

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

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

Лаборатория успешно пройдена! Последняя!
Другие интересные векторы
Следующие параметры тоже могут представлять интерес:
-
cty
(Content Type) — иногда используется для объявления медиатипа содержимого в пейлоаде токена JWT. Обычно он не указывается в заголовке, но библиотека синтаксического анализа, которая используется для парсинга токенов в приложении, может поддерживать его в любом случае. Если ты нашел способ обойти проверку подписи, можешь попробовать внедрить заголовокcty
, чтобы изменить тип содержимого наtext/
илиxml application/
. Это потенциально может открыть новые векторы для атак XXE и десериализации.x-java-serialized-object -
x5c
(X.509 Certificate Chain) — иногда используется для передачи сертификата открытого ключа X.509 или цепочки сертификатов ключа, используемых для цифровой подписи JWT. Этот параметр заголовка может быть использован для инъекции самоподписанных сертификатов, аналогично атакам инъекции заголовкаjwk
, рассмотренным выше. Из‑за сложности формата X.509 и его расширений парсинг этих сертификатов тоже может привести к уязвимостям. Подробности таких атак выходят за рамки этого материала, но ты можешь ознакомиться с ними, заглянув в CVE-2017-2800 и CVE-2018-2633.
Чек-лист проверок
На основе всего перечисленного я составил чек‑лист проверок. Используй его при поиске мисконфигураций, связанных с JWT-токенами.
- Первый взгляд:
- найти токены JWT;
- поискать подходящую для тестов страницу.
- Простые проверки:
- обязателен ли токен JWT;
- выдает ли сайт сервисные ошибки из‑за неправильных токенов;
- используется ли для HMAC слабый секрет;
- проверяется ли срок действия токена.
- Базовые эксплоиты:
- алгоритм
none
(CVE-2015-9235); - атаки с путаницей ключей (CVE-2016-5431);
- JWKS-инъекция (CVE-2018-0114);
- подпись нулевым байтом (CVE-2020-28042).
- алгоритм
- Дополнительные проверки:
- path traversal через kid;
- SQL injection через kid;
- атаки URL tampering (SSRF через параметр
jku
); - JWKS-спуфинг.
- Расширенные проверки:
- межсервисные релей‑атаки.
Автоматизация

На ручную проверку всех проблем, связанных с 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.
на тот сайт, где ты тестируешь токены):
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 и убедись, что полностью понимаешь принцип ее работы, а также все последствия для безопасности. Многие современные библиотеки пытаются взять на себя защиту от случайных небезопасных последствий. Но их авторам приходится учитывать гибкие спецификации, а это негативно сказывается на безопасности.
- Убедись, что присутствует надежная проверка подписи для всех получаемых JWT и что ты учитываешь такие нестандартные случаи, как JWT, подписанные с использованием неожиданных алгоритмов.
- Обеспечь строгий белый список разрешенных хостов для заголовка
jku
. - Убедись, что твоя программа неуязвима к path traversal или SQL-инъекциям через параметр заголовка
kid
.
Также советую соблюдать лучшие практики:
- Устанавливай дату истечения срока действия для любых выпускаемых токенов.
- По возможности избегай отправки токенов в параметрах URL, чтобы они не логировались.
- Добавь в тело
aud
(audience), чтобы указать предполагаемого получателя токена. Это предотвратит использование токена на других сайтах. - Разреши серверу, выдавшему токен, отзывать его (например, при выходе из системы).
Выводы
Круто, если ты прочитал всю статью и дошел досюда! Теперь ты знаешь обо всех ошибках, которые можно допустить при работе с JWT-токенами, — а опасных мест действительно много. Кроме того, мы научились находить такие ошибки, и ты уже можешь использовать это на практике. Более того, эти знания помогут тебе сдать некоторые сертификации, особенно связанные с вебом, — например, тот же Burp Suite Certified Practitioner.