Нам выдается два файла и ссылка на веб-приложение. Один из файлов - Dockerfile
,
который может пригодиться для отладки эксплойта. Если его открыть,
можно увидеть что базовым образом является perl
, а значит бэкенд этого приложения
написан на perl. Это не должно стать большой проблемой, так как в коде используется
достаточно современный фреймворк Mojolicious
, и, в целом, код достаточно читаемый.
На что стоит обратить внимание в первую очередь?
my $secret_key = random_bytes(8);
my $jwt_secret = encode_base64(random_bytes(32), '');
my $flag = $ENV{FLAG};
post '/get-flag' => sub {}
В коде объявляются переменные secret_key
, jwt_secret
, flag
и обработчики API-ручек, один
из которых - get-flag
. Секреты вырабатываются с помощью стойкого рандома из
Crypt::PRNG qw(random_bytes);
, поэтому будем далее считать, что нет способа
их получить быстрее, чем полным перебором.
Посмотрим внимательнее на код функции, вызываемой при POST на get-flag
.
my $package_data;
eval {
$package_data = decode_jwt(token => $package, key => $jwt_secret, alg => 'none', allow_none => 1);
};
unless (verify_signature(
join('|', @$package_data{qw(sender recipient amount timestamp)}),
$package_data->{signature}
)) {
return $c->render(json => { error => "Invalid signature" }, status => 400);
}
if ($package_data->{recipient} ne "1337133713371337133713371337133713371337133713371337133713371337") {
return $c->render(json => { error => "This package is not for flag wallet address" }, status => 400);
}
if ($package_data->{amount} ne "133713371337") {
return $c->render(json => { error => "This package contains not enough money" }, status => 400);
}
return $c->render(json => { success => 1, flag => $flag })
Здесь много условий, заканчивающихся возвратом ответа 400, но если их все пройти - мы получим флаг.
В условиях участвуют поля объекта package_data
, который, в свою очередь, декодируется из токена package
.
Нас интересуют signature
(должно удовлетворять функции verify_signature
),
recipient
(должен быть фиксированным) и amount
(аналогично recipient
).
Токен package
генерируется с помощью функции create-package
, например, через веб-интерфейс.
Это обычный неподписанный JWT токен, так что мы без проблем можем положить внутрь любые данные.
sub verify_signature {
my ($message, $provided_signature) = @_;
my $md5_hash = md5_hex($message);
my $des = Crypt::DES->new($secret_key);
my $encrypted_hash = decode_base64($provided_signature);
my $decrypted_hash = $des->decrypt($encrypted_hash);
return '00'.substr($md5_hash, 0, 6) eq $decrypted_hash;
}
Эту функцию стоит читать снизу вверх, поскольку возвращается boolean от сравнения
$decrypted_hash
и '00'.substr($md5_hash, 0, 6)
. Если переписать эту функцию на псевдокод,
получим примерно следующее сравнение:
('00' + md5_hex(message)[:6]) == DES(secret_key).decrypt(b64.decode(signature))
Т.е. один из параметров хешируется, а другой проворачивается через DES с неизвестным
ключом secret_key
. Или, проще говоря, подпись расшифровывается и должна совпасть с началом
хеша от message
.
Стоит обратить внимание на то, что блок для алгоритма DES - 8 байт, и именно столько байт занимает строка "00xxxxxx", в которой 6 символов - первые 6 символов hexdigest md5 от message.
Это не очень хорошо, потому что на каждый байт буфера (256 значений) приходится
только 16 уникальных значений шестнадцатеричного алфавита. Из всех возможных значений байта
от \x00
до \xFF
там могут быть значения \x30-\x39
(ASCII цифры) и \x61-\x66
(ASCII буквы от a
до f
)
По сути, уникальных значений
decrypted_hash
может быть только
Быстро взглянем на функцию create_signature
и перепишем ее на псевдокод.
sub generate_signature {
my ($message) = @_;
my $md5_hash = md5_hex($message);
my $des = Crypt::DES->new($secret_key);
my $encrypted_hash = $des->encrypt('00'.substr($md5_hash, 0, 6));
return encode_base64($encrypted_hash, '');
}
return DES(secret_key).encrypt(b64.encode('00' + md5_hex(message)[:6]))
Сразу становится понятно, что за $signature
мы расшифровываем в verify_signature
-
это зашифрованный DES на ключе $secret_key
md5 хэш сообщения, таким же образом подрезанный
до первых 6 hex символов.
Процедуры генерации и проверки подписи могут принимать на вход любой $message
, который можно
захэшировать с md5, поэтому отправимся на поиски $message
выше.
join('|', @$package_data{qw(sender recipient amount timestamp)})
В ручках create-package
, redeem-package
и get-flag
можно найти повтояющийся код, который
и генерирует искомый $message
. Перепишем его на псевдокод :)
'|'.join([
package_data.sender,
package_data.recipient,
package_data.amount,
package_data.timestamp,
])
Т.е. $message
- склееная по прямому слешу строчка из значений внутри JWT токена.
Проанализировав логику работы подписи можем сгенерировать пакет, который пройдет проверку
verify_signature
, даже без знания секретного ключа DES.
- Получим произвольный JWT токен с помощью вызова
create-package
- Раскодируем JWT и получим
package_data
- Посчитаем хэш от
hash = md5_hex("|".join(*package_data))[:6]
- Подменим recipient на нужный (мы не можем сразу сгенерировать здесь нужное значение, см. Вторичный вектор)
- Подменим amount на нужный
- Далее, перебирая значения в
package_data.sender
илиpackage_data.timestamp
, добьемся совпадения нового хэшаbadhash
сhash
. - Запакуем
package_data
в JWT и отправим наget-flag
Мы точно знаем, что передается в процедуру шифрования DES - это join
от полей package_data
.
Какой там ключ - нам всё равно, так как мы будем перебирать поля в package_data
до тех пор, пока
первые 6 символов от нового md5 не совпадут со старыми (полученными из create-package
). Свойством
любого алгоритма блочного шифрования, очевидно, является совпадение зашифрованных сообщений CT
при совпадающих шифруемых PT
. Соотвественно, хоть мы и не получим полную коллизию md5 (нам это не
требуется), при частичной коллизии по 3-байтовому префиксу для нашего нового package_data
create_signature
сгенерировал бы совпадающую подпись!
Остается только обернуть это в небольшой скрипт и написать переборщик
Гипотетически, можно пойти по другому пути.
При регистрации пользователя ему выдается 10 монет, которые он может потратить для создания
передачки любому другому пользователю. В теории, зарегистрировав 13371337133 пользователей, можно
передать все средства на одного пользователя и создать желаемую ручкой get-flag
передачку.
Однако, в коде есть проверка, и создать такую передачку не получится.
post '/create-package' => sub {
...
if ($recipient_address eq "1337133713371337133713371337133713371337133713371337133713371337") {
return $c->render(json => { error => "Антифрод система заблокировала операцию"}, status => 418);
}
...
}