Главная / Блог / JWT уязвимости в CTF: none, брутфорс и confusion

14 мин.00

JWT уязвимости в CTF: none, брутфорс и confusion

JWT уязвимости в CTF: none, брутфорс и confusion

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, порядок действий от перехвата токена до флага, таблица предусловий для каждого вектора и разбор ситуаций, когда стандартные техники не срабатывают.

Структура JWT: что нужно видеть перед атакой

JSON Web Token — строка из трёх частей, разделённых точками: header.payload.signature. Каждая часть закодирована в base64url — вариант base64 с заменой + на -, / на _ и без = на конце. Строка токена всегда начинается с eyJ — base64url-кодировка открывающей фигурной скобки JSON-объекта. Увидели eyJ в cookie или заголовке — перед вами JWT. Не ошибётесь. Подробнее — в нашем обзоре создание ctf заданий.

Где искать токен в CTF-задаче

Прежде чем атаковать, токен нужно найти. В веб-задачах JWT в CTF прячется в одном из четырёх мест:

  • Cookie — поля jwt, token, session или кастомное имя. Видны во вкладке Storage в DevTools или через перехват в Burp Suite Proxy.
  • Заголовок Authorization — формат Bearer eyJ0eXAi.... Ловится в Burp Proxy или вкладке Network.
  • Тело ответа API — сервер возвращает токен в JSON при аутентификации, обычно в поле access_token или token.
  • DOM-хранилище — localStorage и sessionStorage в SPA-приложениях. Достаётся через console.log(localStorage) в DevTools.

Первый шаг — перехватить любой аутентифицированный запрос и найти строку из трёх частей, начинающуюся с eyJ. Если токен не нашёлся в очевидных местах, проверьте ответ эндпоинта /login или /api/auth — бывает, JWT приходит только в теле ответа, а клиентский JavaScript руками помещает его в header последующих запросов.

Разбор header и payload

Декодировать 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 токена? 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) — используем подделанный токен для доступа к защищённым ресурсам.

Атака на алгоритм none JWT: полный обход подписи

Самая быстрая эксплуатация 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 сработал.

Автоматизация через jwt_tool

Одна команда закрывает всю алгоритм none JWT уязвимость:

python3 jwt_tool.py <JWT> -X a

Инструмент автоматически генерирует токены с вариациями none: стандартное написание, None, NONE, nOnE, noNe. Серверы с неполной фильтрацией по регистру часто пропускают одну из вариаций — по данным OWASP Web Security Testing Guide, обход через case-sensitive проверку остаётся типовой ошибкой реализации. На практике nOnE срабатывает чаще, чем хотелось бы.

Когда 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.

Подбор секрета JWT: hashcat и jwt_tool

Если сервер корректно отклоняет 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: GPU-ускоренный брутфорс JWT секрета

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

jwt_tool: альтернатива без GPU

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".

Генерация admin-токена после взлома

Получив секрет (допустим, secret123), генерируем токен с нужными claims:

python3 jwt_tool.py <JWT> -T -S hs256 -p "secret123"

Флаг -T открывает интерактивный режим — меняем role на admin, username на нужное значение, подтверждаем. Инструмент подписывает модифицированный payload найденным ключом и выводит готовый JWT для подстановки в запрос.

Альтернативный путь — jwt.io: вставляем оригинальный токен, редактируем payload в визуальном редакторе, вводим секрет в поле «Verify Signature» — декодирование и подпись происходят в реальном времени. Удобно для быстрого прототипирования, когда лень возвращаться в терминал.

Confusion атака JWT: подмена RS256 на HS256

Когда ни 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.

Продвинутые JWT-атаки: kid, jwk и jku

На CTF уровня Hard и в реальных уязвимостях JWT аутентификации встречаются атаки через параметры header, выходящие за рамки alg. Эти параметры управляют тем, какой ключ сервер использует для проверки подписи — и если они инъектируемы, атакующий контролирует процесс верификации целиком.

kid: инъекция через Key ID

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 spoofing и CVE-2018-0114

Параметр 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: подмена URL набора ключей

Параметр 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 — готовый набор ключей для этой атаки.

Kill chain: порядок действий в JWT-задаче на CTF

Не все атаки работают на каждой задаче — и тратить 20 минут на confusion, когда секрет был password, обидно. Вот порядок проверки, экономящий время на соревновании:

  1. Перехватить токен (cookie, Authorization header, localStorage).
  2. Декодировать header, определить alg и наличие дополнительных параметров (kid, jku, jwk).
  3. Если alg = HS256, пробуем none (самое быстрое): python3 jwt_tool.py <JWT> -X a.
  4. Если none заблокирован — брутфорс секрета: hashcat -a 0 -m 16500 <JWT> rockyou.txt.
  5. Если alg = RS256, ищем публичный ключ и пробуем confusion RS256 → HS256.
  6. Если в header есть kid — тестируем path traversal и SQL-инъекцию.
  7. Если есть 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 комментариев

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

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

Читайте также

Linux для CTF: команды, права и поиск файлов

13 мин.

3

Linux для CTF: команды, права и поиск файлов

Enumeration, SUID, sudo, find, grep — пошаговый разбор базовых команд Linux для CTF. От получения шелла до root через privilege escalation на реальных примерах.

12 ОКТЯБРЬ, 2026

Стеганография CTF: steghide, zsteg и binwalk

13 мин.

5

Стеганография CTF: steghide, zsteg и binwalk

Пошаговый алгоритм решения stego-тасков в CTF: от определения формата до извлечения флага через zsteg, steghide, binwalk. Чеклист, команды, типичные ловушки.

12 ОКТЯБРЬ, 2026

Взлом хешей John the Ripper и Hashcat в CTF

13 мин.

3

Взлом хешей John the Ripper и Hashcat в CTF

Пошаговый workflow подбора пароля по хешу в CTF: определение типа через hashid, словарные и mask-атаки, утилиты *2john. Hashcat vs JtR с реальными командами.

11 ОКТЯБРЬ, 2026