Перейти к содержимому
← Каталог php Auth Bypass

JWT: Key Confusion (RS256 to HS256)

Сервер подписывает JWT через RS256, но при валидации читает алгоритм из заголовка токена. Атакующий меняет alg на HS256, подписывает токен публичным RSA-ключом как HMAC-секретом и получает admin.

hardphpPro
Задача
# JWT Key Confusion: подмена асимметричного алгоритма симметричным ## Сценарий Вы — аудитор безопасности **SecureShop** (PHP + Apache + `firebase/php-jwt`). Аутентификация построена на JWT-токенах с **асимметричной подписью**: сервер хранит приватный RSA-ключ, подписывает токены `RS256` и публикует соответствующий публичный ключ через служебные API (это нормальная практика для систем с асимметричной подписью — клиенты и микросервисы могут проверять подпись локально, без обращения к секретам). По данным внутреннего аудита, в админ-панели хранится конфиденциальная метка (CTF-флаг), доступная только пользователям с ролью `admin`. Пароль администратора атакующему неизвестен; ваша задача — получить admin-доступ и извлечь флаг, имея только обычную учётную запись `demo` / `demo`. ## Цель 1. Залогиньтесь как `demo` / `demo` и изучите токен в cookie `token`: декодируйте header (увидите `alg: RS256`) и payload (увидите `user_id`, `username`, `role: user`). 2. Найдите служебный эндпоинт, через который сервер раздаёт публичный ключ (стандартные имена — `/api/public-key`, `/api/jwks`, `/.well-known/jwks.json`). 3. Сформируйте JWT-токен, который сервер примет за валидный и который даст административные права, **не используя приватный ключ сервера**. Для этого нужно понимать класс атаки **JWT Algorithm Confusion** и точный формат материала, который сервер использует как HMAC-секрет в уязвимой ветке. 4. Подмените cookie `token`, откройте `/admin` и извлеките CTF-флаг. ## Теория **JWT Key Confusion** (Algorithm Confusion) — атака на JWT-валидацию, основанная на смешении классов криптографических алгоритмов: - **RSA-подписи** (`RS256`, `RS384`, `RS512`) — асимметричная криптография: приватный ключ подписывает, публичный верифицирует. - **HMAC-подписи** (`HS256`, `HS384`, `HS512`) — симметричный ключ: один и тот же секрет используется и для подписи, и для проверки. Когда серверный верификатор читает поле `alg` из заголовка токена и сам выбирает по нему ветку проверки, атакующий получает контроль над тем, каким алгоритмом сервер будет проверять подпись. Указав в заголовке `alg: HS256`, он заставляет сервер выполнить HMAC-проверку. Если в HMAC-ветке сервер использует материал публичного ключа (PEM-строку, DER-байты или хеш от них) как HMAC-секрет, атакующий может получить те же байты с публичного эндпоинта и подписать любой токен. В этой лабе сервер использует библиотеку `firebase/php-jwt`, корректно подписывает токены `RS256`, но в верификаторе: - читает `alg` из заголовка приходящего токена; - если `alg = RS256` — проверяет подпись публичным ключом (правильно); - если `alg = HS256` — использует **DER-байты публичного ключа** (`base64_decode` тела PEM без заголовков) как HMAC-секрет — это и есть путь к атаке. DER-байты публичного ключа атакующий получает тривиально: скачивает PEM с `/api/public-key`, снимает заголовки `-----BEGIN/END PUBLIC KEY-----`, делает `base64_decode` тела. **Уязвимый PHP-паттерн:** ```php // Verifier reads alg from the token header (attacker-controlled data) $header = json_decode(base64_decode(strtr($parts[0], '-_', '+/')), true); $alg = $header['alg'] ?? 'RS256'; if ($alg === 'RS256') { $decoded = JWT::decode($token, new Key($_publicKeyPem, 'RS256')); } else { // VULNERABILITY: HS256 path — DER bytes of the public key as HMAC secret $decoded = JWT::decode($token, new Key($_publicKeyDer, 'HS256')); } // VULNERABILITY: role from claims, not from DB $user['role'] = $payload['role']; ``` Дополнительно — роль пользователя берётся напрямую из claim'ов, а не из БД, поэтому любая успешная подмена подписи автоматически даёт ровно те привилегии, которые атакующий записал в payload. ## Таблица атак на JWT | Класс атаки | Идея | Применима в этой лабе | |-------------|------|----------------------| | HS256 ↔ RS256 (key confusion) | Подменить алгоритм с асимметричного на симметричный, подписать токен публичным ключом как HMAC-секретом | **Да** — основной вектор | | `alg: none` | Объявить отсутствие подписи; сервер пропускает проверку | Нет — `firebase/php-jwt` отклоняет `alg: none` по умолчанию | | Brute-force HMAC-секрета | Подобрать слабый секрет офлайн (hashcat) | Нет — сервер изначально использует RSA, HMAC-секрета нет | | JWK header injection | Передать собственный публичный ключ прямо в заголовке токена | Нет — верификатор использует серверный ключ из памяти, не доверяет заголовку | | `kid` path traversal | Подсунуть в заголовке путь к контролируемому атакующим файлу как ключ | Нет — поле `kid` верификатор не использует | ## Точка входа атаки | Параметр | Значение | |----------|----------| | Обычная учётная запись | `demo` / `demo` | | Эндпоинт логина | `POST /login` (выдаёт cookie `token` с RS256-JWT) | | Эндпоинт публичного ключа | `GET /api/public-key` (PEM-формат) или `GET /api/jwks` (JWKS) | | Имя cookie | `token` | | Привилегированный эндпоинт | `GET /admin` (требует `role: admin` в подменённом токене) | | Где находится флаг | в HTML-ответе `/admin` в карточке Admin Flag, формат `flag{...}` |
🚧 Сайт в разработке. Полный функционал пока недоступен. Все вопросы — support@hackandfix.ru