
На одном CTF задача в категории web выглядела элементарно: форма логина, гостевой аккаунт, цель — забрать флаг из admin-панели. Два часа я ковырял SQL-инъекцию в форме авторизации, которой там не было. Решение лежало в cookie — JWT-токен с "role": "user". Замена alg на none, удаление подписи, "role": "admin" — флаг через 30 секунд. Обидно? Ещё как. Подделка JWT токена — один из самых частых векторов в CTF-задачах на обход авторизации, а разброс техник широк: от тривиального alg:none до algorithm confusion с подменой RS256 на HS256. Разберём каждый вектор с конкретными командами, предусловиями и порядком проверки — чтобы на соревнованиях время уходило на решение, а не на перебор несуществующих уязвимостей.
JWT — три части, разделённые точками: header, payload, signature. Каждая закодирована в base64url (без = на конце). Перед любой атакой декодируем первые две части и ищем точки входа. Подробнее — в нашем материале про атаки на аутентификацию.
В header главное поле — alg. Оно определяет алгоритм подписи и сразу задаёт направление атаки: HS256 (HMAC, симметричный ключ) — проверяем стойкость секрета; RS256 (RSA, асимметричная пара) — ищем публичный ключ для confusion; ES256 (ECDSA) — проверяем версию Java; none — подпись отсутствует, и если стоит в оригинале, это уже подарок. Также смотрим на необязательные параметры kid, jku, x5u — через них возможны инъекции.
В payload ищем claims, управляющие привилегиями: role, is_admin, isAdmin, admin, group, sub. Тут же служебные поля iat (время выпуска) и exp (время истечения). По данным OWASP WSTG, payload не шифруется — любой перехвативший токен прочитает содержимое через простое base64-декодирование.
Инструменты для анализа JWT токена онлайн и локально: jwt.io в браузере для быстрого декодирования, python3 jwt_tool.py <token> в терминале для полного разбора с подсветкой полей. В Burp Suite расширение JSON Web Tokens автоматически декодирует токены прямо в HTTP-истории.
На этапе разведки проверяем endpoint'ы, которые могут раскрыть дополнительную информацию: /login — для получения новых токенов, /api/v1/profile — для проверки валидности, /.well-known/jwks.json или /api/jwks — если используется асимметричный алгоритм, здесь может лежать публичный ключ в формате JSON Web Key Set.
И сразу — нулевая проверка: модифицируйте один символ в payload (скажем, смените "user" на "User") без изменения header и signature. Сервер принял? Подпись не валидируется вообще. Как описывает PortSwigger Web Security Academy, разработчики иногда путают jwt.decode() (просто декодирует payload) с jwt.verify() (проверяет подпись, затем декодирует). Результат — приложение не проверяет целостность токена. Правим нужные claims, отправляем — никакие криптографические атаки не нужны. Я на CTF видел это раза четыре, и каждый раз удивлялся.
Работает если: сервер принимает значение alg: "none" и не enforce'ит whitelist допустимых алгоритмов; серверная библиотека не блокирует none по умолчанию.
Не работает если: сервер явно задаёт список допустимых алгоритмов (например, algorithms: ["HS256"] в параметрах verify()); библиотека отклоняет none автоматически (большинство актуальных версий).
Спецификация JWT (RFC 7519) допускает алгоритм none для неподписанных токенов. Если серверная логика берёт значение alg прямо из заголовка входящего токена и на его основе решает, как проверять подпись — атакующий контролирует весь процесс верификации. Это первый вектор для проверки в CTF: занимает 30 секунд и не требует ни ключей, ни брутфорса.
Три шага вручную:
"alg": "HS256" на "alg": "none"."role": "admin" или "is_admin": true.<header>.<payload>. — с точкой на конце, но пустой подписью.С jwt_tool это одна команда: python3 jwt_tool.py <JWT> -X a. Флаг -X a генерирует несколько вариантов alg:none с разным регистром — none, None, NONE, nOnE. По данным OWASP WSTG, если сервер блокирует none через case-sensitive проверку, вариант NoNe может обойти фильтр. Подставляем каждый полученный токен в cookie или заголовок Authorization: Bearer <token> и проверяем ответ.
В контексте OWASP Top 10 эта атака попадает под A07:2021 — Identification and Authentication Failures: сервер не проверяет подлинность токена, принимая неподписанные данные как достоверные.
Работает если: алгоритм HS256 (или HS384/HS512); секретный ключ — словарное слово, короткая строка или дефолтное значение (secret, password, changeme, key).
Не работает если: ключ — криптографически стойкая строка длиной 256+ бит; используется асимметричный алгоритм.
Если alg:none не прошёл, а в заголовке стоит HS256 — проверяем стойкость HMAC-секрета. В CTF-задачах слабый ключ встречается постоянно — авторы тасков любят ставить что-нибудь вроде secret или qwerty. Безопасность HMAC-подписи, согласно OWASP WSTG, целиком определяется стойкостью секретного ключа: если приложение использует open-source фреймворк — первым делом стоит проверить документацию на наличие дефолтного значения.
Два основных инструмента для брутфорса секретного ключа JWT:
# jwt_tool — перебор по словарю (CPU, медленнее)
python3 jwt_tool.py <JWT> -C -d /usr/share/wordlists/rockyou.txt
# hashcat — GPU-ускоренный перебор, модуль 16500 (JWT)
hashcat -m 16500 -a 0 <JWT> /usr/share/wordlists/rockyou.txt
hashcat на GPU перебирает на порядки быстрее — для rockyou.txt (14 млн паролей) это вопрос минут. Ключ найден? Подписываем новый токен: python3 jwt_tool.py <JWT> -T -S hs256 -p "<найденный_секрет>" — флаг -T открывает интерактивный режим редактирования claims перед подписанием.
Альтернатива: конвертировать JWT в формат John the Ripper через скрипт jwt2john.py и использовать более продвинутые правила мутации (rules-based attack). По данным OWASP WSTG, если JWT слишком длинный, может потребоваться увеличить значение SALT_LIMBS в исходном коде John и перекомпилировать его.
В Burp Suite для автоматического определения слабых HMAC-ключей есть расширение JWT Heartbreaker — оно проверяет токен против списка распространённых секретов прямо при перехвате. Штука удобная, но словарь у неё маленький — для серьёзного перебора всё равно нужен hashcat.
В терминах MITRE ATT&CK подбор секрета JWT попадает под технику Forge Web Credentials (T1606, Credential Access): атакующий подделывает веб-учётные данные для получения несанкционированного доступа.
Работает если: сервер изначально использует RS256 (асимметричный); публичный ключ доступен атакующему (через /.well-known/jwks.json, TLS-сертификат или другой endpoint); серверная библиотека не проверяет соответствие типа ключа и алгоритма.
Не работает если: сервер явно pin'ит алгоритм RS256 и отклоняет HS256; библиотека верифицирует тип ключа (RSA-объект vs byte string) перед проверкой подписи; публичный ключ не удаётся получить.
Самая элегантная из стандартных атак на JSON Web Token. При RS256 сервер подписывает токен приватным ключом и проверяет публичным. Меняем alg на HS256 — уязвимый сервер переключается на симметричную проверку и использует тот самый публичный ключ как HMAC-секрет. Публичный ключ нам известен — подписываем любой payload.
Как объясняет WorkOS в анализе JWT algorithm confusion attacks: «HMAC-SHA256 принимает любую последовательность байтов в качестве ключа. Ему безразлично, что эти байты представляют RSA-публичный ключ. Вся модель безопасности HS256 строится на секретности ключа — а публичный ключ RSA, по определению, не секрет.»
Красиво, правда? Сервер сам себя подставляет.
Пошаговый процесс:
1. Получаем публичный ключ. Типичные места: /.well-known/jwks.json, /api/v1/jwks, /oauth/certs. Если ключ не лежит на отдельном endpoint — извлекаем из TLS-сертификата: openssl s_client -connect target:443 | openssl x509 -pubkey -noout. Сохраняем результат в файл public.pem.
2. Модифицируем токен. Меняем alg в header на HS256, правим claims в payload ("role": "admin" и всё, что нужно).
3. Подписываем публичным ключом. Команда jwt_tool: python3 jwt_tool.py <JWT> -S hs256 -k public.pem. Инструмент подпишет модифицированный токен алгоритмом HS256, используя содержимое public.pem как HMAC-секрет.
4. Отправляем токен серверу. Уязвимый сервер читает "alg": "HS256" из заголовка, берёт свой «ключ» (публичный RSA-ключ, который он использует и для RS256-верификации) и проверяет HMAC. Подписи совпадают — токен принят.
У этой уязвимости богатая история в реальных библиотеках. Node.js jsonwebtoken (один из самых скачиваемых JWT-пакетов на npm) был подвержен до версии 4.2.2 — функция verify() принимала алгоритм из заголовка токена по умолчанию. Python PyJWT вёл себя аналогично до версии 1.5.0. Библиотеки ruby-jwt, php-jwt и ряд Java-реализаций страдали от того же дефекта в разные периоды. По данным WorkOS, паттерн во всех экосистемах был идентичен: «API библиотеки делал удобным верификацию токенов без указания ожидаемого алгоритма, а поведение по умолчанию — доверять заголовку токена.»
Критический нюанс для CTF: формат ключа должен совпадать побайтово с тем, что использует сервер. Разница в CRLF vs LF, лишний пробел, отсутствие или наличие newline в конце файла — и подпись не совпадёт. Если атака по логике должна работать, но не работает — проверьте формат ключа: xxd public.pem | tail. Этот момент съедает больше времени, чем сама атака.
Работает если: сервер использует параметр kid из заголовка JWT для поиска ключа в файловой системе или базе данных; входное значение kid не санитизируется.
Не работает если: kid валидируется по whitelist допустимых идентификаторов; сервер не использует kid; значение не подставляется в пути/запросы напрямую.
Параметр kid (Key ID) — необязательное поле заголовка, указывающее серверу на конкретный ключ для проверки подписи. Если значение kid подставляется в файловый путь без фильтрации — path traversal. Классический приём: указать путь к файлу с предсказуемым содержимым. В Unix-системах /dev/null возвращает пустую строку — подписываем токен пустым ключом. Важно: эта техника работает преимущественно против устаревших или кастомных реализаций HMAC. Современные стандартные библиотеки (PyJWT 2.x, jsonwebtoken 9.x) требуют непустой ключ и на стороне подписи, и на стороне верификации — так что в актуальных стеках атака с пустым секретом, как правило, не проходит:
`python3 jwt_tool.py
Здесь -I активирует режим инъекций, -hc kid указывает целевой параметр заголовка, -hv задаёт значение для path traversal, -p "" — пустая строка как ключ подписи.
Альтернативный вариант — использовать любой статический файл из веб-директории (CSS, JS, robots.txt), содержимое которого можно получить через HTTP-запрос: python3 jwt_tool.py -I -hc kid -hv "path/to/known/file" -S hs256 -p "содержимое_файла".
Если kid подставляется в SQL-запрос — открывается вектор SQL-инъекции. Значение kid вида ' UNION SELECT 'controlled_secret' -- заставит сервер извлечь из базы подконтрольный атакующему ключ. По данным Intigriti, комбинация kid-инъекции с перебором позволяет добраться до подписи даже когда прямой брутфорс HMAC невозможен.
Работает если: серверная библиотека позволяет встраивать публичный ключ JWK прямо в заголовок токена и доверяет ему для верификации; сервер загружает ключи по URL из параметра jku без whitelist.
Не работает если: сервер игнорирует параметры jwk, jku, x5u и загружает ключи только из локальной конфигурации; URL jku проверяется по строгому whitelist.
CVE-2018-0114 (CVSS 7.5, HIGH, CWE-347 — Improper Verification of Cryptographic Signature) — уязвимость в open-source библиотеке node-jose (Cisco) до версии 0.11.0. Библиотека следовала стандарту JWS, который допускает встраивание публичного ключа JWK в заголовок токена. Этот ключ затем использовался для верификации — что позволяло неаутентифицированному удалённому атакующему переподписать токен собственным ключом, встроив его прямо в JWS-заголовок.
По данным CISA, для этой CVE доступен публичный PoC (статус эксплуатации: poc). На GitHub репозиторий zi0Black/POC-CVE-2018-0114 (26 звёзд) содержит готовый эксплойт. PoC также доступен в Exploit-DB под номером EDB-44324 (автор zioBlack, опубликован 20.03.2018). EPSS — 0.4265 (Top 5% среди всех CVE, очень высокая вероятность эксплуатации).
Атака через jku работает по схожему принципу: атакующий поднимает свой сервер с файлом JWKS, содержащим его публичный ключ; в заголовке токена указывает "jku": "https://attacker.com/jwks.json"; подписывает токен своим приватным ключом. Сервер загружает ключ атакующего, проверяет подпись — она валидна. Защита — whitelist URL для jku или полное игнорирование этого параметра.
Работает если: бэкенд на Java версий 15–18 (Oracle Java SE 17.0.2, 18; Oracle GraalVM Enterprise Edition 21.3.1, 22.0.0.2); алгоритм подписи ES256 (ECDSA).
Не работает если: Java обновлена до 17.0.3+ или 18.0.1+; бэкенд использует не-Java реализацию; алгоритм не из семейства ECDSA.
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, по данным Red Hat).
Суть, описанная в OWASP WSTG: Java некорректно валидировала ECDSA-подписи в определённых условиях. Если уязвимая версия используется для парсинга JWT с алгоритмом ES256, проверку подписи можно полностью обойти — достаточно заменить подпись на строку MAYCAQACAQA. Итоговый токен: eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZG1pbiI6InRydWUifQ.MAYCAQACAQA.
Название «Psychic Signatures» — не ради красного словца. Подпись буквально принимается без проверки, как если бы сервер «угадал», что она верна.
EPSS — 0.6034 (Top 1% среди всех CVE, экстремально высокая вероятность эксплуатации). Red Hat выпустил патчи RHSA-2022:1436 и RHSA-2022:1437 для OpenJDK 17.0.3. Debian закрыл в DSA-5128-1 (openjdk-17 17.0.3+7-1~deb11u1). Ubuntu — в USN-5388-2 и USN-5546-1 для jammy, focal, bionic.
В CTF-контексте: видите ES256 в заголовке — определяйте стек бэкенда. HTTP-заголовки (X-Powered-By, Server), специфичные stacktrace в ошибках, характерные для Java endpoint'ы (/actuator, /swagger-ui.html). Если Java — подставляйте MAYCAQACAQA вместо подписи с модифицированным payload. Проверка занимает секунды.
На соревнованиях время — критический ресурс. Вот последовательность, которая минимизирует потери:
Шаг 1. Декодируем токен, читаем alg. Если HS256 — шаг 2. Если RS256 — шаг 5. Если ES256 — шаг 7.
Шаг 2. Модифицируем payload без изменения header/signature. Сервер принял — подпись не проверяется, правим claims и забираем флаг.
Шаг 3. Пробуем alg:none атаку JWT через jwt_tool -X a (30 секунд на все варианты регистра). Сработало — готово.
Шаг 4. Брутфорсим HMAC-секрет: hashcat -m 16500 с rockyou.txt (2–5 минут на GPU). Параллельно проверяем kid на path traversal (../../dev/null) и SQLi.
Шаг 5 (RS256). Ищем публичный ключ: /.well-known/jwks.json, /api/jwks, TLS-сертификат. Нашли — JWT confusion attack (RS256→HS256).
Шаг 6. Проверяем jku/jwk spoofing, kid injection.
Шаг 7 (ES256). Определяем стек бэкенда. Java 17.x / 18 — пробуем CVE-2022-21449 с подписью MAYCAQACAQA.
Шаг 8. Ничего не сработало — возвращаемся к разведке: ищем дополнительные endpoint'ы, утечки ключей через другие уязвимости (LFI, SSRF, XXE). Утечка SECRET_KEY или приватного ключа через побочный канал — тоже валидный путь (MITRE ATT&CK: Private Keys — T1552.004, Credentials In Files — T1552.001).
Требования к окружению:
jwt_tool (github: ticarpi/jwt_tool), hashcat (apt install hashcat), opensslPortSwigger Web Security Academy, раздел «JWT Attacks» — бесплатные интерактивные лабы с уязвимым сервером. Каждая лаба изолирована под конкретный вектор: отдельно alg:none, отдельно algorithm confusion, отдельно kid injection, отдельно brute-force. По данным PortSwigger, начиная с Burp Suite Professional 2022.5.1 сканер автоматически детектит ряд JWT-уязвимостей.
Для самостоятельной практики: сгенерируйте токен на jwt.io с секретом secret123 и алгоритмом HS256. Прогоните его через hashcat -m 16500 -a 0 token.txt rockyou.txt — увидите, за сколько секунд подбирается слабый ключ. Затем подпишите модифицированный токен с найденным секретом через jwt_tool — полный цикл взлома JWT CTF на собственном стенде.
На реальных CTF-площадках задачи с JWT встречаются в категориях web и crypto. Типичный формат: получить токен с ролью user, повысить привилегии до admin, обратиться к защищённому endpoint для получения флага.
Восемьдесят процентов CTF-задач на JWT, которые я решал за последние пару лет, эксплуатируют одни и те же ошибки разработчиков: отсутствие whitelist алгоритмов, дефолтные секреты из документации фреймворка, слепое доверие параметрам из заголовка токена. Это не баги в криптографии — это баги в логике валидации. И пока разработчики продолжают использовать jwt.decode() вместо jwt.verify(), CTF-задачи на JWT будут в топе.
CVE-2022-21449 — отдельная история. Уязвимость буквально позволяет подписать что угодно пустой подписью. Не теоретический концепт: EPSS 0.6034, Top 1%, PoC на GitHub со 121 звездой. В CTF она встречается реже, но когда встречается — решается за секунды, если знаешь про MAYCAQACAQA. Через год-два, когда уязвимые версии Java уйдут из прода, этот вектор станет экзотикой. А вот algorithm confusion и слабые HMAC-секреты никуда не денутся — потому что это не баг конкретной версии, а системная ошибка проектирования.
Тренд последних двух лет: задачи усложняются. Простой alg:none уходит в категорию easy, средние и сложные таски комбинируют векторы — kid injection для получения ключа плюс algorithm confusion для подписания, или jku spoofing через open redirect на другом endpoint. Если хочешь не просто закрывать простые таски, а проходить цепочку целиком — на WAPT эту механику разбирают с лабами на каждый вектор и ментором в чате.
🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».
0 комментариев
Пожалуйста, войдите, чтобы оставить комментарий.
Загрузка комментариев...