JWT: Weak Secret
JWT подписан слабым секретом — подберите его через brute-force и подделайте токен администратора.
easyphpPro
Задача
# JWT: подбор слабого HMAC-секрета подписи
## Сценарий
Вы — аудитор безопасности **SecureShop** (PHP + Apache + `firebase/php-jwt`). Аутентификация построена на JWT-токенах с алгоритмом HS256: после успешного входа сервер выдаёт клиенту HMAC-подписанный токен в cookie `token`, и каждый последующий запрос авторизуется по нему. В отличие от лаб про `alg: none` и Key Confusion, здесь сервер корректно настраивает алгоритм и проверяет подпись «как положено». Уязвимость спрятана в другом месте — в качестве самого секрета подписи.
По данным внутреннего аудита, в админ-панели хранится конфиденциальная метка (CTF-флаг), доступная только пользователям с ролью `admin`. Пароль администратора атакующему неизвестен; ваша задача — получить admin-доступ и извлечь флаг, имея только обычную учётную запись `demo` / `demo`.
## Цель
1. Залогиньтесь как `demo` / `demo` и изучите структуру токена в cookie `token`: алгоритм подписи (HS256), claims (`user_id`, `username`, `role: user`, `exp`).
2. Найдите способ восстановить HMAC-секрет — атака полностью офлайн, без обращения к серверу.
3. Подпишите новый токен с восстановленным секретом и значением `role: admin`, подмените cookie и получите доступ к `/admin`. Извлеките CTF-флаг.
## Теория
JWT-подпись HS256 (HMAC-SHA256) — **симметричная**: тот же секрет, что использовался для подписи, нужен и для проверки. Криптостойкость HMAC-SHA256 как алгоритма не имеет значения, если злоумышленник может перебрать сам секрет. По BCP RFC 8725, HMAC-секрет должен содержать минимум столько энтропии, сколько даёт сам алгоритм — для HS256 это **256 бит** (32 случайных байта).
Если секрет короткий, словарный (`secret`, `password`, `key`, `jwt_secret`, `your_secret_key`) или иначе предсказуемый, любой, у кого на руках есть один валидный токен, может подобрать его офлайн — без обращения к серверу, без блокировок по rate-limit и без следов в логах. Готовые инструменты делают это за секунды:
- `hashcat -m 16500` (mode для JWT HS*) — словарь rockyou.txt;
- `ticarpi/jwt_tool.py -C -d <wordlist>` — комбинированный инструмент;
- `john --format=HMAC-SHA256` — альтернатива hashcat.
Усиливает уязвимость практика помещать `role`/привилегии напрямую в payload токена: тот, кто контролирует подпись, контролирует все авторизационные решения. В этой лабе именно так и происходит — функция получения текущего пользователя берёт `role` из claims, а не из БД.
**Уязвимый PHP-паттерн:**
```php
// VULNERABILITY: weak, dictionary-word secret — brute-forceable in milliseconds
$GLOBALS['JWT_SECRET'] = 'secret';
// later, in getUser():
$decoded = JWT::decode($token, new Key($GLOBALS['JWT_SECRET'], 'HS256'));
// ...
// VULNERABILITY: role overridden from JWT claims
$user['role'] = $decoded['role'] ?? $user['role'];
```
Подпись `JWT::decode` сама по себе корректна; проблема — в `'secret'`. После того как атакующий восстановил это значение по словарю, он может подписать любой токен — `firebase/php-jwt` его примет как валидный.
## Таблица атак на JWT
| Класс атаки | Идея | Применима в этой лабе |
|-------------|------|----------------------|
| Brute-force HMAC-секрета | Подобрать слабый HS256-секрет по словарю (hashcat, jwt_tool) | **Да** — основной вектор |
| Подмена `role` в claims | Поменять `role` на `admin` после подписания валидным секретом | **Да** — финальный шаг |
| `alg: none` | Объявить отсутствие подписи | Нет — `firebase/php-jwt` отклоняет `alg: none` |
| Key Confusion (RS256 → HS256) | Подменить алгоритм; использовать публичный ключ как HMAC-секрет | Нет — сервер изначально на HS256, асимметричной схемы и публичного ключа нет |
| JWK header injection | Передать свой ключ в заголовке токена | Нет — секрет берётся из серверной переменной, не из токена |
## Точка входа атаки
| Параметр | Значение |
|----------|----------|
| Обычная учётная запись | `demo` / `demo` |
| Алгоритм JWT | HS256 (HMAC-SHA256) |
| Эндпоинт логина | `POST /login` (выдаёт cookie `token` с HS256-JWT) |
| Имя cookie | `token` |
| Привилегированный эндпоинт | `GET /admin` (требует `role: admin` в подписанном токене) |
| Где находится флаг | в HTML-ответе `/admin` в карточке Admin Flag, формат `flag{...}` |