
Задание Web 300 на CTF: форма логина, кнопка «Войти как гость», cookie с JWT в ответе. Декодируешь payload в CyberChef за 30 секунд: {"username":"guest","role":"user"}. Запускаешь jwt_tool.py -X a, меняешь role на admin — ещё две минуты, и флаг у тебя. Из 180 команд задачу закрыли 12. Разница между ними и остальными — не набор инструментов, а понимание того, как сервер валидирует подпись токена и что происходит, когда в header стоит "alg":"none". Ниже — пошаговый разбор ключевых JWT атак CTF: от декодирования до подделки, с конкретными командами jwt_tool и hashcat, границами применимости каждой техники и decision tree для быстрого выбора вектора.
JWT — три части через точку: header, payload, signature. Первые две — Base64URL-кодированный JSON, третья — результат криптографической операции. Типичный токен из CTF-задания: Подробнее — в нашем обзоре атаки на аутентификацию.
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0In0.OnuZnYMdetcg7AWGV6WURn8CFSfas6AQej4V9M13nsk
Первое действие — декодировать header и payload. В CyberChef рецепт From Base64 с опцией URL-safe. Берём первую часть eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9 — получаем {"typ":"JWT","alg":"HS256"}. Вторую часть eyJ1c2VybmFtZSI6Imd1ZXN0In0 — {"username":"guest"}. Ещё быстрее: jwt_tool.py <token> декодирует обе части и выводит в читаемом виде. В терминале: echo 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9' | base64 -d (иногда нужно дописать padding-символы =, если вывод обрезан).
Base64URL отличается от стандартного Base64: вместо + используется -, вместо / — _, padding = убирается. CyberChef с опцией URL-safe обрабатывает это автоматически.
Что искать в header:
alg — алгоритм подписи. HS256 = симметричный ключ (один секрет для подписи и проверки). RS256 = асимметричный (приватный подписывает, публичный проверяет). none = подписи нет вообще.kid — идентификатор ключа. Если значение подставляется в файловый путь или SQL-запрос без санитизации — у нас инъекция.jku, x5u — URL для загрузки ключей. В CTF редкость, но если есть — серьёзный вектор.Что искать в payload:
role, admin, is_admin, isAdmin — то, что меняем для эскалации привилегий.exp (expiration) — если токен просрочен, сервер отклонит его даже с валидной подписью.username, sub, user_id — для подмены пользователя.Signature (третья часть) — результат применения алгоритма из header к строке base64url(header).base64url(payload) с секретным ключом. Для HS256 это HMAC-SHA256(header.payload, secret). Для RS256 — RSA-SHA256(header.payload, private_key). От того, как сервер проверяет подпись, зависит, какие атаки сработают.
Подделка JWT-токена в контексте MITRE ATT&CK затрагивает несколько тактик:
По OWASP Top 10 атаки на JWT затрагивают Broken Access Control (A01:2021), Cryptographic Failures (A02:2021) и Security Misconfiguration (A05:2021).
На CTF цепочка обычно линейна: получение токена → декодирование → определение вектора → подделка → отправка → флаг. На реальном пентесте JWT-атака даёт initial foothold или privilege escalation, после чего идёт lateral movement и persistence. Понимание позиции в kill chain определяет, какие данные нужны для атаки и что искать дальше после получения доступа.
| Критерий | jwt_tool | hashcat | CyberChef |
|---|---|---|---|
| Преимущества | All-in-one: decode, forge, scan, crack | GPU-ускорение для крупных словарей | Мгновенный визуальный decode в браузере |
| Ограничения | CPU-only cracking, медленнее hashcat | Только crack, не умеет forge/tamper | Нет автоматизации атак |
| Когда использовать | Первый инструмент на любом JWT-таске | Словари >100 MB, сложные маски | Быстрый анализ структуры без установки |
| Когда не использовать | Полный перебор rockyou.txt на скорость | Когда нужна подделка, а не подбор | Автоматизированное тестирование |
jwt_tool (GitHub: ticarpi/jwt_tool, активно поддерживается) — основной инструмент. Умеет декодировать (-d), подделывать claims (-I), автоматически сканировать уязвимости (-M at), крякать секрет (-C), атаки alg:none (-X a), algorithm confusion (-S), kid injection. На CTF начинаю с jwt_tool.py <token> — мгновенный decode header и payload.
hashcat — подключается для офлайн-подбора HMAC-секрета с GPU. Режим -m 16500 для JWT. На GPU уровня RTX 3060 перебор rockyou.txt (14 млн записей) — секунды вместо минут на CPU.
CyberChef — декодирование токена без установки Python. Рецепт: From Base64 (URL safe). Работает в браузере, хватает для первичного анализа.
Для практики JWT-тасков есть бесплатные лабы: PortSwigger Web Security Academy (серия JWT-лабов с нарастающей сложностью), root-me.org (JWT Introduction — задание на alg:none), jwt.io для ручного декодирования и экспериментов с подписью.
На CTF время — главный ресурс. Decision tree, оптимизированный по скорости проверки:
| Условие | Вектор | Инструмент |
|---|---|---|
| Подпись не проверяется вообще | Прямая модификация payload | jwt_tool <JWT> -I -pc role -pv admin |
| Сервер принимает alg:none | Удаление подписи | jwt_tool <JWT> -X a |
| HS256 + подозрение на слабый секрет | Брутфорс HMAC-ключа | hashcat -m 16500 / jwt_tool -C |
| RS256 + доступен публичный ключ | Algorithm confusion RS256→HS256 | jwt_tool <JWT> -S hs256 -k public.pem |
| kid в header + предположение LFI | Path traversal через kid | jwt_tool -I -hc kid -hv "../../dev/null" |
| ES256 + Java 15-18 на бэкенде | Psychic signatures CVE-2022-21449 | Замена подписи на 64 нулевых байта (Base64URL) для JWT или MAYCAQACAQA для TLS |
Порядок проверки: (1) изменить payload без смены подписи — если сервер принял, подпись не проверяется; (2) alg:none — самый быстрый вектор; (3) crack с коротким словарём (top-1000); (4) algorithm confusion при RS256; (5) kid injection при наличии kid в header. Порядок идёт от секундных проверок к минутным.
git clone https://github.com/ticarpi/jwt_tool && python3 -m venv venv && source venv/bin/activate && pip install -r requirements.txt./usr/share/wordlists/rockyou.txt.gz.Алгоритм none определён в RFC 7519 и означает отсутствие подписи. Если сервер читает значение alg из header токена и принимает none без ограничений, проверка подписи пропускается целиком. По OWASP Web Security Testing Guide, это одна из первых проверок при тестировании JWT — сервер обязан явно отклонять токены с алгоритмом none, но многие кастомные реализации этого не делают.
Исходный токен после гостевого входа: eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0In0.OnuZnYMdetcg7AWGV6WURn8CFSfas6AQej4V9M13nsk
Header: {"typ":"JWT","alg":"HS256"}. Payload: {"username":"guest"}. Цель — войти как admin.
Флаг -X a генерирует несколько вариантов none-атаки: none, None, NONE, nOnE и другие case-вариации — для обхода фильтров, проверяющих алгоритм регистрозависимо:
python3 jwt_tool.py eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0In0.OnuZnYMdetcg7AWGV6WURn8CFSfas6AQej4V9M13nsk -X a
jwt_tool выдаст набор токенов-кандидатов. Каждый подставляем в cookie (через EditThisCookie, Burp Repeater или curl -H "cookie: jwt=...") и отправляем запрос. Для одновременной подмены username на admin: jwt_tool.py <JWT> -I -pc username -pv admin в комбинации с -X a.
Ручная подделка без инструмента: header {"typ":"JWT","alg":"none"} → Base64URL eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0; payload {"username":"admin"} → eyJ1c2VybmFtZSI6ImFkbWluIn0; итоговый токен: eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIn0. — с точкой в конце, без подписи. Именно так решается JWT Introduction на root-me.org, где флаг — S1gn4tuR3_v3r1f1c4t10N_1S_1MP0Rt4n7.
Работает если: сервер не имеет whitelist допустимых алгоритмов; серверная библиотека при alg=none пропускает проверку подписи; фильтр проверяет none регистрозависимо (тогда nOnE пройдёт).
Не работает если: сервер игнорирует alg из токена и использует фиксированный алгоритм из конфигурации; библиотека явно блокирует none — PyJWT, jose4j, jsonwebtoken v9+ делают это по умолчанию; case-insensitive фильтр блокирует все вариации.
В CTF alg:none — частый вектор на Easy-Medium заданиях. На реальных пентестах встречается реже, но по данным PortSwigger Web Security Academy, проверять этот вектор стоит всегда — затраты минимальны, а потенциальный импакт максимален.
Если сервер использует HS256 и секретный ключ JWT словарный (password, secret, admin123), его можно подобрать. Подпись HS256 — HMAC-SHA256(base64url(header).base64url(payload), secret). Зная содержимое токена и подпись, перебираем секреты до совпадения. В терминах MITRE ATT&CK — Password Cracking (T1110.002): та же логика, что при подборе хешей паролей.
jwt_tool имеет встроенный режим crack: jwt_tool.py <JWT> -C -d <словарь>. Для быстрой проверки хватает top-1000 паролей. Если не помогло — подключаем hashcat. Весь JWT (три части через точки) сохраняем в файл jwt.txt:
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt --force
Режим -m 16500 — JWT (HMAC-SHA256/384/512). Флаг --force запускает перебор на CPU, если нет совместимого GPU. После успешного подбора hashcat выведет результат в формате <jwt>:<secret>. Теперь подписываем произвольный токен: jwt_tool.py <JWT> -I -pc role -pv admin -S hs256 -p "<найденный_секрет>".
На CTF типичные слабые секреты: secret, password, admin, test, key, 12345678, changeme, supersecret. Проверьте эти строки вручную через jwt_tool.py <JWT> -C -p "secret" — часто секрет находится за 10 секунд, без запуска полного словаря. На одном CTF я потратил 20 минут на rockyou.txt, а секрет оказался jwt_secret. Иногда стоит подумать головой, прежде чем запускать перебор.
Если словарь не помог — проверьте, не утёк ли секрет через другой вектор: исходники на GitHub, .env через path traversal, config.js в публичных assets, комментарии в HTML-коде.
Работает если: секрет короткий (<15 символов) и словарный; алгоритм HS256/HS384/HS512.
Не работает если: секрет длиннее 32 символов и криптографически случайный; секрет — UUID или hex-строка, отсутствующая в стандартных словарях; используется key derivation function поверх основного секрета.
Время перебора: rockyou.txt на CPU — 2-5 минут; на GPU RTX 3060 — под 10 секунд. Для CTF этого достаточно. Если задание подразумевает brute-force, секрет обычно попадает в первые 10 000 строк rockyou.txt.
Algorithm confusion — атака на уровень сложнее, чем alg:none. Сервер ожидает RS256 (асимметричный: приватный ключ подписывает, публичный проверяет), но атакующий подменяет алгоритм на HS256 (симметричный). Если серверная библиотека использует единую функцию verify() и не фиксирует алгоритм жёстко, она возьмёт публичный ключ RS256 как HMAC-secret. Красиво, правда?
По данным PortSwigger Web Security Academy, Node.js библиотека jsonwebtoken в ранних версиях использовала одну функцию jwt.verify(token, key) для обоих типов алгоритмов — algorithm confusion эксплуатировалась тривиально при доступе к публичному ключу.
Где искать публичный ключ: endpoint /.well-known/jwks.json; TLS-сертификат сервера (редко совпадает с JWT-signing key — работает только если приложение переиспользует TLS-ключ для подписи JWT); утечки в исходном коде; claim pk прямо в payload токена (да, бывает и такое).
Команда: jwt_tool.py <JWT> -S hs256 -k public.pem — подписывает модифицированный токен алгоритмом HS256, используя содержимое public.pem как HMAC-secret. Перед этим нужно сохранить публичный ключ сервера в файл.
Работает если: библиотека использует единую verify-функцию для всех алгоритмов; публичный ключ доступен; сервер не проверяет alg против whitelist.
Не работает если: библиотека разделяет функции для RSA и HMAC (современные версии PyJWT, jose4j); сервер фиксирует алгоритм в конфигурации и игнорирует header; публичный ключ недоступен.
В CTF algorithm confusion — задания Medium-Hard. Ключевой признак: RS256 в header. Увидели RS256 — сразу ищите публичный ключ.
kid (Key ID) — необязательный параметр header, указывающий серверу, какой ключ использовать для проверки подписи. Если сервер подставляет значение kid в путь к файлу или SQL-запрос без санитизации — у нас инъекция.
Path traversal через kid. Классический вариант: kid указывает на файл с ключом. Подставляем ../../dev/null — файл, который в Unix всегда возвращает пустую строку. Если сервер использует содержимое файла как ключ, HMAC-подпись с пустым секретом будет валидна: jwt_tool.py <JWT> -I -hc kid -hv "../../dev/null" -S hs256 -p "". Альтернатива — любой файл с предсказуемым содержимым (CSS, robots.txt): jwt_tool.py <JWT> -I -hc kid -hv "path/to/known/file" -S hs256 -p "содержимое файла".
SQL-инъекция через kid. Если kid извлекается через SQL-запрос, payload 1' UNION SELECT 'known_secret' -- заставит сервер вернуть known_secret как ключ: jwt_tool.py <JWT> -I -hc kid -hv "1' UNION SELECT 'known_secret' -- " -S hs256 -p "known_secret". Сервер выполнит запрос, вернёт подставленное значение как ключ, и подпись с тем же секретом пройдёт валидацию.
Работает если: kid присутствует в header; значение подставляется без валидации в файловый путь или SQL-запрос.
Не работает если: kid проверяется по whitelist; ключи хранятся в HSM или key vault; SQL-запрос использует параметризованные выражения; фильтруются спецсимволы в kid.
В CTF kid injection — уровень Hard. Часто комбинируется с другими уязвимостями: kid + SSRF для доступа к внутреннему ключу, kid + LFI для чтения конфигурации.
CTF-авторы нередко моделируют реальные json web token уязвимости. Две CVE особенно актуальны.
CVSS 7.5 (HIGH), вектор CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:N/I:H/A:N. CWE-347 — Improper Verification of Cryptographic Signature.
Java некорректно валидировала ECDSA-подписи: для TLS-контекста (ASN.1 DER) достаточно заменить подпись на MAYCAQACAQA; для JWT ES256 (формат r||s по RFC 7518 §3.4) — на 64 нулевых байта в Base64URL — и подпись считается валидной для любого payload. OWASP WSTG называет это «psychic signatures». EPSS score 0.4824, percentile 0.9873 — Top 5% по вероятности эксплуатации. Публичный PoC: notkmhn/CVE-2022-21449-TLS-PoC на GitHub (121 звезда).
В TLS/X.509-контексте (ASN.1 DER) blank-подпись имеет вид MAYCAQACAQA (как в notkmhn/CVE-2022-21449-TLS-PoC). Для JWT ES256 формат подписи другой: RFC 7518 §3.4 требует raw concatenation (r||s) — два 32-байтовых нулевых значения, что в Base64URL даёт строку из 86 символов A. Пример подделанного JWT: eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6InRydWUifQ.AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA — header задаёт ES256, payload содержит admin: true, подпись — 64 нулевых байта.
Уязвимость в библиотеке Cisco node-jose до версии 0.11.0. CVSS 7.5 (HIGH), CWE-347. Атакующий встраивает произвольную пару ключей в header через параметр jwk, и библиотека использует этот ключ для проверки вместо серверного. По данным Intigriti, это type confusion: атакующий подписывает токен собственным ключом и прикладывает этот ключ к токену — сервер доверяет. EPSS score 0.4265, percentile 0.9856 — тоже Top 5%. Публичный PoC: zi0Black/POC-CVE-2018-0114 (26 звёзд на GitHub), зарегистрирован в Exploit-DB (EDB-44324).
Обе CVE объединяет CWE-347 — некорректная проверка криптографической подписи. В CTF они появляются в заданиях, где нужно идентифицировать конкретную уязвимую технологию на бэкенде по косвенным признакам: версия Java в заголовках ответа, библиотека в package.json.
Не все JWT-задания решаются описанными атаками. Вот что часто сбивает с толку.
Фиксированный алгоритм на стороне сервера. Сервер игнорирует alg из header и проверяет подпись алгоритмом из конфигурации — ни alg:none, ни algorithm confusion не сработают. Признак: любые изменения alg приводят к одной и той же ошибке. Стена.
Проверка exp/nbf claims. Даже с валидной подписью токен отклоняется, если exp указывает на прошедшее время. Добавляем -pc exp -pv <timestamp_в_будущем> при подделке. Забыть про это — классика.
Двойная валидация. Сервер проверяет подпись, затем ищет пользователя в базе. Если admin не существует как запись — подмена claims бесполезна.
Refresh tokens. Два токена: access (короткоживущий) и refresh. Атака на access бесполезна, если refresh валидируется отдельно.
Кастомные алгоритмы. Нестандартный алгоритм подписи, который jwt_tool не поддерживает — придётся писать PoC на Python руками.
Если alg:none, brute-force и algorithm confusion не сработали — перечитайте описание задания. Подсказка к вектору часто скрыта в названии таска, HTML-комментариях или endpoint, возвращающем фрагмент конфигурации.
Authorization: Bearer, URL-параметры, localStorage.jwt_tool.py <JWT> или CyberChef (From Base64 URL safe).jwt_tool.py <JWT> -X a.jwt_tool.py <JWT> -C -d rockyou.txt.../../dev/null) и SQL-инъекция.jwt_tool.py <JWT> -I -pc <claim> -pv <value> с соответствующим методом подписи.Большинство CTF-райтапов по JWT сводятся к формуле «скопируй команду jwt_tool — получи флаг». На Easy-заданиях это работает. На соревнованиях уровня выше Medium стандартные атаки alg:none и словарный брутфорс всё чаще закрыты: авторы тасков читают те же райтапы и намеренно блокируют очевидные вектора.
Разница между теми, кто решает Hard-задания, и остальными — не набор инструментов. Она в понимании «под капотом»: почему сервер доверяет значению alg из header, почему HMAC-подпись с публичным ключом проходит валидацию, почему /dev/null как kid позволяет подписать токен пустым секретом. Без этого каждое нестандартное задание — стена.
За последние пару лет CTF-задания по JWT сместились от «подставь none и получи флаг» к комбинированным сценариям: kid injection + SSRF для получения внутреннего ключа, algorithm confusion + утечка через отдельный endpoint, кастомные алгоритмы подписи поверх стандартного формата. Автоматизация через jwt_tool перестаёт быть достаточной — нужно разбираться в RFC 7519, понимать реализации конкретных библиотек и уметь собирать PoC руками. Если хочешь не просто writeup, а пройти всю атаку самому — на WAPT эту цепочку проходят в течение двух модулей с лабами.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...