01.03.2015

Вычисление Struts CSRF Token (CVE-2014-7809)

image

Так как уже прошло некоторое время с момента выхода фрейморка Struts 2.3.20, я хочу рассказать о том, насколько просто вычисляются CSRF-токены при помощи уязвимости S2-023.

Автор: Philippe Arteau

Так как уже прошло некоторое время с момента выхода фрейморка Struts 2.3.20, я хочу рассказать о том, насколько просто вычисляются CSRF-токены при помощи уязвимости S2-023.

В этой статье будет рассказано о практической «эксплуатации» псевдо случайного генератора LCG (Linear congruential generator, линейный конгруэнтный генератор). Далее мы займемся исследованием кода, математическим анализом и поиграемся с шестнадцатеричными байтами. Не забудьте пристегнуть ремни J.

Рисунок 1: Настоящий генератор случайных чисел в действии

Исследование кода

В веб-фреймворке Struts 2 для генерации CSRF-токенов используется класс TokenHelper. Безопасность сгенерированных токенов имеет первостепенное значение, и предполагается, что токены устойчивы к прямому перебору и любым другим алгоритмам вычисления. При желании, вы можете изучить класс TokenHelper и, возможно, тоже найдете уязвимость.

TokenHelper.java (Struts 2.3.17)

import java.math.BigInteger;
import java.util.Map;
import java.util.Random;
[...]

public class TokenHelper {
/**
* Стандартное пространство имен для хранения сессий токенов
*/
public static final String TOKEN_NAMESPACE = "struts.tokens";

/**
* Стандартное имя, связанное со значение токена
*/
public static final String DEFAULT_TOKEN_NAME = "token";

/**
* Имя поля, которое будет хранить имя токена
*/
public static final String TOKEN_NAME_FIELD = "struts.token.name";
private static final Logger LOG = LoggerFactory.getLogger(TokenHelper.class);
private static final Random RANDOM = new Random();

[...]

/**
* Устанавливается транзакционный токен, на основе представленного имени токена
*
* @param tokenName имя токена, на основе которого сгенерированное значение токена
хранится в сессии; для текущей сессии это имя будет с префиксом пространства имен
*
* @return строка токена
*/
public static String setToken( String tokenName ) {
String token = generateGUID();
setSessionToken(tokenName, token);
return token;
}

[...]

public static String generateGUID() {
return new BigInteger(165, RANDOM).toString(36).toUpperCase();
}
}

Идею поняли? Или сдаетесь? Переходим к следующему разделу.

Поиск слабых мест

Для того чтобы проанализировать код, приведенный выше, необходимо ознакомиться еще с двумя классами.

java.security.SecureRandom

Класс SecureRandom представляет собой генератор случайных чисел, считающийся «криптографически» надежным. Реализация класса будет зависеть от системы с JVM. С хорошей энтропией сгенерированные значения вычислить невозможно. Кроме того, важно отметить, что каждое сгенерированное значение не основано на предыдущих значениях и не является членом какой-либо последовательности.

java.util.Random

Класс Random представляет собой линейный конгруэнтный генератор (Linear Congruential Generator, LCG). Генератор основан на изменяющейся переменной, которая умножается на огромное число и уменьшается до младших значащих битов (эти операции будут объяснены позже). Важно отметить, что главные задачи LCG – производительность и равномерное распределение битов.

Давайте рассмотрим метод для генерации GUID (Globally Unique Identifier, Глобальный уникальный идентификатор). Метод generateGUID взят из предыдущего примера.

TokenHelper.java (Struts 2.3.17)

private static final Random RANDOM = new Random();

public static String generateGUID() {
return new BigInteger(165, RANDOM).toString(36).toUpperCase();
}

Начальное число и мера случайности

Вначале (строка 1) класса Random используется внутреннее (implict) начальное число, равное времени (в наносекундах) загрузки класса (System.nanoTime()). Если злоумышленник знает, когда был загружен класс, начальное число может быть вычислено.

Самое слабое звено – использование класса java.util.Random вместо java.security.SecureRandom. Генератор LCG не предназначен для того, чтобы генерировать невычисляемые случайные числа.

В теории все замечательно, а что на практике?

Теоретически мы нашли уязвимость. Но можем ли мы реально вычислять токены, уже имея в наличие несколько сгенерированных экземпляров (или может быть хватит только одного)? Чтобы разобраться с этим вопросом, необходимо более детально проанализировать класс Random и, в частности, что происходит, когда генерируются случайные числа.

java.util.Random

Для вычисления случайного числа, необходимо выяснить алгоритм работы линейного конгруэнтного генератора.

Жизненный цикл генератора

Рисунок 2: Жизненный цикл линейного конгруэнтного генератора

1. Начальное число умножается на константу (multiplier).

2. К предыдущему результату прибавляется константа (addend).

3. К предыдущему результату прикладывается 48-битная маска (младшие значащие биты).

4. К предыдущему результату прикладывается 32-битная маска (старшие значащие биты), которая, однако, не влияет на начальное число при генерации следующего значения.

Полная реализация метода генерации случайного числа класса java.util.Random:

protected int next(int bits) {
long oldseed, nextseed;
AtomicLong seed = this.seed;
do {
oldseed = seed.get();
nextseed = (oldseed * multiplier + addend) & mask;
} while (!seed.compareAndSet(oldseed, nextseed));
return (int)(nextseed >>> (48 - bits));
}

В чем суть метода next()?

Предыдущий алгоритм генерирует целое число длиной 32 бита (int). Больше крупные величины (64-битное число и байтовый массив) генерируются при помощи многократной генерации 32-битных чисел.

Рисунок 3: Порядок байтов в методах при генерации величин различной длины

Почему важны эти детали? Во фреймворке Struts метод nextBytes вызывается внутри класса BigInteger. Для того чтобы вычислить начальное число, нам нужно выстроить байты в обратном порядке для сравнения с первоначальными целыми значениями (типа int).

Использование класса java.util.Random в Struts (TokenHelper.java)

Как фреймворк Struts взаимодействует с классом java.util.Random? Иерархия наиболее важных вызовов показана ниже:

-TokenHelper.generateGUID()
-new Random()
-new BigInteger()
-Random.nextBytes()

Как было сказано выше, даже несмотря на использование метода nextBytes(), генерируется последовательность чисел типа int.

Алгоритм эксплоита

Чтобы научиться вычислять случайные числа, необходимо выстроить взаимосвязь между двумя сгенерированными значениями. Единственная помеха – утерянные 16 бит начального числа, которые можно легко получить методом прямого перебора. Как только начальное число найдено, мы можем генерировать все последующие значение, поскольку экземпляр класса Random используется повторно в глобальном контексте (см. ключевое слово static).

Рисунок 4: Операция прямого перебора

В полной версии кода присутствую дополнительные детали, которые не столь интересны. Если вам нужен рабочий эксплоит, взгляните на экспериментальную версию: struts-csrf-cracker.

Результат работы:

== Initial token
H6P3Y3GHIC2865ASZVQ913NR93QZO7BR
== Initial token in hex (easier evaluation)
14b08fcbf6523eecd7dd7d3e89cf97d6f478db5617

Guessing part..
== bytes representation (reconstructed byte array)
14b08fcb
f6523eec
d7dd7d3e
89cf97d6
f478db56
Seed found: 259752424024079
== following int .. (should match the initial token last part)
d7dd7d3e
89cf97d6
f478db56
175a6e1c
== (prediction) Next token
1590c1573a30295e6d87082bf2c389f575f5fcfa3890b8ac

== (actual) Next token
HWVVZO2VGBZOYD0QFWE8GU3BW4DCRVW8
== (actual) Next token in hex (easier evaluation)
1590c1573a30295e6d87082bf2c389f575f5fcfa38

Заключение

Если в коде для генерации секретного значения используется класс java.util.Random, скорее всего код уязвим.

Вы можете просканировать свой код и библиотеки при помощи сканера уязвимостей, умеющего находить различные бреши, включая использование предсказуемого псевдослучайного генератора.

Ссылки

Struts 2 Advisory S2-023 : Official Struts advisory


Cracking Random Number Generators by James Roper : 3 parts articles series explaining PRNG in Java.

Black-Box Assessment of Pseudorandom Algorithms by Derek Soeder, Christopher Abad and Gabriel Acevedo : Excellent paper presented at BlackHat USA 2013. The tool presented "Prangster" is probably your best bet when source code is not available (Detailed paper).