Криптостойкость и небезопасное сравнение

Hint: flag is not a frag: once you've got it, you can get one more...

Ниже просто пример почти безопасного кода, описанный в процессе поиска в нём опасности :). Пригодится как очередной PoC подобных уязвимостей.

Итак, смотрим PHPBB3/includes/ucp_remind.php — этот файл отвечает за напоминание пароля (forum/ucp.php?mode=sendpassword).
Видим такие строки:

// For the activation key a random length between 6 and 10 will do.
$user_actkey = gen_rand_string(mt_rand(6, 10));

Ниже этот ключ заносится в БД. Далее видем такие строки:

'U_ACTIVATE' => "$server_url/ucp.$phpEx?mode=activate&u={$user_row['user_id']}&k=$user_actkey")

Теперь ищем PHPBB3/includes/ucp_activate.php, там находим следующее:

if (($user_row['user_inactive_reason'] == INACTIVE_MANUAL) || $user_row['user_actkey'] != $key)

Видим, что проверка правильности ключа активации производится без проверки типов (!=, а не !==), то есть типы приводятся.
Теперь узнаем, как же генерируется ключ. Лезем в PHPBB3/includes/functions.php, находим нужную функцию:

function gen_rand_string($num_chars = 8 )
{
// [a, z] + [0, 9] = 36
return substr(strtoupper(base_convert(unique_id(), 16, 36)), 0, $num_chars);
}

Что происходит? Берётся некий unique_id(), после чего, его, как числа, основание, переводится из 16 в 36 (это максимально возможное основание для base_convert(), ибо в английском алфавите 26 букв + 10 цифр). А затем, от полученного отрезаются первые $num_chars символов.

Узнаем, как устроена функция unique_id(). Нас интересуют отдельные строки из тела функции:

...
$val = $config['rand_seed'] . microtime();
$val = md5($val);
...
return substr($val, 4, 16);

Итак, у нас генерируется md5()-хеш на основе текущего времени, после чего из него вырезается кусок из 16 символов, начиная с четвёртого (интересно, зачем именно так? :) ).

Затем над этим хешом продолжают издеваться, переводя его в тридцатишестиричную систему (да, я выговорил это!!:) ), переводя в верхний регистр и обрезая..

Собственно, в чём слабость криптостойкости? А в том, что в PHP истинны такие сравнения, как '1E0123123' == '1' или '000.0E321321' == '0', поскольку благодаря букве e/E запись слева рассматривается, как число в стандартном формате (мантисса, экспонента).

Получается, мы можем влиять на этот хеш, повторно отправляя запрос на напоминание пароля.
И мы можем запрашивать каждый раз forum/ucp.php?mode=activate&u=1&k=0 и forum/ucp.php?mode=activate&u=1&k=1, дожидаясь, пока наш 6-10-значный хеш не примет формат 1EXXXXX или формат (N нулей)E(остаток), например, 000EXXXX.

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

Тогда сравнение $user_row['user_actkey'] != $key не выполнится, поскольку выполнится равенство == (точнее называть это эквивалентность, а === — это идентичность).
А значит, пароль будет изменён.

Нам от этого ни горячо, ни холодно конечно, но… ;)

UPD: Реальная эксплуатируемая уязвимость такого же плана описана Арсением Реутовым