Уязвимость ​​CVE-2022-21449 или “Psychic Signatures”, которая была обнаружена в Java 15-18, позволяет обойти механизм проверки ECDSA-подписи и подделать исходное сообщение. Если приложение использует уязвимую версию Java для валидации JWT-токенов на базе алгоритма ES256, злоумышленник может получить доступ к приложению от лица любого пользователя. Подробное описание причины проблемы можно найти в этой статье, но первоначальный proof of concept не дает полного представления о том, какие приложения подвержены этой уязвимости. Чтобы исправить этот пробел, а также иметь возможность «поиграть» с приложением, которое максимально приближено к реальному, я создал стенд. На нем можно протестировать все возможные векторы атаки.  Для начала давайте вспомним, в чем причина уязвимости из оригинальной статьи. ECDSA-подпись состоит из двух значений — r и s. Для проверки подписи приложение должно вычислить уравнение, которое включает r, s, публичный ключ и хэш от сообщения. Если обе стороны уравнения равны, то подпись верна, в противном случае — нет. На одной стороне уравнения — r, на противоположной — произведение r и s. Если r и s равны нулю, то уравнение теряет смысл и будет всегда верно.  0 = 0 ⨉ [a bunch of stuff], где a bunch of stuff — это сообщение и публичный ключ. Поэтому при вычислении важно проверить, что r и s больше или равны 1. Если проверка не сделана, r и s равны нулю и должным образом закодированы, то проверка подписи будет пройдена. Оригинальный код, который демонстрировал уязвимость, использовал формат InP1363Format, но оговаривалось, что DER-формат тоже можно эксплуатировать подобным образом. На этом этапе я предлагаю остановиться и сделать два важных замечания, которые можно было упустить в процессе поиска причин уязвимости.  Синтаксис передачи данных, используемый Distinguished Encoding Rules (DER), всегда соответствует правилу: «тег, длина, значение». Формат обычно называют триплетом TLV, в котором каждое поле (T, L или V) содержит один или несколько байтов. В поле «тег» указывается тип отправляемой структуры данных, в поле «длина» — количество байтов передаваемого содержимого, а в поле «значение» — содержимое. Поле «значение» может быть триплетом, если содержит сконструированный тип данных.  Для представления цифровой подписи нам понадобятся следующие типы данных: SEQUENCE («тег» 0x30) и INTEGER («тег» 0x02). Давайте рассмотрим пример подписи [0x30, 0x06, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00]. Данные начинаются с байта 0x30. Это означает, что перед нами последовательность длиной в 6 байт, а «значение» — два целых числа r и s, каждое из которых равно 0x00. Национальный институт стандартов и технологий (NIST) рекомендует 15 эллиптических кривых. Для случайных эллиптических кривых рекомендованы: Curve P-192, Curve P-224, Curve P-256, Curve P-384, Curve P-521.  Нас интересует простое число (r): например, для кривой P-256 оно равно: Все вычисления выполняются по модулю данного простого числа n, поэтому так важно, чтобы r и s не были равны n. В противном случае n = 0 (mod n). Для вычисления подписи это будет иметь такой же эффект, как r и s = 0.  На данный момент существует несколько эксплоитов для уязвимости, но их объединяет Java-библиотека Java JWT — JSON Web Token for Java and Android. Ключевая особенность этой библиотеки заключается в том, что в ней используется DER-формат подписи, а не прямая конкатенация r и s в подписи.  Эксплоит для ​​CVE-2022-21449 в DER-формате подписи — MAYCAQACAQA=.  Это можно декодировать в: Более подробно можно об этом прочитать в статьях на OWASP  и попробовать тестовое приложение от DataDog. У этого эксплоита есть существенный недостаток: он узкоспециализированный и не подходит для большинства других JWT-библиотек. Поэтому я хотел рассказать о том, каким еще способом можно решить эту задачу. Для примера мы будем использовать Google IAP, авторизация которого полностью построена на JWT-токенах с использованием алгоритма ES256, а ключи Key ID доступны публично. Начнем с библиотеки com.google.auth.oauth2. Для проверки цифровой подписи в документации предлагается использовать класс com.google.auth.oauth2.TokenVerifier. Если пойти по пути наименьшего сопротивления и взять готовый эксплоит, то мы обнаружим, что подпись не проходит проверку длины: signature.length == 64 DerEncoder.encode. Это происходит, потому что Google ожидает, что значения r и s будут просто сконкатенированы, а дальнейшее кодирование выполнит библиотека. Поэтому нам нужно просто сгенерировать два массива длиной в 32 байта, инициализированных 0 для r и s соответственно, и передать приложению.  Предварительно необходимо закодировать Base64: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA У этого подхода есть существенный недостаток — использование 0 для r и s. Чтобы разобраться, почему использование нуля — это недостаток, рассмотрим вторую библиотеку — com.nimbusds.  В библиотеке com.nimbusds.jose реализованы собственные проверки для предотвращения схожих атак, поэтому в нашем примере мы будем рассматривать версию до патча 9.22.  Библиотека nimbusds (как и Google) ожидает, что цифровая подпись будет представлена в виде конкатенации чисел r и s, а значит можно просто взять готовое сообщение: AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA  Но в этом случае приложение вылетит с неожиданной ошибкой — om.nimbusds.jose.JOSEException: Index 64 out of bounds for length 64. Это происходит из-за того, что в алгоритме transcodeSignatureToDER существует ошибка, которая не позволяет корректно обрабатывать 0. Однако алгоритм можно просто «поломать» при помощи отрицательного числа: например, -1. В таком случае подпись будет преобразована в нужный DER-формат — [0x30, 0x06, 0x02, 0x01, 0x00, 0x02, 0x01, 0x00]. Этот эксплоит не будет работать при наличии проверок на длину подписи, которая была добавлена в версии 9.4.2.  Выходит, что библиотека com.nimbusds.jose неуязвима из-за ошибки. Secure by mistake? На самом деле нет, нам просто необходимо вспомнить, что n по модулю n равно нулю. Мы можем улучшить эксплоит, присвоив r и s значение простого числа n для соответствующей эллиптической кривой. Для P-256 — это 115792089210356248762697446949407573529996955224135760342422259061068512044369.  Соответствующим образом генерируем нашу подпись: В итоге мы получим:   Или в Base64: _____wAAAAD__________7zm-q2nF56E87nKwvxjJVH_____AAAAAP__________vOb6racXnoTzucrC_GMlUQ Библиотека com.nimbusds.jose используется во многих проектах, и этот эксплоит будет работать во многих фреймворках: например, в Spring-реализации IAP.  Библиотеки com.auth0 java-jwt org.bitbucket.b_c jose4j по аналогии с com.nimbusds «падают» с ошибкой Index 64 out of bounds for length 64 при использовании подписи A…AAA.  Библиотеки java-jwt и jose4j реализуют похожий алгоритм. Есть только одно исключение: в jose4j проверки на длину алгоритма были добавлены только после апреля 2022-го года, поэтому эксплоит с отрицательной цифровой подписью будет работать в версиях до 0.7.11.  Что получилось в итоге: Библиотека io.jsonwebtoken / jjwt-root использует другой формат подписи, но все выше описанные вектора будут работать. Для ее эксплуатации необходимо использовать кодирование DER : MAYCAQACAQA=. Библиотека com.google.auth.oauth2 не реализует дополнительных проверок, чтобы обезопасить от таких атак (на момент написания статьи). Исправление для com.nimbusds.jose доступно в версии 9.22, com.auth0 java-jwt - 3.19.2, а org.bitbucket.b_c jose4j 0.7.12 Это касается версии Java 15, 16, 17, и 18.  Пример патча для com.nimbusds.jose: ​​Psychic Signatures — идеальный пример того, насколько тяжело оценить риск одной конкретной уязвимости. Кусочки информации разбросаны по плохо индексируемым источникам информации: блоги, Twitter, отдельные патчи для каждой библиотеки.  Если вы хотите самостоятельно протестировать все уязвимости, вот ссылка на репозиторий с тестовым приложением.