Главная / Блог / Уязвимости JWT токенов в CTF: подделка подписи

11 мин.00

Уязвимости JWT токенов в CTF: подделка подписи

Уязвимости JWT токенов в CTF: подделка подписи

На последнем CTF я потратил 40 минут на web-задание, где вся авторизация держалась на JWT с HMAC-подписью и секретом password1. Hashcat раскрыл его за 12 секунд — режим 16500, словарь rockyou.txt. Три четверти участников не решили таск. Полезли искать SQL-инъекцию в форме логина вместо того, чтобы заглянуть в токен. Типичная история: уязвимости JWT токенов — один из самых частых векторов в CTF-категории Web, и большинство эксплуатируются за считанные минуты, если знаешь правильную последовательность проверок.

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

Base64 и JSON здесь расписывать не буду — кто участвует в CTF, с этим разберётся. Фокус на том, что критично для эксплуатации. Подробнее — в нашем обзоре атаки на аутентификацию.

JWT — три части через точку: Header.Payload.Signature. Заголовок содержит поле alg — алгоритм подписи. Payload хранит claims: идентификатор, роль, время выдачи. Signature — криптографическая подпись, вычисленная по Header и Payload с использованием секретного (или приватного) ключа.

Корневая проблема: сервер при верификации токена часто берёт значение alg прямо из заголовка входящего JWT, а не использует жёстко заданный алгоритм на стороне бэкенда. PortSwigger Web Security Academy формулирует точно: «This is inherently flawed because the server has no option but to implicitly trust user-controllable input». Вот на этом и строятся все атаки ниже.

По классификации OWASP, подделка JWT токена попадает сразу в две категории: A01:2021 — Broken Access Control (обход авторизации) и A02:2021 — Cryptographic Failures (кривая криптография). В терминах MITRE ATT&CK — техника Forge Web Credentials (T1606, Credential Access): атакующий создаёт собственные валидные токены авторизации. Дальше — Valid Accounts (T1078) для закрепления и эскалации привилегий.

Зачем злоумышленнику подделывать JWT? Один поддельный токен с правами администратора в микросервисной архитектуре открывает доступ не к одному эндпоинту, а ко всем внутренним сервисам, которые доверяют этому JWT. Эскалация от user до admin, горизонтальное перемещение между сервисами, доступ к чужим данным — всё через один изменённый токен.

JWT alg none уязвимость: обнуление подписи

Самый простой вектор — и самый частый в CTF начального уровня. Спецификация JWT допускает алгоритм none — отсутствие подписи. Если серверная библиотека не фильтрует это значение, токен без подписи принимается как валидный.

По данным PortSwigger, уязвимость возникает так: JWT-библиотеки обычно дают два метода — verify() (проверяет подпись) и decode() (просто декодирует). Разработчики путают эти методы или пропускают проверку при alg: none. Результат: приложение глотает токен с любыми claims, без всякой подписи.

Пошаговая эксплуатация в CTF:

  1. Перехватите JWT из cookie или заголовка Authorization: Bearer (Burp Suite Proxy или DevTools браузера)
  2. Декодируйте Header — увидите что-то вроде {"alg":"HS256","typ":"JWT"}
  3. Замените alg на none
  4. В Payload измените целевой claim — "role":"admin", "isAdmin":true, "sub":"administrator", зависит от реализации
  5. Закодируйте Header и Payload обратно в Base64url (без паддинга =)
  6. Соберите токен: <header>.<payload>. — подпись пустая, но финальная точка обязательна

Нюанс, который регулярно ломает новичков: некоторые серверы проверяют регистр значения alg. Варианты none, None, NONE, nOnE, nonE — всё стоит перебрать. jwt_tool автоматизирует это: jwt_tool <token> -X a запускает атаку alg:none со всеми вариациями написания. Инструмент пробует варианты с удалением подписи и без, с точкой в конце и без неё.

Ещё момент: иногда сервер проверяет наличие третьего сегмента (подписи), но не его содержимое. Тогда пустая строка после последней точки проходит, а отсутствие точки — нет. Не сработало без подписи вообще? Попробуйте <header>.<payload>.AA — произвольная строка вместо подписи.

Подбор секретного ключа JWT: hashcat mode 16500

Если alg:none заблокирован — следующий шаг: проверить слабость секрета для HMAC-подписи. В CTF среднего уровня авторы намеренно ставят секрет из распространённых словарей.

Hashcat в режиме 16500 принимает полный JWT-токен и перебирает HMAC-секреты, пересчитывая подпись для каждого кандидата:

hashcat -m 16500 jwt.txt -a 0 /usr/share/wordlists/rockyou.txt

Файл jwt.txt содержит полный JWT одной строкой (header.payload.signature), флаг -a 0 — атака по словарю. На GPU уровня RTX 3060 перебор rockyou.txt (14 миллионов записей) занимает секунды — HMAC-SHA256 вычисляется быстро.

Альтернатива без GPU: jwt_tool <token> -C -d wordlist.txt. Работает на CPU, медленнее hashcat на порядки, но для CTF со слабым секретом из топ-1000 паролей — хватает за глаза.

Если словари не сработали — пробуйте стандартные секреты из документации и примеров фреймворков. secret, changeme, your-256-bit-secret (дефолт с jwt.io), supersecretkey, jwt_secret, s3cr3t. В CTF авторы часто используют именно такие значения, рассчитывая что участник проверит очевидное.

После получения секрета пересобрать токен можно через PyJWT: jwt.encode({"sub": "admin", "role": "admin"}, "найденный_секрет", algorithm="HS256"). Или через jwt_tool с флагом -T (tamper) и указанием секрета через -S.

JWT confusion attack: подмена RS256 на HS256

Одна из самых элегантных атак на уязвимости алгоритма подписи JWT. Суть: сервер использует RS256 (асимметричный — приватный ключ подписывает, публичный верифицирует), а атакующий переключает алгоритм на HS256 (симметричный — один ключ и для подписи, и для верификации) и подписывает токен публичным ключом сервера как HMAC-секретом.

Почему это работает? Tim McLean в исследовании для Auth0 описал корневую причину: API большинства JWT-библиотек имеет форму verify(token, verificationKey). Для RS256 в качестве verificationKey передаётся публичный ключ, для HS256 — секрет. Если библиотека берёт алгоритм из заголовка токена и слепо подставляет verificationKey, то при alg: HS256 публичный ключ (который по определению общедоступен) становится HMAC-секретом. Атакующий подписывает токен этим публичным ключом — сервер верифицирует его тем же ключом — подпись сходится. Красиво, правда?

Пошаговая эксплуатация:

  1. Найдите публичный ключ сервера. Типичные места: /.well-known/jwks.json, /api/v1/keys, /oauth/jwks, иногда ключ валяется в JavaScript-бандле приложения. В CTF ключ может лежать в robots.txt, .git-репозитории или прямо на странице /public-key
  2. Сохраните ключ в PEM-формате. Критически важно: строки должны совпадать побайтово с тем, что использует сервер — лишний перенос строки в конце файла сломает подпись
  3. Измените alg в заголовке JWT с RS256 на HS256
  4. Модифицируйте Payload (добавьте нужные привилегии)
  5. Подпишите токен публичным ключом как HMAC-секретом

jwt_tool делает это одной командой: jwt_tool <token> -X k -pk public.pem. Флаг -X k активирует key confusion атаку с подменой алгоритма.

Типичная засада в CTF: публичный ключ получен из JWKS (JSON-формат), а для подписи нужен PEM. Конвертация через CyberChef (операция «From JWK»), либо через python-скрипт с библиотекой cryptography. Формат не совпадает — подпись невалидна, даже если атака применима.

Ещё вариант: публичный ключ нигде не торчит, но доступны два валидных JWT с подписью RS256. Есть инструменты (rsa_sign2n, jwt_forgery.py), которые пытаются восстановить публичный ключ из двух подписей. PortSwigger описывает этот сценарий в лабе «JWT authentication bypass via algorithm confusion with no exposed key».

JWK и JKU Injection: встраиваем свой ключ в токен

Эта категория атак эксплуатирует опциональные параметры JWT-заголовка: jwk (встроенный публичный ключ верификации) и jku (URL набора ключей).

Подмена через jwk

Спецификация JWS позволяет передавать публичный ключ прямо в заголовке токена через параметр jwk. Если сервер принимает этот ключ без проверки, что он принадлежит доверенной стороне, — атакующий генерирует свою пару RSA, подписывает токен приватным ключом и встраивает публичный в заголовок.

Именно эту уязвимость обнаружили в библиотеке Cisco node-jose (CVE-2018-0114). По данным NVD: 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): node-jose до версии 0.11.0 позволяла удалённому неаутентифицированному атакующему пере-подписать токен ключом, встроенным в сам токен. Как отмечает Intigriti в разборе этой CVE — проблема возникла из-за type confusion: библиотека не проверяла, совпадает ли ключ в заголовке с ключом, которым токен должен был быть подписан.

Разбор вектора CVSS: AV:N (атака по сети), AC:L (низкая сложность), PR:N (привилегии не нужны), UI:N (действие пользователя не требуется), I:H (высокое влияние на целостность). По данным CISA, для CVE-2018-0114 существует публичный PoC-эксплойт (SSVC decision: Track*), статус эксплуатации — poc, атака автоматизируема (automatable: yes). EPSS-скор: 0.4265, percentile 98.67 — top 5% по вероятности эксплуатации среди всех CVE. Публичные PoC-репозитории: zi0Black/POC-CVE-2018-0114 (26 звёзд на GitHub), adityathebe/POC-CVE-2018-0114 (Go-реализация).

В Burp Suite расширение JWT Editor позволяет провести атаку визуально: создаёте RSA-пару в JWT Editor Keys, затем в окне редактирования токена выбираете «Embed JWK» — расширение автоматически подпишет модифицированный токен и вставит публичный ключ в заголовок.

Подмена через jku

Параметр jku (JWK Set URL) указывает серверу адрес, откуда скачать набор ключей верификации. Если URL не валидируется — атакующий размещает свой JWKS на контролируемом сервере.

Последовательность: генерируете RSA-пару, публикуете публичный ключ в формате JWKS ({"keys":[...]}) на своём сервере или через Burp Collaborator, подписываете токен приватным ключом, указываете "jku": "https://attacker.com/.well-known/jwks.json" в заголовке. Сервер скачивает ваш набор ключей и принимает подпись.

В CTF часто добавляют частичную валидацию jku — проверяют, что URL начинается с домена приложения. Типичные обходы: https://app.ctf.local@attacker.com/jwks.json, https://app.ctf.local/redirect?url=https://attacker.com/jwks.json через open redirect, https://app.ctf.local/.well-known/jwks.json#@attacker.com через фрагмент.

kid header injection: path traversal и SQLi

Параметр kid (Key ID) в заголовке JWT указывает серверу, какой ключ использовать для верификации из набора. По спецификации — произвольная строка-идентификатор, но серверная реализация может использовать kid как путь к файлу или как параметр SQL-запроса. И тогда открывается инъекция.

Path traversal через kid

Если сервер читает ключ верификации из файла по пути на основе kid:

{
  "alg": "HS256",
  "typ": "JWT",
  "kid": "../../../../dev/null"
}

/dev/null возвращает пустую строку. Подписываем токен пустой строкой как HMAC-секретом — сервер проверит подпись тем же пустым значением, подпись сойдётся. jwt_tool автоматизирует: jwt_tool <token> -X i -hc kid -hv "../../dev/null" -pc role -pv admin.

Альтернативные файлы для CTF: /proc/sys/kernel/hostname (предсказуемое содержимое), любой статический файл приложения с известным содержимым — CSS, JS. Если содержимое угадываемо, его можно использовать как секрет подписи.

SQL-инъекция через kid

Если kid подставляется в SQL-запрос (SELECT key_value FROM jwt_keys WHERE kid='<kid>'), возможна UNION-инъекция. В заголовке указываете "kid": "xxx' UNION SELECT 'known-secret' -- ", запрос возвращает строку known-secret, которой вы подписываете токен. Пересечение JWT-уязвимости и классической SQL Injection (OWASP A03:2021 — Injection).

Маркеры kid-инъекции в CTF: сервер возвращает ошибки при нестандартных значениях kid, присутствие параметра kid в токене вообще (не все реализации его используют), разные ответы при kid='1' и kid='1'+'1' (признак SQL-контекста).

Чек-лист взлома JWT в CTF

Порядок проверок — от быстрого к сложному. На соревнованиях время критично, так что не лезьте сразу в kid injection, пока не проверили очевидное.

Шаг Проверка Инструмент Время
1 Декодировать payload, изучить claims CyberChef, jwt.io 30 сек
2 Изменить payload без пересчёта подписи Burp Repeater 1 мин
3 alg:none (все варианты регистра) jwt_tool -X a 1 мин
4 Брутфорс HMAC-секрета по словарю hashcat -m 16500 2-5 мин
5 Key confusion RS256 → HS256 jwt_tool -X k 5 мин
6 JWK embed (свой ключ в заголовок) Burp JWT Editor 5 мин
7 jku подмена (внешний URL ключей) Свой сервер + jwt_tool 10 мин
8 kid path traversal jwt_tool -X i 5 мин
9 kid SQLi Ручная инъекция 10-15 мин

По инструментарию: jwt_tool (ticarpi/jwt_tool) покрывает 90% CTF-сценариев одним инструментом. Burp Suite с расширением JWT Editor даёт визуальный контроль и удобен для JWK-embed. Hashcat незаменим для GPU-брутфорса. CyberChef — быстрое декодирование и конвертация JWK в PEM. PyJWT — для случаев, когда нужен кастомный скрипт.

Паттерны распознавания вектора в CTF: JWT подписан RS256 и где-то доступен публичный ключ (robots.txt, /.well-known/, .git) — авторы хотят key confusion. Токен содержит параметр kid — ищите path traversal или SQLi. HS256 без других зацепок — hashcat с rockyou.txt. В задании есть эндпоинт, возвращающий JWKS — пробуйте jku injection. Эти маркеры сэкономят часы на CTF, но по-настоящему паттерны запоминаются только когда сам прогоняешь их на стендах. Готовый набор таких стендов есть на HackerLab.pro — российская CTF-платформа с категориями web, crypto и другими, нужна регистрация, после неё доступны таски от начальных до продвинутых.

Как защиты влияют на выбор атаки

Понимание серверных защит ускоряет выбор правильного вектора. Если не понимаешь, что закрыто — будешь долбиться в стену.

Жёсткий whitelist алгоритмов — сервер принимает только RS256 или только HS256. Атаки alg:none и confusion не пройдут. В CTF это маркер: ищите kid injection, jku spoofing или утёкший секретный ключ.

Игнорирование alg из токена — сервер использует алгоритм из своей конфигурации, а не из заголовка JWT. Рекомендация Auth0: JWT-библиотеки не должны даже смотреть на поле alg в заголовке, кроме проверки совпадения с ожидаемым. Если сервер реализовал это правильно — все атаки на подмену алгоритма бесполезны.

Валидация jwk/jku домена — сервер принимает ключи только с определённого домена. Обход через open redirect или нестандартный URL-парсинг (фрагменты, символ @).

Типизированная обработка kid — значение kid используется как индекс массива или enum, а не как путь к файлу или SQL-параметр. Инъекция невозможна.

В CTF задания с правильными защитами обычно имеют один намеренный пробел. Базовые атаки не проходят — ищите нестандартное: утёкший приватный ключ в .env-файле, дефолтный секрет фреймворка, race condition при ротации ключей.


Мой опыт с JWT на CTF за последние два года укладывается в одну мысль: 80% web-тасков с JWT решаются первыми тремя проверками из чек-листа — alg:none, слабый секрет, key confusion. Оставшиеся 20% — kid injection и JWK/JKU spoofing. Проблема большинства участников не в незнании этих атак, а в неправильной последовательности: тратят время на сложные векторы, не проверив простые. На одном соревновании я видел команду, которая три часа реверсила кастомный алгоритм подписи — другая команда решила тот же таск за две минуты через hashcat с дефолтным словарём.

Момент, который редко обсуждают в writeup-ах: JWT-атаки в CTF — не академическое упражнение. CVE-2018-0114 в библиотеке Cisco node-jose — реальная уязвимость с CVSS 7.5, EPSS в top 5% по вероятности эксплуатации, публичными PoC и статусом «automatable» по оценке CISA. Разница между CTF и пентестом здесь только в масштабе последствий: на CTF получаете флаг, на пентесте — полный обход аутентификации микросервисной архитектуры с десятками внутренних сервисов. Навык один и тот же. На WAPT эту цепочку проходят в течение двух модулей с лабами — если хочешь не просто writeup прочитать, а пройти всю атаку самому с прогрессией.

🚀 Хочешь закрепить на практике? Реши задачи по теме на HackerLab — категория «pentest-machines».

Поделиться

0 комментариев

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

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

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

SQL-инъекции для начинающих: ручной поиск и sqlmap

12 мин.

6

SQL-инъекции для начинающих: ручной поиск и sqlmap

Пошаговый разбор SQL-инъекций: ручной поиск уязвимостей, UNION и blind-атаки, автоматизация через sqlmap с конкретными командами — на примерах из CTF.

29 СЕНТЯБРЬ, 2026

XXE-инъекции: эксплуатация XML в CTF

11 мин.

11

XXE-инъекции: эксплуатация XML в CTF

Пошаговый разбор XXE-эксплуатации для CTF: чтение файлов, blind OOB-exfiltration, обход фильтров через UTF-16 и XInclude с готовыми пейлоадами.

29 СЕНТЯБРЬ, 2026

Reverse engineering в Ghidra: crackme с нуля

12 мин.

14

Reverse engineering в Ghidra: crackme с нуля

Полный разбор crackme в Ghidra: от file и strings до XOR-декодирования пароля и патчинга JNZ. Python-скрипт, anti-reversing техники, stripped-бинарники

28 СЕНТЯБРЬ, 2026