
Hashcat в режиме 16500 прогоняет rockyou.txt — 14 миллионов паролей — за секунды на GPU уровня RTX 3060. Если автор CTF-задачи подписал JWT словарным секретом вроде secret или password, флаг окажется в терминале раньше, чем соседняя команда допьёт кофе. Но брутфорс — лишь один из пяти рабочих векторов.
JWT уязвимости в CTF попадают сразу под три категории OWASP Top 10 2021: A01 Broken Access Control — подмена claims для эскалации привилегий, A02 Cryptographic Failures — слабая криптография и предсказуемые ключи, A05 Security Misconfiguration — принятие алгоритма none без валидации подписи. Ниже — пять конкретных атак с командами jwt_tool и hashcat, порядок действий от перехвата токена до флага, таблица предусловий для каждого вектора и разбор ситуаций, когда стандартные техники не срабатывают.
JSON Web Token — строка из трёх частей, разделённых точками: header.payload.signature. Каждая часть закодирована в base64url — вариант base64 с заменой + на -, / на _ и без = на конце. Строка токена всегда начинается с eyJ — base64url-кодировка открывающей фигурной скобки JSON-объекта. Увидели eyJ в cookie или заголовке — перед вами JWT. Не ошибётесь. Подробнее — в нашем обзоре создание ctf заданий.
Прежде чем атаковать, токен нужно найти. В веб-задачах JWT в CTF прячется в одном из четырёх мест:
jwt, token, session или кастомное имя. Видны во вкладке Storage в DevTools или через перехват в Burp Suite Proxy.Authorization — формат Bearer eyJ0eXAi.... Ловится в Burp Proxy или вкладке Network.access_token или token.console.log(localStorage) в DevTools.Первый шаг — перехватить любой аутентифицированный запрос и найти строку из трёх частей, начинающуюся с eyJ. Если токен не нашёлся в очевидных местах, проверьте ответ эндпоинта /login или /api/auth — бывает, JWT приходит только в теле ответа, а клиентский JavaScript руками помещает его в header последующих запросов.
Декодировать JWT можно на jwt.io (визуальный редактор с проверкой подписи), через CyberChef от GCHQ или командой в терминале:
echo '<часть>' | tr '_-' '/+' | base64 -d
Типичный header CTF-задачи: {"typ":"JWT","alg":"HS256"}. Поле alg — первое, на что смотрим. HS256 — симметричный HMAC-SHA256: один и тот же секрет для создания и проверки подписи. RS256 — асимметричный RSA-SHA256: приватный ключ подписывает, публичный проверяет. Значение none означает отсутствие подписи — и это первый вектор атаки.
В payload ищем кастомные claims, контролирующие привилегии: "role":"user", "isAdmin":false, "username":"guest". Стандартные claims по RFC 7519 — iat (время создания), exp (время истечения), sub (идентификатор субъекта) — тоже критичны: токен с exp в прошлом сервер отклонит, и придётся генерировать свежий.
Payload не шифруется — кто угодно прочитает содержимое через base64url-декодирование. Вся безопасность JWT держится на подписи. Для HS256 она вычисляется как HMAC-SHA256(base64url(header) + "." + base64url(payload), SECRET_KEY). Изменение одного байта в header или payload ломает подпись — если сервер корректно её проверяет. Вот это «если» и делает JWT таким благодатным вектором на CTF.
Зачем атакующему взлом JWT токена? JWT — «паспорт» пользователя в приложении. Подделав claims, атакующий получает чужую сессию, повышает привилегии до admin или обходит access control целиком. По данным Verizon DBIR 2025, 26% подтверждённых нарушений — веб-атаки, и обход аутентификации JWT — один из типовых сценариев. В MITRE ATT&CK подделка JWT образует цепочку: Exploit Public-Facing Application (T1190, Initial Access) — находим уязвимость в обработке токенов, Web Cookies (T1606.001, Credential Access) — создаём или модифицируем токен для захвата сессии, Application Access Token (T1550.001, Lateral Movement) — используем подделанный токен для доступа к защищённым ресурсам.
Самая быстрая эксплуатация JWT в CTF — и самая обидная, когда не проверили первой. Спецификация JWT (RFC 7518) допускает значение "alg":"none" — отсутствие подписи. Если серверная библиотека доверяет полю alg из самого токена и не сверяет его с whitelist допустимых алгоритмов, подпись можно убрать, а payload подменить. Как отметил Tim McLean в анализе на Auth0: «Unfortunately, some libraries treated tokens signed with the none algorithm as a valid token with a verified signature.»
По сути — мы говорим серверу «я без подписи, и так поверь». И он верит.
Декодируем header:
echo 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9' | tr '_-' '/+' | base64 -d
# {"typ":"JWT","alg":"HS256"}
Меняем alg на none, кодируем обратно в base64url. Аналогично в payload: "role":"user" заменяем на "role":"admin", кодируем. Собираем итоговый токен:
eyJ0eXAiOiJKV1QiLCJhbGciOiJub25lIn0.eyJ1c2VybmFtZSI6ImFkbWluIiwicm9sZSI6ImFkbWluIn0.
Точка в конце — подпись пуста, но разделитель обязателен. Некоторые серверы принимают токен без trailing dot (header.payload), другие — только с ним. Если один вариант вернул 401 — пробуйте второй. Отправляем через curl -H "Cookie: jwt=<token>" <target_url> и смотрим ответ: статус 200 и изменённый контент — обход аутентификации JWT сработал.
Одна команда закрывает всю алгоритм none JWT уязвимость:
python3 jwt_tool.py <JWT> -X a
Инструмент автоматически генерирует токены с вариациями none: стандартное написание, None, NONE, nOnE, noNe. Серверы с неполной фильтрацией по регистру часто пропускают одну из вариаций — по данным OWASP Web Security Testing Guide, обход через case-sensitive проверку остаётся типовой ошибкой реализации. На практике nOnE срабатывает чаще, чем хотелось бы.
Современные библиотеки блокируют none по умолчанию: PyJWT 2.x+, jsonwebtoken 9.x для Node.js, java-jwt от Auth0 — все отклоняют неподписанные токены. На CTF уровня Hard автор задачи обычно использует актуальную библиотеку с жёстким whitelist алгоритмов.
Признаки: сервер возвращает 401 или 403 на все вариации, в ответе — явная ошибка ("Invalid algorithm" или "Signature verification failed"). Но не спешите сдаваться: некоторые серверы отклоняют none, но принимают токен с произвольной подписью (фактически не проверяя её) — попробуйте python3 jwt_tool.py <JWT> -I -pc role -pv admin без изменения алгоритма. Если и это не прошло — переходим к подбору секрета JWT.
Если сервер корректно отклоняет none, но использует HS256 с коротким или словарным секретом — ключ подбирается офлайн за секунды. По MITRE ATT&CK это Password Cracking (T1110.002, Credential Access). Atomic Red Team включает тест «Password Cracking with Hashcat» для верификации детектирования этой техники.
В CTF слабые секреты закладываются авторами намеренно: secret, password, admin123, iloveyou, your-256-bit-secret (дефолт jwt.io — удивительно, сколько раз он попадался на соревнованиях). В реальных приложениях ситуация не сильно лучше — по данным IBM X-Force Threat Intelligence Index 2025, рост атак с использованием действительных учётных данных составил 71% год к году, и предсказуемые секреты остаются нормой.
hashcat -a 0 -m 16500 \
"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6Imd1ZXN0In0.ZvkYYnyM929FM4NW9_hSis7_x3_9rymsDAx9yuOcc1I" \
/usr/share/wordlists/rockyou.txt
Флаг -a 0 — атака по словарю, -m 16500 — режим JWT для HS256/HS384/HS512. На GPU уровня RTX 3060 перебор rockyou.txt (14 миллионов записей) занимает секунды. На CPU — от нескольких минут до получаса в зависимости от железа. Успешный результат выводится в формате <JWT>:найденный_секрет.
Если rockyou.txt не дал результата — пробуйте кастомные списки: дефолтные значения из документации фреймворков (changeme, supersecretkey, jwt_secret), вариации имени приложения и его домена, стандартные значения из GitHub-репозиториев проекта. На одном CTF секрет оказался названием самой задачи в нижнем регистре — такие вещи словари не покрывают, но голова должна.
python3 jwt_tool.py <JWT> -C -d /usr/share/wordlists/rockyou.txt
Флаг -C запускает режим crack. Работает медленнее hashcat, но не требует CUDA/ROCm-драйверов и выводит найденный ключ прямо в терминал. Для коротких кастомных словарей до 100К записей — хватает за глаза. Для проверки одного конкретного секрета: python3 jwt_tool.py <JWT> -C -p "secret123".
Получив секрет (допустим, secret123), генерируем токен с нужными claims:
python3 jwt_tool.py <JWT> -T -S hs256 -p "secret123"
Флаг -T открывает интерактивный режим — меняем role на admin, username на нужное значение, подтверждаем. Инструмент подписывает модифицированный payload найденным ключом и выводит готовый JWT для подстановки в запрос.
Альтернативный путь — jwt.io: вставляем оригинальный токен, редактируем payload в визуальном редакторе, вводим секрет в поле «Verify Signature» — декодирование и подпись происходят в реальном времени. Удобно для быстрого прототипирования, когда лень возвращаться в терминал.
Когда ни none, ни брутфорс секрета не дали результата, а в header стоит RS256 — время для confusion-атаки JWT. Эта подделка JWT токенов эксплуатирует фундаментальную проблему: сервер берёт значение alg из заголовка токена вместо того, чтобы проверять его по whitelist.
RS256 использует приватный ключ для подписи и публичный для верификации. HS256 использует один и тот же секрет для обеих операций. Если сервер видит "alg":"HS256" в header, он применит для проверки тот же ключ, что использовал бы для RS256 — публичный. А публичный ключ, по определению, доступен всем. Атакующий скачивает его, подписывает произвольный payload через HS256 с публичным ключом в качестве секрета — и сервер принимает токен как валидный.
Перечитайте ещё раз. Сервер сам отдаёт ключ, которым можно подписать любой токен. Красиво, правда?
Как описал Tim McLean в исследовании на Auth0: «If a server is expecting a token signed with RSA, but actually receives a token signed with HMAC, it will think the public key is actually an HMAC secret key.» Confusion-атака затронула множество JWT-библиотек: node-jsonwebtoken, pyjwt, namshi/jose, php-jwt — все до выпуска фиксов были уязвимы. Рекомендация Auth0: библиотека обязана принимать параметр algorithm явно в функции верификации, а не тянуть его из header.
В CTF-задачах публичный ключ обычно доступен по одному из эндпоинтов: /.well-known/jwks.json (стандартный путь по RFC 7517), /jwks, /api/v1/jwks или кастомные вариации. Иногда ключ лежит в исходниках приложения — если задача даёт доступ к коду или Dockerfile. Реже — в claims самого токена (поле pk в payload). По данным PortSwigger Web Security Academy, публичные ключи по определению не секрет, поэтому их намеренно размещают на доступных эндпоинтах.
Получив публичный ключ в формате PEM (файл public.pem):
python3 jwt_tool.py <JWT> -S hs256 -k public.pem
Для одновременной модификации claims:
python3 jwt_tool.py <JWT> -I -pc role -pv admin -S hs256 -k public.pem
Инструмент меняет alg на HS256, модифицирует payload, подписывает публичным ключом как HMAC-секретом и выводит готовый токен.
Строка ключа должна совпадать с серверной байт в байт. Лишний перевод строки в конце файла, отсутствующий \n между -----BEGIN PUBLIC KEY----- и самим ключом, разница в кодировке (UTF-8 vs ASCII) — всё ломает подпись. Я на этом терял по 20 минут не раз. Если confusion не срабатывает при правильном ключе, проверьте файл: file public.pem покажет кодировку, xxd public.pem | tail — наличие trailing bytes. Иногда помогает пересохранение: openssl rsa -in public.pem -pubin -out clean.pem.
На CTF уровня Hard и в реальных уязвимостях JWT аутентификации встречаются атаки через параметры header, выходящие за рамки alg. Эти параметры управляют тем, какой ключ сервер использует для проверки подписи — и если они инъектируемы, атакующий контролирует процесс верификации целиком.
Key ID (kid) — необязательный параметр header, указывающий серверу какой ключ использовать для верификации. Если значение kid подставляется в файловый путь или SQL-запрос без санитизации, открываются два вектора.
Path traversal: значение kid указывает на файл с предсказуемым содержимым. /dev/null в Unix всегда возвращает пустую строку — подписываем токен пустым ключом:
python3 jwt_tool.py <JWT> -I -hc kid -hv "../../dev/null" -S hs256 -p ""
Сервер читает «ключ» из /dev/null, получает пустоту, проверяет подпись с пустым секретом — подпись совпадает. Элегантно. Альтернатива — любой файл с известным содержимым в корне веб-сервера (CSS, JS): python3 jwt_tool.py <JWT> -I -hc kid -hv "путь/к/файлу" -S hs256 -p "содержимое_файла".
SQL-инъекция: если kid подставляется в запрос к БД для выбора ключа, классическая инъекция позволяет контролировать возвращаемое значение:
python3 jwt_tool.py <JWT> -I -pc role -pv admin -hc kid -hv "' UNION SELECT 'known_value'--" -S hs256 -p "known_value"
Подписываем токен известной строкой и делаем так, чтобы SQL вернул ту же строку. Два в одном — и SQLi, и подделка JWT.
Параметр jwk в header позволяет встроить публичный ключ прямо в токен. Уязвимая библиотека Cisco node-jose версий до 0.11.0 (CVE-2018-0114, 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) доверяла ключу из header для верификации подписи. Интерпретация вектора: атака доступна по сети (AV:N), не требует ни привилегий (PR:N), ни действий пользователя (UI:N), при этом полностью компрометирует целостность (I:H).
Атакующий генерировал произвольную пару RSA-ключей, подписывал токен своим приватным ключом, вставлял публичный в header через параметр jwk — и сервер принимал подпись как валидную. По сути — «вот мой паспорт, я сам его нарисовал, но вы же видите — печать на месте». По данным CISA-ADP, для CVE-2018-0114 существует публичный PoC (SSVC decision: Track*, exploitation: poc, automatable: yes), а EPSS-оценка составляет 0.4265 (98-й перцентиль — Top 5% по вероятности эксплуатации). Публичный PoC-репозиторий: zi0Black/POC-CVE-2018-0114 на GitHub. Исправление — обновление node-jose до 0.11.0+.
Параметр jku (JSON Web Key Set URL) указывает на внешний URL с набором публичных ключей в формате JWKS. Если сервер не валидирует домен jku по whitelist, атакующий размещает свой JWKS на контролируемом сервере, подписывает токен собственным ключом и указывает jku на свой URL. Для обхода фильтров домена используют: open redirect на целевом сервере, добавление @attacker.com после hostname или поддомен формата trusted.com.attacker.com. Инструмент jwt_tool при первом запуске создаёт файл jwttool_custom_jwks.json — готовый набор ключей для этой атаки.
Не все атаки работают на каждой задаче — и тратить 20 минут на confusion, когда секрет был password, обидно. Вот порядок проверки, экономящий время на соревновании:
alg и наличие дополнительных параметров (kid, jku, jwk).alg = HS256, пробуем none (самое быстрое): python3 jwt_tool.py <JWT> -X a.none заблокирован — брутфорс секрета: hashcat -a 0 -m 16500 <JWT> rockyou.txt.alg = RS256, ищем публичный ключ и пробуем confusion RS256 → HS256.kid — тестируем path traversal и SQL-инъекцию.jku или jwk — пробуем подмену ключей.| Атака | Предусловие | Когда не работает |
|---|---|---|
| alg: none | Библиотека доверяет alg из токена | PyJWT >= 2.x, jsonwebtoken >= 9.x с whitelist |
| Брутфорс секрета | HS256 + словарный или короткий секрет | Длинный случайный секрет >= 256 бит |
| RS256 → HS256 confusion | Сервер принимает alg из токена + публичный ключ доступен | Явный whitelist алгоритмов |
| kid path traversal | kid подставляется в filesystem path | Валидация kid, параметризация |
| kid SQL injection | kid подставляется в SQL-запрос | Prepared statements, ORM |
| JWK spoofing | Библиотека доверяет jwk из header | node-jose >= 0.11.0 и аналоги |
| jku redirect | Сервер не валидирует домен jku | Whitelist доменов для jku |
Минимальный набор: jwt_tool (установка: git clone https://github.com/ticarpi/jwt_tool && pip3 install -r requirements.txt), hashcat (предустановлен в Kali Linux), curl для отправки запросов и jwt.io для визуального декодирования. В Burp Suite плагин JSON Web Tokens позволяет декодировать и модифицировать токены прямо в перехваченных запросах — удобно для задач, где нужно быстро перебрать варианты claims без выхода из прокси.
Каждый вектор из чек-листа соответствует конкретной тактике MITRE ATT&CK: перехват и подделка токена — Web Cookies (T1606.001, Credential Access), брутфорс секрета — Password Cracking (T1110.002, Credential Access), эксплуатация ошибки в обработке JWT — Exploit Public-Facing Application (T1190, Initial Access). Со стороны защиты MITRE D3FEND рекомендует: Credential Hardening (D3-CH) — усиление хранения и проверки учётных данных, Authentication Cache Invalidation (D3-ANCI) — инвалидация кешированных сессий при аномалиях, Credential Transmission Scoping (D3-CTS) — ограничение области передачи credentials. OWASP Web Security Testing Guide рекомендует: принудительный whitelist алгоритмов на сервере, использование длинных случайных секретов (минимум 256 бит для HS256), обязательную проверку exp и iat claims, отказ от хранения чувствительных данных в payload без шифрования (JWE).
За последние полтора года я решил порядка тридцати CTF-задач на JWT — от Easy на Root-me до финальных задач региональных соревнований. Вывод однозначный: примерно 80% задач закрываются первыми тремя техниками из чек-листа (none, брутфорс, confusion), а оставшиеся 20% — kid-инъекции и JWK/jku-подмены — почти всегда приходятся на уровень Hard. Проблема большинства русскоязычных разборов — они останавливаются на трёх базовых техниках и не объясняют, что делать, когда ни одна из них не сработала.
И вот что ещё: мало кто разбирает, почему конкретная атака провалилась. Сервер вернул 401 на none? Причин минимум три: whitelist алгоритмов, проверка длины подписи или case-sensitive фильтр. Каждая причина — отдельный вектор обхода, и именно это отличает решённую задачу от зависания на ней полчаса. jwt_tool — отличный инструмент, но без понимания криптографии за ним вы встанете на первой нестандартной задаче, когда автор использует кастомную обёртку поверх библиотеки или нестандартный формат ключа. Если хочешь не просто writeup прочитать, а пройти всю атаку руками — на WAPT есть лаба на каждый из этих кейсов.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...