Содержание:
- Почему JWT уникален для брутфорса
- Как определить, что алгоритм симметричный
- Hashcat mode 16500
- Скорости на конкретном железе
- jwt.secrets.list - словарь от Wallarm
- jwt_tool - встроенный крекер
- Что говорит RFC
- Чеклист для тестирования
- Что дальше
До сих пор мы подменяли алгоритмы (статьи 3-4) и инжектили параметры заголовка (статьи 5-6). Все эти атаки эксплуатируют логические ошибки в обработке токена. А что если логика правильная, подпись проверяется корректно - но секрет просто слабый?
Почему JWT уникален для брутфорса
Вот что делает JWT идеальной мишенью для офлайн-атаки: весь необходимый материал содержится в одном токене.
Когда ты брутфорсишь пароль к веб-форме, каждая попытка - это запрос к серверу. Rate limiting, блокировка аккаунта, CAPTCHA. Тысяча попыток в секунду - потолок.
С JWT все иначе. Как я показывал во второй статье, токен состоит из трех частей: header.payload.signature. Header и payload - это сообщение. Signature - это HMAC от этого сообщения с секретным ключом. У тебя есть и сообщение, и подпись. Осталось подобрать ключ. И для этого сервер не нужен. Все вычисления происходят локально, на твоем GPU.
Как определить, что алгоритм симметричный
Первый шаг - декодировать заголовок и проверить алгоритм:
echo -n "$TOKEN" | cut -d. -f1 | base64 -d 2>/dev/null
Если видишь "alg": "HS256", "HS384" или "HS512" - алгоритм симметричный (HMAC). Брутфорс возможен. Если "RS256", "ES256", "PS256" - асимметричный. Брутить нечего, приватный ключ в токене не участвует. Для асимметричных алгоритмов нужны другие подходы: algorithm confusion (статья 4) или psychic signatures (статья 8).
HS256 - самый популярный алгоритм JWT. Он единственный обязательный для реализации по RFC 7518 (кроме none). Большинство tutorials, примеров на Stack Overflow и конфигов по умолчанию используют HS256. А значит - большинство JWT в дикой природе уязвимы для брутфорса, если секрет слабый.
Hashcat mode 16500
Hashcat - инструмент для GPU подбора паролей и ключей. Режим 16500 - это HMAC-SHA256 для JWT.
# Записываем токен в файл
echo -n "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ1c2VyIiwiYWRtaW4iOmZhbHNlfQ.signature_here" > jwt.txt
# Словарная атака
hashcat -a 0 -m 16500 jwt.txt \
/usr/share/wordlists/jwt.secrets.list
# С правилами мутации - hashcat модифицирует каждое слово
# из словаря (добавляет цифры, меняет регистр и т.д.)
hashcat -a 0 -m 16500 jwt.txt wordlist.txt \
-r /usr/share/hashcat/rules/best64.rule
# Маска: все 6-символьные комбинации в нижнем регистре
hashcat -a 3 -m 16500 jwt.txt ?l?l?l?l?l?l
# Маска: 8 символов, буквы + цифры + спецсимволы
hashcat -a 3 -m 16500 jwt.txt ?a?a?a?a?a?a?a?a
Скорости на конкретном железе
Вот реальные цифры, чтобы было понятно, насколько это быстро:
- RTX 4090 (GPU): ~150 миллионов HS256/сек
- RTX 3090 (GPU): ~100 миллионов HS256/сек
- i9-13900K (CPU): ~5 миллионов HS256/сек
- jwt_tool на Python: ~50 тысяч HS256/сек
А вот что эти скорости означают для подбора секрета на RTX 4090:
- 6 символов, только строчные буквы [a-z]: 308 миллионов вариантов = 2 секунды
- 8 символов, только строчные [a-z]: 208 миллиардов = 23 минуты
- 8 символов, буквы + цифры [a-zA-Z0-9]: 218 триллионов = 17 дней
- 32 байта из криптографически стойкого генератора: не подберешь никогда
Разница между “secret” и 32 случайных байта - это разница между 2 секундами и бесконечностью.
jwt.secrets.list - словарь от Wallarm
Первое, что я запускаю при тестировании JWT - словарную атаку со специализированным словарем jwt.secrets.list, собранным командой Wallarm. Он содержит реальные секреты из утечек, дефолтных конфигов и CVE:
secret,password,123456- классикаyour-256-bit-secret- дефолт с jwt.io, который разработчики копируют в продакшнa-string-secret-at-least-256-bits-long- ещё один популярный дефолт из туториаловdjango-insecure-*- дефолтные секреты фреймворковchangeme,test,development- секреты, которые “потом поменяем”notfound- тот самый секрет из CVE-2025-20188 (Cisco IOS XE, CVSS 10.0)
Важный момент: your-256-bit-secret и a-string-secret-at-least-256-bits-long содержат “256 bit” в названии и оба длиннее 32 байт. Разработчик видит требование RFC “ключ >= 256 бит”, берёт строку с “256-bit” в названии и думает что всё ок. Но 256 бит - это про энтропию, не про длину строки. a-string-secret-at-least-256-bits-long - это 38 ASCII-символов, формально 304 бита. Но это читаемая английская фраза, она в словаре и подбирается мгновенно. Когда RFC говорит “256 бит”, он имеет в виду openssl rand -base64 32 - 32 случайных байта, где каждый бит непредсказуем.
Словарная атака с этим словарем занимает доли секунды и часто срабатывает. Если не помогло - подключаем правила мутации, потом маски. И только потом переходим к полному перебору.
jwt_tool - встроенный крекер
Если нет GPU или hashcat - jwt_tool имеет встроенную функцию подбора:
python3 jwt_tool.py "$TOKEN" -C \
-d /usr/share/wordlists/jwt.secrets.list
Работает на CPU, скорость на порядки ниже hashcat, но для словарных атак на слабые секреты вполне достаточно.
Что говорит RFC
RFC 7518 Section 3.2 содержит нормативное требование: ключ для HS256 MUST быть не менее 256 бит (32 байта). Для HS384 - 384 бита. Для HS512 - 512 бит.
RFC 8725 усиливает: ключ MUST NOT быть человеческим паролем. Только криптографически стойкий генератор случайных чисел (CSPRNG). То есть openssl rand -base64 32, а не mysupersecretkey.
Реальность: разработчики ставят "secret", "password", process.env.JWT_SECRET || "fallback" (где fallback - дефолтное значение, если переменная окружения не задана). И секрет "fallback" оказывается в продакшене.
Чеклист для тестирования
- Перехватил JWT с HS256 - сразу в hashcat с jwt.secrets.list
- Не нашел? jwt.secrets.list + best64.rule (правила мутации)
- Проверь дефолтные секреты конкретного фреймворка (Next.js, Django, Spring Boot)
- Если доступен .env файл (через LFI, git-утечку, docker inspect) - ищи JWT_SECRET, SECRET_KEY, SIGNING_KEY
- Docker-образ доступен?
docker save image | tar -xO | grep -ri "secret\|jwt\|signing" - Ничего не нашел? Маска на 8 символов [a-zA-Z0-9] - 17 дней на одном GPU
Что дальше
Мы ломали HMAC (этот пост) и RSA через algorithm confusion (статья 4). Осталось ECDSA. В следующей статье - Psychic Signatures: баг в Java, при котором подпись из одних нулей проходит верификацию для любого сообщения, с любым ключом. Пять строк Python - и ты admin на любом Java-сервисе с Java 15-18.