Главная / Блог / JWT атаки CTF: от структуры токена до захвата флага через alg:none и слабый секрет

13 мин.00

JWT атаки CTF: от структуры токена до захвата флага через alg:none и слабый секрет

JWT атаки CTF: от структуры токена до захвата флага через alg:none и слабый секрет

JWT атаки CTF: от структуры токена до захвата флага через alg:none и слабый секрет

На одном из 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

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 в CTF веб-задаче

Токен аутентификации веб-приложения прячется в одном из четырёх мест:

  • Cookie — самый частый вариант в CTF. Ищем через DevTools (Application → Cookies) или в Burp Suite в перехваченном запросе. Cookie может называться jwt, token, session или что-то кастомное
  • Заголовок Authorization — формат Bearer eyJ0eXAi.... Виден в Burp Proxy или вкладке Network в DevTools
  • Тело ответа — после логина сервер возвращает JWT в JSON, клиентский JavaScript сохраняет его для последующих запросов
  • localStorage / sessionStorage — типично для SPA на React или Vue. Проверяем через DevTools → Application → Local Storage

Первый шаг в любой JWT-задаче: найти токен, декодировать header и payload, определить алгоритм и интересные claims. Дальше — выбираем вектор атаки.

Использование jwt_tool, hashcat, Burp Suite JWT — требования к окружению

Прежде чем переходить к атакам — что нужно поставить.

Требования к окружению:

  • ОС: Linux (Kali 2023+ рекомендуется), macOS, Windows с WSL2
  • RAM: 4 ГБ минимум; 8 ГБ для hashcat с GPU-ускорением
  • Python: 3.8+
  • jwt_tool — GitHub: ticarpi/jwt_tool, активно поддерживается, >5000 звёзд. Установка: git clone https://github.com/ticarpi/jwt_tool && pip3 install -r requirements.txt
  • hashcat 6.2+ — офлайн-брутфорс HMAC-ключей, mode 16500
  • Wordlist: rockyou.txt (в Kali: /usr/share/wordlists/rockyou.txt; отдельно ~140 МБ)
  • CyberChef — веб-интерфейс для ручного декодирования, установка не нужна (gchq.github.io/CyberChef)
  • Burp Suite Community + плагин JWT Editor через BApp Store — перехват и редактирование токенов в HTTP-запросах
Инструмент Задача Когда использовать Ограничения
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).

Alg none атака JWT — подделка токена без подписи

Самая частая JWT-уязвимость в CTF, и самая обидная, когда не проверил первой. Спецификация JWT (RFC 7518) допускает значение "alg": "none" — отсутствие подписи. Если серверная библиотека доверяет значению alg из самого токена и не проверяет алгоритм отдельно — подпись можно убрать, а payload подменить.

[Применимо: CTF уровня Easy/Medium; legacy-реализации JWT без whitelist алгоритмов]

Пошаговая эксплуатация (ручной способ):

  1. Перехватываем JWT из cookie или заголовка Authorization через Burp Suite
  2. Берём первую часть токена (до первой точки) — это header. Декодируем: echo 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9' | tr '_-' '/+' | base64 -d{"typ":"JWT","alg":"HS256"}
  3. Формируем новый header с отключённой подписью: {"typ":"JWT","alg":"none"}
  4. Кодируем в base64url: eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0
  5. Декодируем payload (вторая часть), меняем нужные claims: {"username":"admin","role":"admin"}
  6. Кодируем новый payload в base64url: eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0
  7. Собираем итоговый токен: eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0. — обратите внимание на точку в конце: подпись пуста, но разделитель обязателен
  8. Подставляем в cookie через EditThisCookie, Burp Repeater или curl -H "Cookie: jwt=<token>" <URL>
  9. Если сервер принял — в ответе будет флаг или приветствие администратора

Через jwt_tool весь процесс — одна команда: python3 jwt_tool.py <JWT> -X a. Инструмент автоматически генерирует несколько вариантов токена с разными написаниями none, подставляет их и показывает, какие сервер принял.

Для совсем минималистичного подхода хватит curl: собираем токен руками и отправляем curl -H "Cookie: jwt=eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIn0." -v http://target/ — если в ответе статус 200 и контент изменился, обход авторизации CTF-задачи сработал.

Вариации alg:none и обход фильтров

Согласно OWASP Web Security Testing Guide, если блокировка алгоритма none реализована без учёта регистра, её обходят альтернативными написаниями: None, NONE, nOnE, noNe. jwt_tool при использовании флага -X a перебирает эти варианты автоматически.

Формат signature-части тоже влияет на результат: одни серверы ожидают header.payload. (точка в конце, подпись отсутствует), другие — header.payload (без финальной точки). Если один формат не сработал — пробуем другой. На некоторых CTF-платформах оба формата принимаются, но бывают задачи, где автор жёстко проверяет trailing dot.

Когда alg:none не работает:

  • Современные библиотеки (jsonwebtoken для Node.js >= 9.0, PyJWT >= 2.0, java-jwt) отклоняют none по умолчанию. На CTF уровня Hard автор задания обычно использует актуальную библиотеку с корректной конфигурацией
  • Серверная логика применяет whitelist алгоритмов: разрешён только HS256 или RS256, всё остальное отклоняется
  • Сервер проверяет наличие непустой signature-части независимо от значения alg

Не прошло? Переходим к брутфорсу секрета.

Слабый секрет JWT брутфорс: HS256 через hashcat и jwt_tool

Если сервер корректно отклоняет 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-драйверов и выводит найденный ключ прямо в терминале. Для коротких словарей — за глаза.

После взлома ключа: подделка JWT токена

Получив секрет (допустим, secret123), генерируем новый токен с нужными claims через jwt_tool: python3 jwt_tool.py <JWT> -T -S hs256 -p "secret123". Флаг -T открывает интерактивный режим — меняем role на admin, username на нужное значение, подтверждаем. Инструмент подписывает токен найденным ключом и выводит готовый JWT.

Другой вариант — jwt.io: вставляем токен в веб-интерфейс, редактируем payload в визуальном редакторе, вводим секрет в поле Verify Signature. Подпись пересчитывается автоматически, готовый токен копируем из верхнего поля.

Когда брутфорс не сработает

  • Секрет сгенерирован криптографически стойким ГПСЧ (32+ случайных символа) — подбор нереалистичен даже с мощным GPU-кластером
  • Алгоритм асимметричный (RS256, ES256) — HMAC-брутфорс к ним неприменим, там нет общего секрета
  • JWT использует JWKS-эндпоинт с ротацией ключей — подобранный ключ устареет через минуты
  • Секрет хранится в HSM (Hardware Security Module) — невозможно даже теоретически

Если rockyou.txt не дал результат — проверьте дефолтные ключи конкретного фреймворка: changeme, your-256-bit-secret, supersecretkey, shhhhh, jwt_secret_key. Авторы CTF-задач нередко берут их прямо из документации или Stack Overflow.

JWT авторизация обход: подмена алгоритма RS256 на HS256

[Применимо: 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).

Процесс атаки:

  1. Получить публичный ключ сервера: эндпоинт /.well-known/jwks.json, /jwks, открытые конфиги приложения, или ключ указан в условии задачи. В редких случаях приложение использует тот же ключ для TLS и JWT — тогда его можно вытянуть через openssl s_client -connect target:443 | openssl x509 -pubkey -noout, но обычно TLS-ключ и JWT-ключ — разные сущности
  2. Сохранить ключ в файл public.pem
  3. Запустить: python3 jwt_tool.py <JWT> -X k -pk public.pem
  4. jwt_tool выполнит key confusion атаку: перепишет alg на 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
  • В CTF встречается реже, чем alg:none и слабый секрет, но на соревнованиях уровня Hard — регулярно, особенно в комбинации с jku/x5u-инъекциями

Дерево решений: какую атаку на JWT выбрать в CTF

Вместо того чтобы перебирать все техники вслепую — алгоритм выбора вектора:

  1. Декодируем header → определяем alg
  2. HS256 / HS384 / HS512 (симметричный): - Пробуем alg:none → python3 jwt_tool.py <JWT> -X a - Не сработало → брутфорсим секрет: hashcat -m 16500 <JWT> rockyou.txt - Секрет не словарный → ищем утечку ключа через LFI, SSRF, открытый .env или /config
  3. RS256 / RS384 / RS512 (асимметричный): - Пробуем alg:none (работает так же часто, как с HMAC) - Ищем публичный ключ → пробуем подмену RS256 на HS256: jwt_tool.py <JWT> -X k -pk public.pem - Проверяем параметры jku / x5u в header → JWK spoofing с подменой URL на свой сервер
  4. ES256 / ES384 / ES512 (эллиптические кривые): - Пробуем alg:none - Java-бэкенд → проверяем CVE-2022-21449: подмена подписи на MAYCAQACAQA
  5. Ничего не помогло: - Проверяем параметр kid в 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 комментариев

Пожалуйста, войдите, чтобы оставить комментарий.

Загрузка комментариев...