Уязвимость ​​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-формат тоже можно эксплуатировать подобным образом.

На этом этапе я предлагаю остановиться и сделать два важных замечания, которые можно было упустить в процессе поиска причин уязвимости. 

Что такое 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.

Большое простое число n

Национальный институт стандартов и технологий (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=.

Это можно декодировать в:

$ echo -ne "MAYCAQACAQA=" | base64 -d | openssl asn1parse -inform der
0:d=0  hl=2 l=   6 cons: SEQUENCE
2:d=1  hl=2 l=   1 prim: INTEGER           :00
5:d=1  hl=2 l=   1 prim: INTEGER           :00

Более подробно можно об этом прочитать в статьях на OWASP  и попробовать тестовое приложение от DataDog.

У этого эксплоита есть существенный недостаток: он узкоспециализированный и не подходит для большинства других JWT-библиотек. Поэтому я хотел рассказать о том, каким еще способом можно решить эту задачу. Для примера мы будем использовать Google IAP, авторизация которого полностью построена на JWT-токенах с использованием алгоритма ES256, а ключи Key ID доступны публично.

Библиотека com.google.auth.oauth2

Начнем с библиотеки 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

В библиотеке 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

Соответствующим образом генерируем нашу подпись:

var keys = KeyPairGenerator.getInstance("EC").generateKeyPair();
ECPrivateKey privateKey = (ECPrivateKey) keys.getPrivate();
var genPrimeN = privateKey.getParams().getOrder();
var doublePrimeSignature = new byte[64];
var xPart = genPrimeN.toByteArray();
var yPart = genPrimeN.toByteArray();
System.arraycopy(xPart, 1, doublePrimeSignature, 0, 32);
System.arraycopy(yPart, 1, doublePrimeSignature, 32, 32);

В итоге мы получим:  

P-256 DER Signature
P-256 DER Signature

Или в 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:

// Разделим массив с подпиьсю пополам 
final int valueLength = signatureLength / 2;
// Извлечем R
final byte[] rBytes = ByteUtils.subArray(jwsSignature, 0, valueLength);
final BigInteger rValue = new BigInteger(1, rBytes);
// Извлечем S
final byte[] sBytes = ByteUtils.subArray(jwsSignature, valueLength, valueLength);
final BigInteger sValue = new BigInteger(1, sBytes);
// Банальная проверка что R и S не равны 0
if (sValue.equals(BigInteger.ZERO) || rValue.equals(BigInteger.ZERO)) {
	throw new JOSEException("S and R must not be 0");
}
final BigInteger N = ecParameterSpec.getOrder();
// R и S должны быть не больше чем простое число N для нашей кривой
if (N.compareTo(rValue) < 1 || N.compareTo(sValue) < 1) {
	throw new JOSEException("S and R must not exceed N");
}
// Дополнительные проверки
if (rValue.mod(N).equals(BigInteger.ZERO) || sValue.mod(N).equals(BigInteger.ZERO)) {
	throw new JOSEException("R or S mod N != 0 check failed");
}
// Подпись корректна и можно продолжать

Заключение

​​Psychic Signatures — идеальный пример того, насколько тяжело оценить риск одной конкретной уязвимости. Кусочки информации разбросаны по плохо индексируемым источникам информации: блоги, Twitter, отдельные патчи для каждой библиотеки. 

Если вы хотите самостоятельно протестировать все уязвимости, вот ссылка на репозиторий с тестовым приложением.

Комментарии (0)