Ищем уязвимости, как «белые хакеры». Подборка задач из CTF-турниров в 2024 году
Делимся решением задач от CTF-турниров со всего мира. Пригодится как опытным, так и начинающим специалистам по кибербезопасности.
В мире кибербезопасности есть такое событие, как CTF-турнир (Capture The Flag). В нем участники захватывают флаги соперников и защищают свои. Организаторы «зашивают» их в специально разработанные системы — заполучить флаги можно только через взлом. В подборке рассказываем о прошедших CTF-турнирах и делимся решением самых интересных задач по мнению Ивана, специалиста по кибербезопасности в Selectel.
Материал не обучает хакингу и не призывает к противозаконным действиям. Все описанное ниже лишь демонстрирует, какие пробелы в безопасности встречаются в реальных веб-приложениях. И предупреждает, на что нужно обратить внимание при разработке программного обеспечения.
В январе прошел CTF-турнир KnightCTF 2024, который организовали cybersecurity-энтузиасты из Бангладеша. За 24 часа участникам предстояло решить задачи из категорий cryptography, reverse engineering, digital forensics, pwn, steganography, web и networking. Коллеге особенно понравилась задача Gain Access 2 из веб-технологий. Ниже рассказываем, как он ее решил.
У вас есть все, чтобы достичь цели. Рыцарь указал вам путь. Следуйте по нему и получите флаг. Имейте в виду, что все задачи основаны на реальных ошибках в приложениях.
Решение
Дана форма авторизации:
В коде страницы закомментирован путь к файлу. Переходим к нему и видим послание.
На странице выводится или логин/пароль, или логин/куки — надо проверить оба варианта. Возвращаемся на форму авторизации и вводим полученные данные — логин или пароль неверный.
Заходим в исходный код страницы и видим условие: если авторизация пройдет успешно, пользователя перенаправят на страницу 2fa.php.
Включаем Burp в режим proxy и делаем запрос к 2fa.php. В перехваченном запросе меняем кукки на полученное ранее значение d05fcd90ca236d294384abd00ca98a2d. После — переходим на страницу для ввода второго значения:
Аналогично предыдущему условию работаем с dashboard.php. Делаем запрос к dashboard.php, подменяем в Burp кукки и попадаем на страницу с флагом. Готово — задача решена.
KnightCTF 2024, часть 2
Продолжение материала о KnightCTF 2024, но уже с другими задачами из категории networking. Некоторые из них повторяют предыдущие — так, например, автор находит скрытый флаг в уже известном .pcap-файле и использует bash-history TCP-протокола. Последнее было в задаче Confidential, рассказываем о ней подробнее.
Возвращаемся к bash-history из предыдущей задачи. На 162 строке видим скачанный архив maybeconfidential.zip. Узнаем, что в нем находится.
В Wireshark можно получить файлы, которые передавались в момент записи дампа трафика. Выбираем Файл → Экспортировать объекты → HTTP, добавляем в фильтр maybeconfidential.zip:
Распаковываем архив. Внутри — .docx-файл с картинкой. На ней изображен маскот рыцаря Knight CTF 2024:
Формат .docx — тоже архив, нужно его открыть. В нем находятся три папки с файлами и XML-документ.
С помощью обычного поиска по тексту ищем в названии KCTF. Находим флаг в maybeconfidential/maybeconfidential/word/document.xml. Задача решена!
DiceCTF 2024 Quals
В начале февраля команда DiceGang провела квалификацию DiceCTF 2024 Quals. Это был Jeopardy-турнир длительностью 48 часов. Все задачи были распределены по пяти направлениям: crypto, misc, pwn, rev и web. В последнем запомнилась задача dicedicegoose: из-за неоднозначного условие было непонятно, что нужно делать. Посмотрим, какое решение нашел наш коллега Иван.
Дана только ссылка на сайт. Переходим на нее и видим такую игру:
Заходим в исходный код страницы, а там — полотно из JavaScript-кода. Не будем приводить здесь весь листинг, вместо этого покажем интересные куски, за которые можно зацепиться. Перед нами — переменные player и goose с числовыми значениями.
let player = [0, 1];
let goose = [9, 9];
Из кода понятно, что переменные — это массивы с исходными координатами красного кубика и черного квадрата. Расположим их в массиве history:
let history = [];
history.push([player, goose]);
Далее видим блок с изменениями координат player и goose:
document.onkeypress = (e) => {
if (won) return;
let nxt = [player[0], player[1]];
switch (e.key) {
case "w":
nxt[0]--;
break;
case "a":
nxt[1]--;
break;
case "s":
nxt[0]++;
break;
case "d":
nxt[1]++;
break;
}
if (!isValid(nxt)) return;
player = nxt;
if (player[0] === goose[0] && player[1] === goose[1]) {
win(history);
won = true;
return;
}
do {
nxt = [goose[0], goose[1]];
switch (Math.floor(4 * Math.random())) {
case 0:
nxt[0]--;
break;
case 1:
nxt[1]--;
break;
case 2:
nxt[0]++;
break;
case 3:
nxt[1]++;
break;
}
} while (!isValid(nxt));
goose = nxt;
history.push([player, goose]);
redraw();
};
Изменение координат player происходит нажатием клавиш W, A, S, D. Координаты goose меняются на единицу в сторону, выбранную случайным образом. И после каждого изменения — добавляются в history.
То есть игрок двигает кнопками красный кубик, тогда как черный квадрат перемещается случайным образом. А массив history сохраняет всю историю координат.
Из кода видно, что игрок выигрывает, если координаты player и goose совпадают:
if (player[0] === goose[0] && player[1] === goose[1]) {
win(history);
won = true;
return;
}
Чтобы получить флаг, нужно набрать девять очков и с помощью вызова функции передать в нее массив history:
if (score === 9) log("flag: dice{pr0_duck_gam3r_" + encode(history) + "}");
Функция encode выглядит следующим образом:
function encode(history) {
const data = new Uint8Array(history.length * 4);
let idx = 0;
for (const part of history) {
data[idx++] = part[0][0];
data[idx++] = part[0][1];
data[idx++] = part[1][0];
data[idx++] = part[1][1];
}
let prev = String.fromCharCode.apply(null, data);
let ret = btoa(prev);
return ret;
}
Каждое нажатие клавиши управления добавляет одно очко. Получается, выигрыш возможен только в сценарии, когда красный кубик двигается вниз, а черный квадрат — влево.
Для примера прикрепляем результат игры без изменения кода, а также содержимое массива history и результат выполнения функции encode:
Логика игры понятна. Теперь нужно получить массив history определенного вида, чтобы захватить флаг. Есть два варианта, как это сделать. Можно через консоль задать значение массива и вызвать функцию encode или просто набрать девять очков в игре. Мы пойдем по второму пути.
Сохраняем index.html и меняем в блоке кода координаты черного квадрата так, чтобы за каждый ход он двигался только влево:
# Было
do {
nxt = [goose[0], goose[1]];
switch (Math.floor(4 * Math.random())) {
case 0:
nxt[0]--;
break;
case 1:
nxt[1]--;
break;
case 2:
nxt[0]++;
break;
case 3:
nxt[1]++;
break;
}
}
# Стало
do {
nxt = [goose[0], goose[1]];
nxt[1]--;
}
Переходим из кода страницы в Source, затем — в Override. В браузере заменяем index.html отредактированным JavaScript-кодом.
Перезагружаем страницу, нажимаем девять раз S и получаем результат:
Далее идем в консоль, вызываем функцию encode и передаем в нее аргумент history, чтобы получить недостающую часть флага. Готово!
0xL4ugh CTF 24
Третий CTF-турнир, но от команды 0xL4ugh из Египта. В течение суток участникам нужно было решить 36 заданий из категории web, DFIR, reverse, crypto, pwn, misc и osint. По сравнению с предыдущими турнирами задачи здесь сложнее: они основаны на реальных случаях и исследованиях. Ниже делимся решением Simple WAF — задачей по веб-технологиям.
В init.db нам предлагают создать таблицу с тремя столбцами.
CREATE TABLE IF NOT EXISTS `users` (
`id` varchar(50) NOT NULL,
`username` varchar(20) NOT NULL,
`password` varchar(50) NOT NULL
) ENGINE=InnoDB DEFAULT CHARSET=latin1 AUTO_INCREMENT=66 ;
После — добавить запись об администраторе:
insert into users(id,username,password) values('1','admin','c0b12ccad044e2e525cf818077413c4c');
На этот раз пароль — не admin. Хеш md5 к нему не подходит.
Перейдем к index.php. В нем есть ряд условий:
if(isset($_POST['login-submit']))
{
if(!empty($_POST['username'])&&!empty($_POST['password']))
{
$username=$_POST['username'];
$password=md5($_POST['password']);
if(waf($username))
{
die("WAF Block");
}
else
{
$res = $conn->query("select * from users where username='$username' and password='$password'");
if($res->num_rows ===1)
{
echo "0xL4ugh{Fake_Flag}";
}
else
{
echo "<script>alert('Wrong Creds')</script>";
}
}
}
else
{
echo "<script>alert('Please Fill All Fields')</script>";
}
}
Если username и password в отправленном POST-запросе не пустые, то идет проверка в функции waf:
Поскольку в базе данных находится одна строка, то обход авторизации должен сработать независимо от переданных значений username, password. Добавляем еще одно условие, чтобы обойти авторизацию:
‘ or 1=1;#
‘ or 1=1;-- -
‘ or true;#
‘ or true;-- -
Данные в username не должны попадать под шаблон preg_match("/([^a-z])+/s",$input). В нем происходит проверка наличия букв: если они есть, то запрос к БД не доходит. Поэтому стоит учитывать фильтр preg_match php, чтобы не допустить появление букв в username.
В итоговой SQL-инъекции меняем or 1=1-- — на || 1=1-- -.
Теперь необходимо решить, как обойти preg_match. По ссылке можно найти информацию о функции и ее ошибках. Делаем вывод, что preg_match, являясь PCRE-функцией, рекурсивно проверяет переданную строку по шаблону и имеет предел входных значение. Информацию о лимитах функции можно найти в руководстве по PHP. При превышении этого значения waf вернет false, что нам и требуется.
Собираем POST-запрос в Python и передаем его в тестовое приложение, запущенное в Docker. Собираем образ Docker и запускаем контейнер:
Интересное наблюдение: при 10’001 символов функция возвращает false. Возможно, это значение может быть еще ниже, его можно определить параметром pcre.recursion_limit в php.ini.
Теперь этим скриптом можно обратиться к основному приложению, чтобы получить флаг. Готово!
Хотите подробнее узнать о кибербезопасности, чтоб решать «хакерские» задачи? Начните с курса «Введение в сетевую безопасность». В нем делимся инструкциями, с помощью которых вы научитесь анализировать сетевой трафик и проводить сканирование портов.
Дальнейшая обработка запроса происходит только в том случае, если проверка в waf прошла неуспешно. После этого она выполняет запрос к базе данных:
"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"select * from users where username='$username' and password='$password'","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"
Если в результате запроса к БД количество строк равняются единице, то мы получим флаг. Для этого необходимо выполнить два условия.
Поскольку в базе данных находится одна строка, то обход авторизации должен сработать независимо от переданных значений username, password. Добавляем еще одно условие, чтобы обойти авторизацию:
"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"‘ or 1=1;#\n‘ or 1=1;-- -\n‘ or true;#\n‘ or true;-- -","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"
Данные в username не должны попадать под шаблон preg_match(\"/([^a-z])+/s\",$input). В нем происходит проверка наличия букв: если они есть, то запрос к БД не доходит. Поэтому стоит учитывать фильтр preg_match php, чтобы не допустить появление букв в username.
Теперь необходимо решить, как обойти preg_match. По ссылке можно найти информацию о функции и ее ошибках. Делаем вывод, что preg_match, являясь PCRE-функцией, рекурсивно проверяет переданную строку по шаблону и имеет предел входных значение. Информацию о лимитах функции можно найти в руководстве по PHP. При превышении этого значения waf вернет false, что нам и требуется.
Интересное наблюдение: при 10’001 символов функция возвращает false. Возможно, это значение может быть еще ниже, его можно определить параметром pcre.recursion_limit в php.ini.
Хотите подробнее узнать о кибербезопасности, чтоб решать «хакерские» задачи? Начните с курса «Введение в сетевую безопасность». В нем делимся инструкциями, с помощью которых вы научитесь анализировать сетевой трафик и проводить сканирование портов.
"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":0,"favorites":3,"reposts":0,"views":3132,"hits":3211,"reads":null,"online":0},"dateFavorite":0,"hitsCount":3211,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/dev/1089007-ishem-uyazvimosti-kak-belye-hakery-podborka-zadach-iz-ctf-turnirov-v-2024-godu","author":{"id":172558,"name":"Selectel","nickname":null,"description":"Крупнейший независимый провайдер сервисов IT-инфраструктуры в России. Облачные серверы от 29 ₽/час: slc.tl/bfnnu","uri":"","avatar":{"type":"image","data":{"uuid":"bdaede51-3c77-55c3-a3ab-e9940df92258","width":640,"height":640,"size":103719,"type":"jpg","color":"d2454a","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQEASABIAAD/2wBDAAICAgICAQICAgIDAgIDAwYEAwMDAwcFBQQGCAcJCAgHCAgJCg0LCQoMCggICw8LDA0ODg8OCQsQERAOEQ0ODg7/2wBDAQIDAwMDAwcEBAcOCQgJDg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg4ODg7/wAARCAAKAAoDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAACAIG/8QAIRAAAgEDBQADAAAAAAAAAAAAAQIDBAURAAYHITEIEiP/xAAVAQEBAAAAAAAAAAAAAAAAAAAEBf/EAB4RAAEDBAMAAAAAAAAAAAAAAAECAwQABREhQYHB/9oADAMBAAIRAxEAPwA8cV8WcVX34ebqv98rlj3FSozQO9yjhWMBcj8zlnLHzrB8yND6e2Wxa2ZRUhQHIA+3nertk8y7UqQszgDOAHPWsoSSSSck+nVKMw4h1wlwnJpl4uMR2FFSiMlJCdkc6A8z2a//2Q=="}},"cover":{"cover":{"type":"image","data":{"uuid":"861886cd-1ef2-5754-af38-6219cc645017","width":1280,"height":511,"size":28049,"type":"jpg","color":"142434","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAAcFBQYFBAcGBgYIBwcICxILCwoKCxYPEA0SGhYbGhkWGRgcICgiHB4mHhgZIzAkJiorLS4tGyIyNTEsNSgsLSz/2wBDAQcICAsJCxULCxUsHRkdLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCwsLCz/wAARCAAKAAoDASIAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAAAAMH/8QAGhAAAQUBAAAAAAAAAAAAAAAAAAEDE1SSAv/EABUBAQEAAAAAAAAAAAAAAAAAAAQF/8QAFBEBAAAAAAAAAAAAAAAAAAAAAP/aAAwDAQACEQMRAD8AxOFmymFELNnnKkQNT3//2Q=="}},"cover_y":57},"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":5249500,"userId":172558,"count":0,"shareImage":"https://api.vc.ru/achievements/share/5249500"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":1625413,"userId":172558,"count":0,"shareImage":"https://api.vc.ru/achievements/share/1625413"},{"title":"5 лет на vc.ru","code":"registration_5_years","description":"Провёл 5 лет вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"a9140d54-73b8-5f40-afa8-449fbaafd42b","formats":{"glb":"https://static.vc.ru/achievements/whale.glb","usdz":"https://static.vc.ru/achievements/whale.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.533203125,"textY":0.658203125,"logoX":0.533203125,"logoY":0.77734375,"logoXNoText":0.4375,"logoYNoText":0.66015625},"id":398705,"userId":172558,"count":0,"shareImage":"https://api.vc.ru/achievements/share/398705"}],"lastModificationDate":1764939319,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":true,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":235819,"name":"Разработка","description":"Сообщество разработчиков: публикации о личном опыте, выдающиеся приёмы при решении рутинных задач, полезные материалы для профессионального роста.","uri":"/dev","avatar":{"type":"image","data":{"uuid":"fef5b5fb-e488-5b7f-8445-e3a26a910b44","width":1200,"height":1200,"size":7757,"type":"png","color":"343434","hash":"04042b2b1c1000","external_service":[]}},"cover":{"type":"image","data":{"uuid":"2a214cc5-35cc-58ca-bc07-fc1c892d2101","width":960,"height":280,"size":177,"type":"png","color":"343434","hash":"","external_service":[]}},"lastModificationDate":1642411346,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":false,"isDisabledAd":false,"nickname":"dev","isUnsubscribable":true,"badge":null,"badgeId":null,"isDonationsEnabled":false,"isOnline":false,"isPlus":false,"isUnverifiedBlogForCompanyWithoutPro":false,"isVerified":false,"isRemovedByUserRequest":false,"isFrozen":false,"isPro":false,"type":2,"subtype":"community"},"reactions":{"counters":[{"id":1,"count":12}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null,"keywords":[],"media":{"type":"image","data":{"uuid":"e3856fea-ebcf-5e93-9d46-f8ce2029a1d4","width":1344,"height":648,"size":61570,"type":"jpg","color":"29365a","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDAA4KCw0LCQ4NDA0QDw4RFiQXFhQUFiwgIRokNC43NjMuMjI6QVNGOj1OPjIySGJJTlZYXV5dOEVmbWVabFNbXVn/2wBDAQ8QEBYTFioXFypZOzI7WVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVlZWVn/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAAAgMEBv/EACEQAAEDAgcBAAAAAAAAAAAAAAEAAhEDEgQTISJRUpHB/8QAGAEAAgMAAAAAAAAAAAAAAAAAAAECAwT/xAAZEQEBAQEBAQAAAAAAAAAAAAAAEQESIVH/2gAMAwEAAhEDEQA/AM4MG0ti4S6I0+rZxlV8kZVPrU8UZnwpioDYU56YLRwEQP/Z"}},"customCover":null,"robotsTag":"noindex","categories":[],"isAnonymized":true}};