ShadowCTF 2019 Finals. Youndex Writeup

ShadowCTF 2019 Finals. Youndex Writeup

Ivan Novikov @jnovikov

Нам давался сервис, который позволял добавлять сайты в поисковую базу и искать по ней. Например, добавим сайт http://2019.shadowctf.ru:

А теперь попробуем поискать по нашей базе:

Окей, работает. Теперь давайте посмотрим на исходники, которые были нам даны и постараемся найти где искать флаг.

Недолгими(или нет) поисками находим файл config.py, где видим переменную FLAG

Окей. Дальше есть два пути раздумий:

Первый — переменная кладется в конфиг, может быть можно как-то его получить из питоновского кода.

Второй — переменная лежит в файле, может быть этот файл можно как-то прочитать.

Единственный разумный вариант для первого — RCE или SSTI, но в коде нет намеков на такие уязвимости, шаблоны используются правильно, с pickle и прочим работы нет.
Остается чтение файлов. Приложение почти никак не работает с файлами, только отдает статику. Статику отдает стандартный модуль Flask'a, вряд ли он уязвим.

Однако файл нам нужно прочитать. Тут остается только одно — у нас есть штука, которая качает сайты в БД. Может она может закачать наш файл ?
Уязвимости такого типа называются SSRF, это когда вы делаете запрос к внутренней инфраструктуре сервисов, тем самым получая какую-то ценную информацию.

В нашем случае поможет следующий факт — если библиотеки честно обрабатывают url, то они должны поддерживать очень много протоколов, в том числе протокол file://, который должен отдать локальный файл.
"Это то, что нам нужно", — подумали вы. И вы правы, но давайте посмотрим на код, который добавляет url.

Окей, тут есть функция validate, погнали ее посмотрим.


Данная функция как бы намекает нам, что SSRF мы вам не дадим.
Запрещены все виды локалхоста, а так же все протоколы кроме http и https. Кстати, здесь не запрещены редиректы, так что в теории можно достучаться до локалхоста с помощью редиректа(это вам на будущее), но в данном таске это не поможет, нам нужно прочитать локальный файл.

Окей, казалось бы все плохо, но давайте дальше почитаем код самого crawler'a(штука которая ходит по url'ам)

Давайте посмотрим на этот код и ответим себе на вопрос: "Чем занимается каждая функция ?"

Функция crawl_site берет url, вызывает функцию crawl_url, которая видимо возвращает content и список url'ов, которые есть на этом сайте, добавляет информацию в БД, а потом проходится по свеже-полученным url'ам и начинает обходить их. Получается такой рекурсивный обход сайтов.

Функция crawl_url делает запрос к сайту, парсит его html, возвращает в ответ текст, и все ссылки, которые содержатся в тэгах <a>.

Ну и последняя функция full_url, она используется для того, чтобы добавить к ссылке слева url, если это нужно.
Многие ссылки на сайтах относительные, например /info, очевидно, что к таким ссылкам нужно добавлять текущий url сайта, чтобы было http://some_site.net/info

А есть ли здесь проверка на то, что url "хороший" ? Нет, ее нет

Тогда наша задача найти такой сайт, который отдаст в ответ любой html, где будет текст <a href="file:///app/app/settings.py">.

Но не все так просто, вспомните про функцию full_url: если в нашей ссылке не будет слова http, то сервис добавит слева url сайта. И получится что-то около

http://some_site.netfile:///, что нас вообще не устраивает.
Что же делать ? Тут нужно было заметить странную особенность:

Функция full_url проверяет, что в строке ссылки нет подстроки http. А должна проверять, что ссылка начинается с подстроки http.

А как это взломать ? Здесь поможет тот факт, что в url'ах много разных компонент, и есть компоненты, которые не несут нагрузки в нашем случае. Например — якоря(все что идет в конце url после символа '#'). Если мы добавим в конец якорь 'http', то в ссылке будет подстрока http, но никакой смысловой нагрузки нести не будет.

Осталось собрать наш пэйлоад. Я буду использовать сайт https://gist.github.com.

Добавляем новый gist: https://gist.github.com/jnovikov/d6ffb1c5505e8f80d155b890d2827984

И скармливаем сайту ссылку на raw содержимое gist'a:


Дальше ищем в базе по слову "FLAG" — именно так называлась переменная в конфиге.

Таск решен :) Флаг — shadowctf{yeap_we_love_ssrf_too}

К сожалению таск не решила ни одна команда, хотя я не считал его гробом :(

Надеюсь, что этот разбор был познавательным, и таск вам в итоге понравился.

Report Page