Автор: Иван, ведущий инженер по информационной безопасности
Давно я не публиковал свои похождения по CTF — пора это исправить! За прошедшее время мне удалось попробовать себя в трех турнирах. В ImaginaryCTF, посвященном кибербезопасности, Umasscybersec, адаптированном под Губку Боба, и n00bzCTF, который сделали для опытных n00bz.
Категорий задач на турнирах было много. Среди них — OSINT, Reverse, Forensics, Pwn и Web. Последнее направление для меня самое интересное, поэтому его я и выбрал. Что из этого получилось, показываю в статье!
Readme
Задание
Попробуйте прочитать файл file.txt.
readme.chal.imaginaryctf.org →
Решение
1. Перехожу по ссылке и убеждаюсь, что она работает. Открываю код страницы.
2. В коде страницы ничего интересного — надо искать глубже. Запускаю dirsearch.
Сервер отвечает ошибкой 404 на запросы страниц flag.txt и index.html. А в файле default.conf с настройками nginx находится такая конфигурация:
server {
listen 80 default_server;
listen [::]:80;
root /app/public;
location / {
if (-f $request_filename) {
return 404;
}
proxy_pass http://localhost:8000;
}
}
В директиве location есть условие проверки наличия файла. Если запрашиваемый в URL файл существует, то сервер возвращает ошибку 404. Результаты поиска dirsearch и эта конфигурация подтверждают, что на сервере есть оба файла: flag.txt, index.html. Необходимо как-то обойти это условие — есть несколько вариантов.
3. Вручную или с помощью модуля Intruder в burpsuite можно перебрать предлагаемые варианты нагрузок. В результате вместо /flag.txt можно запросить страницу по такой форме:
GET /flag.txt/.
4. Сервер вернул внятный ответ — флаг в кармане!
Journal
Задание
Дорогой дневник, в этом приложении нет LFI.
journal.chal.imaginaryctf.org →
Решение
На страницы находятся лишь ссылки на пять файлов с текстами. Однако наибольший интерес вызывает URL:
http://journal.chal.imaginaryctf.org/?file=file1.txt
Имя запрашиваемого файла передается в параметре file, который потенциально может быть уязвимым.
1. Смотрю приложенные конфигурационные файлы. Начинаю с index.php.
В нем находятся те самые ссылки на текстовые файлы, а также некая функция assert, которая вместе с strpos проверяет наличие многоточия в имени запрашиваемого файла. Вероятно, так реализована защита от cmd-инъекции. Далее просто проверяется наличие файла и отдается содержимое, если условие истинно.
2. По логике кода понятно, что можно попробовать обратиться по пути flag.txt. Иду смотреть Dockerfile.
Самая интересная строка в файле выглядит так:
RUN mv /flag.txt /flag-’tr -dc A-Za-z0-9 < /dev/random | head -c 20’.txt
В имя файла flag.txt добавляется двадцать случайных символов. При этом сам файл с флагом находится в корневом каталоге. Значит, чтобы добраться до ОС сервера, необходимо работать с функцией:
assert("strpos('$file', '..') === false") or die("Invalid file!");
3. Так как передаваемое в параметр file значение используется в функции strpos без какой-либо валидации, можно провести php-инъекцию. Она будет выглядеть следующим образом:
‘).system(“ls”);//
В результате строка кода примет такой вид:
assert("strpos('‘).system(“ls”);//', '..') === false") or die("Invalid file!");
Все, что будет после знака комментария «//», программа проигнорирует. До него будет выполнено strpos(‘’).system(‘ls’). Кроме того, должна появиться ошибка выполнения функции assert, т. к. она принимает два параметра.
4. Пробую подать нагрузку:
Все работает — файлы текущего каталога отдаются.
5. Поднимаюсь по файловой системе до корневого каталога, где должен находиться файл с флагом.
Действительно, файл есть. Брутить имя, конечно, было бы очень долгим занятием.
6. Читаю файл и получаю заветный флаг!
Passwordless
Задание
Устали от хранения паролей? Не беспокойтесь! Этот сверхзащищенный веб-сайт не содержит паролей!
Решение
На странице задания встречает такая форма ввода логина:
Если ввести любое имя, то происходит редирект.
1. В коде страницы нет ничего интересного — иду в исходники. В app.py есть следующий скрипт маршрутизации запроса:
@app.route('/<uid>')
def user_page(uid):
if uid != str(uuid.uuid5(leet,'admin123')):
return f'Welcome! No flag for you :('
else:
return flag
Флаг отдается только в случае, если перейти по ссылке:
str(uuid.uuid5(leet,'admin123'))
В самом начале файла задается значение переменной leet:
global leet=uuid.UUID('13371337-1337-1337-1337-133713371337')
2. Пробую сформировать целевую ссылку — для этого в IDLE выполняю часть кода из конфигурационного файла:
import uuid
leet=uuid.UUID('13371337-1337-1337-1337-133713371337')
print(uuid.uuid5(leet,'admin123'))
На выходе получаю следующую строку:
3c68e6cc-15a7-59d4-823c-e7563bbb326c
3. Используя полученную строку, собираю новую ссылку и пробую обратиться по ней:
http://24.199.110.35:40150/3c68e6cc-15a7-59d4-823c-e7563bbb326c
Готово — сервер возвращает флаг: n00bz{1337-13371337-1337-133713371337-1337}.
Holesome Birthday Party
Задание
Тебя только что пригласили на день рождения Губки Боба! Но он решил испытать вашу дружбу, прежде чем выдать входной билет. Сможешь ли ты пройти испытания и заслужить право попасть на вечеринку?
holesomebirthdayparty.ctf.umasscybersec.org →
Решение
Губка Боб ждет на своем дне рождения только настоящих друзей — надо обязательно попасть на тусовку!
1. Перехожу по ссылке, вижу первое задание:
Похоже, необходимо поработать с HTTP-запросом и в заголовке User-Agent указать «Bikini Bottom»:
User-Agent: Bikini Bottom
Отлично! Первое задание пройдено.
2. Далее необходимо поменять дату в запросе через заголовок Date. Гуглю — оказывается, у Губки Боба день рождения 14 июля. Меняю дату на нужную.
Готово — перехожу к следующему заданию.
3. Теперь Боб хочет учить французский язык, поэтому просит говорить соответствующе.
В HTTP-запросе язык указывается в заголовке Accept-Language — меняю на fr:
Прекрасно, двигаюсь дальше.
4. Именинник просит захватить печенье со вкусом шоколадной крошки. Пойду навстречу и добавлю в HTTP-запрос Cookie с требуемым значением.
Губка Боб выдал билет на тусовку, но что-то в нем не так…
В Cookie прилетает новое значение (Login=eyJsb2dnZWRpbiI6IGZhbHNlfQ==) — логин в BASE-64 кодировке. У Burpsuite есть модуль Decode, воспользуюсь им:
Меняю на true и отдаю в заголовке. Финальный HTTP-запрос (с учетом предыдущих заданий) будет следующим:
Флаг получен — вечеринка тусовка с грустным Сквидвардом началась!
CTF турниры помогают расширить знания о, казалось бы, понятных функциях языков программирования или конфигурации nginx. А также позволяют поработать со структурой протокола HTTP.
Надеюсь, подобный разбор заданий поможет начинающим игрокам в CTF понять способы подхода к процессу поиска флагов. До встречи в следующих частях!