Содержание:
- Как отличить JWE от JWS
- Двухуровневое шифрование: зачем два алгоритма
- Формат JWE: пять частей
- Invalid Curve Attack на ECDH-ES
- Bleichenbacher на RSA1_5
- Padding Oracle на AES-CBC
- PBES2 DoS: одним запросом положить сервер
- AES-GCM: повторный IV = катастрофа
- Что ещё ломается в JWE
- Где встречается JWE
- Инструменты
- Чеклист JWE-атак
- Что дальше
До сих пор мы работали с JWS - подписанными токенами. Три части через точку. Payload виден всем, подпись гарантирует целостность, но не конфиденциальность. Любой может декодировать Base64url и прочитать содержимое - помним примеры из второй статьи?
JWE устроен иначе. Payload зашифрован. Ты не можешь просто декодировать и прочитать claims. Пять частей вместо трех, два уровня шифрования и набор криптоатак, о которых почти никто не говорит.
Как отличить JWE от JWS
Быстрый способ: посчитай точки.
# 2 точки, 3 части = JWS (подпись) header.payload.signature
# 4 точки, 5 частей = JWE (шифрование) header.key.iv.ciphertext.tag
Или декодируй заголовок (первая часть до первой точки, декодируется одинаково для JWS и JWE):
# base64url - base64: замена символов + padding
echo "$TOKEN" | cut -d. -f1 | tr -- '-_' '+/' | \
awk '{while(length%4)$0=$0"=";print}' | base64 -d
JWS: {"alg":"RS256"} - только алгоритм подписи.
JWE: {"alg":"RSA-OAEP","enc":"A256GCM"} - два алгоритма. Если видишь поле enc - это JWE.
Двухуровневое шифрование: зачем два алгоритма
JWE использует гибридное шифрование. Идея простая: асимметричные алгоритмы (RSA, ECDH) умеют шифровать только маленькие блоки данных (RSA-OAEP с 2048-битным ключом и SHA-1 - максимум 214 байт, RSA-OAEP-256 с SHA-256 - 190 байт), а payload JWT может быть значительно больше. Симметричные алгоритмы (AES) шифруют любой объем данных быстро, но требуют общего секрета.
Решение: совместить оба подхода.
- Генерируется случайный симметричный ключ - CEK (Content Encryption Key). Например, 32 случайных байта для AES-256-GCM. Для AES-CBC-HMAC режимов CEK длиннее: 256 бит для A128CBC-HS256 (128 на шифрование + 128 на MAC), 512 бит для A256CBC-HS512.
- CEK используется для шифрования payload алгоритмом AES. Это быстро и работает с данными любого размера.
- Сам CEK защищается: шифруется (RSA-OAEP), оборачивается (AES Key Wrap) или выводится через согласование ключей (ECDH-ES). Это безопасно, потому что CEK маленький.
Получатель достает CEK своим приватным ключом и расшифровывает payload. Два уровня - отсюда два поля в заголовке: alg для защиты CEK (key management), enc для шифрования payload (content encryption).
Формат JWE: пять частей
Header.EncryptedKey.IV.Ciphertext.Tag
Header - JSON с алгоритмами, закодированный в Base64url. Пример: {"alg":"RSA-OAEP","enc":"A256GCM"}. Этот заголовок не зашифрован (как и в JWS), но защищен от модификации: он включается в Additional Authenticated Data (AAD) при шифровании. Если кто-то изменит заголовок, Authentication Tag не сойдется.
Encrypted Key - CEK, зашифрованный алгоритмом из alg. Для прямого шифрования (алгоритм dir) или ECDH-ES (прямое согласование ключей) это поле пустое - CEK не передается, а вычисляется или используется напрямую.
IV (Initialization Vector) - вектор инициализации для симметричного шифрования. 12 байт для AES-GCM, 16 байт для AES-CBC.
Ciphertext - зашифрованный payload. Тут лежат claims, но прочитать их без ключа нельзя.
Tag (Authentication Tag) - тег аутентификации. Гарантирует, что ни ciphertext, ни заголовок не были модифицированы. 16 байт для AES-GCM.
Invalid Curve Attack на ECDH-ES
Март 2017 года. Antonio Sanso находит баг в пяти JWT-библиотеках одновременно: go-jose, node-jose, jose2go, Nimbus JOSE+JWT, jose4j. Каждая позволяет полностью восстановить приватный ключ сервера.
Контекст. ECDH-ES (Elliptic Curve Diffie-Hellman Ephemeral Static) - алгоритм согласования ключей. Клиент генерирует эфемерную (одноразовую) пару ключей и отправляет свой эфемерный публичный ключ в заголовке JWE через параметр epk (ephemeral public key). Сервер берет этот ключ и вычисляет общий секрет: shared_secret = server_private_key * client_ephemeral_public_key. Из общего секрета выводится CEK.
Проблема. Эллиптическая кривая определяется уравнением y^2 = x^3 + ax + b (mod p). Формулы сложения и удвоения точек используют параметр a, но игнорируют b. Это значит, что если подставить точку, которая лежит не на P-256 (правильной кривой), а на кривой с другим b, сервер все равно выполнит вычисление без ошибок. Библиотеки не проверяли, что точка из epk лежит на правильной кривой.
Атака по шагам:
Атакующий находит альтернативные кривые (с тем же a и p, но другим b'), у которых есть подгруппы малого порядка. Малый порядок - например, 7 или 11 элементов. Берет точку из такой подгруппы и подставляет в epk:
{
"alg": "ECDH-ES",
"enc": "A128GCM",
"epk": {
"kty": "EC", "crv": "P-256",
"x": "<точка на невалидной кривой>",
"y": "<...>"
}
}
Сервер вычисляет d * P, где d - его приватный ключ, P - наша точка из подгруппы малого порядка n_i. Результат попадает в эту подгруппу. Перебираем n_i вариантов, для каждого создаем JWE, отправляем серверу. По ответу (200 vs 400) определяем, какой вариант правильный. Так узнаем d mod n_i.
Повторяем для разных кривых с разными малыми подгруппами. Когда собрали достаточно значений d mod n_1, d mod n_2, …, d mod n_k, применяем CRT (Китайскую теорему об остатках) и восстанавливаем полный приватный ключ d.
Сколько запросов? Jager et al. (2015) считали для аналогичной атаки на TLS-ECDH: ~3300 на Oracle/SunEC, ~17000 на Bouncy Castle. Для JWE-библиотек порядок тот же - тысячи запросов, не миллионы. Полное восстановление приватного ключа. Расшифровка всех прошлых и будущих JWE. А если ключ общий для подписи и шифрования (cross-protocol key reuse) - подделка подписей в придачу.
Уязвимые версии: go-jose < 1.0.5, node-jose < 0.9.3, Nimbus JOSE+JWT < 4.34.2, jose4j < 0.5.1, jose2go (исправлено без номера версии). Та же атака работает на ECDH-ES+A128KW и ECDH-ES+A256KW - те же библиотеки, та же проблема.
Защита: проверять y^2 == x^3 + ax + b (mod p) для каждой точки из epk перед ECDH. Все пять библиотек добавили эту проверку после раскрытия.
Bleichenbacher на RSA1_5
Атака 1998 года, которая до сих пор находит жертв.
alg: "RSA1_5" шифрует CEK через PKCS#1 v1.5 padding. Формат: 0x00 || 0x02 || [случайные ненулевые байты] || 0x00 || CEK. При расшифровке сервер проверяет, что первые два байта - 0x00 0x02. Если сервер по-разному реагирует на правильный и неправильный padding, это oracle.
CVE-2026-28490 (Authlib) - свежий пример. Python-библиотека cryptography при невалидном padding возвращает случайные байты (как и рекомендует RFC 3218 для защиты от Bleichenbacher). Но Authlib проверяла длину полученного CEK до AES-GCM расшифровки:
- Padding валидный, но MAC не сходится:
InvalidTag(ошибка AES-GCM) - Padding невалидный, случайный CEK неверной длины:
ValueError(ошибка длины)
Разные исключения приводили к разным HTTP-кодам. Два разных ответа сервера - и oracle создан. ~14500 запросов для расшифровки CEK.
# Тестирование: модифицируем encrypted_key побитово
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $JWE_ORIGINAL" \
https://target/api
# vs
curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: Bearer $JWE_MODIFIED" \
https://target/api
# Разные HTTP-коды = oracle существует
Защита: использовать RSA-OAEP вместо RSA1_5. OAEP устойчив к классическому Bleichenbacher, но не неуязвим в принципе: Manger (2001) показал атаку на RSA-OAEP через различение ошибок декодирования - ~1000 запросов. Главное правило для любого alg - единый error response для всех криптографических ошибок. Атакующий не должен различать “плохой padding”, “плохой MAC” или “плохой OAEP”.
Padding Oracle на AES-CBC
enc: "A128CBC-HS256" - это AES в режиме CBC с HMAC для аутентификации. AES-CBC требует PKCS#7 padding: последний блок дополняется байтами, каждый из которых равен количеству дополненных байт (например, 4 байта со значением 0x04).
Если сервер различает “invalid padding” и “invalid MAC” - это классическая атака Vaudenay (2002):
- Берем блок ciphertext и меняем один байт в предыдущем блоке
- Отправляем серверу. 256 вариантов на каждый байт
- “Invalid MAC” вместо “Invalid padding” = мы угадали правильный padding
- Из свойства XOR в CBC вычисляем байт plaintext
4096 запросов на 16-байтный блок. Весь payload расшифровывается блок за блоком.
RFC 7518 (Section 5.2.2.2) защищает от этого: HMAC проверяется первым. Если HMAC не сходится, padding вообще не проверяется. Атака ломается, когда реализация проверяет padding до HMAC или возвращает разные коды ошибок.
Защита: использовать AES-GCM (A128GCM, A256GCM). GCM - нативный AEAD (Authenticated Encryption with Associated Data). Нет отдельного padding, нет отдельной MAC-проверки. Шифрование и аутентификация в одной операции. Padding oracle невозможен by design.
PBES2 DoS: одним запросом положить сервер
Самая простая атака из всех. CVE-2023-52428 (Nimbus JOSE+JWT).
alg: "PBES2-HS256+A128KW" - PBKDF2 (Password-Based Key Derivation Function 2). Алгоритм выводит ключ из пароля через многократное хеширование. Параметр p2c в заголовке JWE определяет количество итераций. И этот параметр контролируется атакующим:
import json, base64
def b64url(d):
return base64.urlsafe_b64encode(d).rstrip(b'=').decode()
header = {
"alg": "PBES2-HS256+A128KW",
"enc": "A128GCM",
"p2s": b64url(b"salt"),
"p2c": 2147483647 # 2^31-1 итераций
}
h = b64url(json.dumps(header).encode())
print(f"{h}.AAAA.AAAA.AAAA.AAAA")
2.1 миллиарда итераций HMAC-SHA256. На одном ядре это ~1000 секунд процессорного времени. Один запрос = сервер занят 16 минут на одном ядре. Несколько параллельных запросов = полный DoS.
Защита: лимит на p2c. draft-ietf-oauth-rfc8725bis рекомендует не более 1 200 000 итераций (подробнее в статье 19). Если p2c больше лимита - отклонять токен до начала вычислений.
AES-GCM: повторный IV = катастрофа
AES-GCM с одним и тем же IV (nonce) и ключом на двух разных сообщениях - одна из самых недооцененных ошибок. Что происходит:
XOR plaintext’ов. GCM шифрует через CTR-режим:
ciphertext = plaintext XOR keystream. Два сообщения с тем же IV дают тот же keystream. XOR двух ciphertext’ов = XOR двух plaintext’ов. Это не сами plaintext’ы, но если один из них частично известен (а JWT payload предсказуем -{"sub":",{"iss":"), второй восстанавливается.Утечка ключа аутентификации. Joux (2006, “forbidden attack”) показал: при повторном nonce можно восстановить GHASH-ключ H. С этим ключом атакующий подделывает Authentication Tag для произвольных сообщений. Integrity protection больше нет.
Как проверить: собери несколько JWE-токенов от сервера, декодируй третью часть (IV) из Base64url. Совпадение = сервер использует статический или предсказуемый IV. Это бывает, когда разработчик захардкодил nonce или использует счетчик, который сбрасывается при рестарте.
Что ещё ломается в JWE
Algorithm downgrade. Те же грабли, что с JWS (статья 4). Если сервер принимает alg из заголовка без allowlist: RSA-OAEP -> RSA1_5 открывает Bleichenbacher, -> PBES2 открывает DoS, -> dir позволяет подставить свой ключ напрямую.
Compression bomb. JWE поддерживает "zip":"DEF" - сжатие payload перед шифрованием. Атакующий создает токен, где сжатые данные разворачиваются в гигабайты (CVE-2024-33664, CVE-2024-21319). Один запрос = OOM на сервере.
JWE-JWS confusion. Что если внутри зашифрованного JWE лежит plaintext JWT с alg: "none"? CVE-2026-29000 (pac4j-jwt, CVSS 10.0) - сервер расшифровывает JWE, получает вложенный JWT и принимает его без проверки подписи. Полный auth bypass.
Header injection. Всё из статьи 6 (kid, jku, x5u, jwk) работает и для JWE. Но blast radius выше: kid-инъекция в JWE может извлечь приватный ключ расшифровки, а не просто подменить ключ верификации.
Где встречается JWE
Не так часто, как JWS, но в серьезных местах: Azure AD Token Encryption, ADFS, OpenID Connect (encrypted ID Tokens), PSD2/Open Banking (PSD2 SCA), Apple APNs. Если на пентесте видишь токен с пятью частями - не пролистывай.
Инструменты
jwt_tool -E- манипуляции с JWE-токенами, выбор алгоритмовPadBuster- автоматизация padding oracle на AES-CBCROBOT scanner/marvin-toolkit- детекция Bleichenbacher oracle на RSAjose(CLI от panva/jose) - кодирование/декодирование JWE для ручных тестов
Чеклист JWE-атак
- Декодируй заголовок, определи
algиenc - RSA1_5 - тестируй Bleichenbacher (разные коды ошибок при модификации encrypted_key)
- RSA-OAEP - тестируй Manger (разные ошибки при невалидном OAEP-декодировании)
- ECDH-ES / ECDH-ES+AxxxKW - invalid curve attack (подставь точку с невалидной кривой, проверь ответ)
- A128CBC-HS256 - padding oracle (разные ошибки при модификации ciphertext)
- PBES2-* - подставь
p2c: 2147483647и проверь, зависает ли сервер - AES-GCM - проверь nonce reuse (собери несколько JWE, сравни IV - совпадение = XOR plaintext’ов + утечка GHASH-ключа)
- zip:DEF - compression bomb
- Algorithm downgrade - подмени
algна RSA1_5/PBES2/dir, проверь принимает ли сервер - Header injection - kid/jku/x5u в JWE-контексте (статья 6)
Что дальше
Дальше - рейтинг дырявости JWT-библиотек, JWT в OAuth/OIDC, инструменты, методология пентеста. Фундамент - в этих десяти статьях.