A/D. А вы любите мороженое?
Henry Rozenttag @herrlestrateСпециально для @ctfby
Сегодня мы будем разбирать сервис с 3-ей открытой тренировки команды C4T BuT S4D, тренировка проходила 27 октября 2019 года.
Автор сервиса: Ivan Novikov
Установка
Первым делом скачаем репозиторий с сервисом.
Сделать это вы можете следующей командой:
git clone https://github.com/C4T-BuT-S4D/training-27-10-19

Вся дальнейшая работа будет происходить из каталога training-27-10-19.
Сервис находится в папке services/icecreams.
Чекер находится в папке checkers/icecreams.
Запускаем сервис
Переходим в папку с сервисом. Там мы увидим docker-compose.yml. Т.е. автор сразу предоставил возможность развернуть сервис с помощью докера одной командой.

Запустить сервис можно следующей командой:
docker-compose up --build -d
Остановить:
docker-compose down -v
После запуска сервиса, проверяем доступность сервиса по адресу http://localhost:5555

Прежде чем читать дальше, рекомендую самостоятельно разобрать сервис и только потом читать мои рассуждения.
Функционал сервиса
Сервис предлагает нам следующие функции:
- Регистрацию пользователя (Register)
- Вход пользователя (Login)
- Просмотр своего списка мороженого (My icecreams)
- Просмотр последних пользователей сервиса (Last users)
- Добавление в список нового мороженого (Add icecream)
Если мы не вошли в аккаунт, функции My icecreams и Add icecream не будут работать.
Ищем место хранения флагов
У нас всего несколько мест, где могут храниться текстовые данные:
- Логин пользователя
- Пароль пользователя
- Список мороженого
Очевидно, что самым вероятным местом хранения флагов будет список мороженого (My icecreams).
Хранить флаг в логине пользователя несколько бессмысленно, поэтому этот вариант мы не рассматриваем, аналогично и с паролем пользователя.
Флаги ищем в My icecreams.
Формулируем задачу
Мы знаем, где искать флаги. С помощью Last users мы можем получить последних 40 пользователей (точнее, их логины).
Тогда нам необходимо получить доступ к каждому аккаунту пользователя, а после "вытянуть" его список.
Алгоритм действий:
1) Получаем список аккаунтов
2) Осуществляем взлом аккаунта
3) Забираем список мороженого
4) Из этого списка выбираем все флаги (формат флага: [A-Z0-9]{31}=)
Ищем баги в коде
Сервис написан на Pascal (.NET). Поэтому рассматривать мы будем файлы *.pas.

Точка входа (или запуска) приложения - main.pas.

В router описаны все ссылки, которые обрабатываются сервисом.
В controllers.pas написана реализация основной логики сервиса:
- GetUser()
- GetLastUsers()
- AddUser()
- GetIcecreams()
- AddIcecream()
Из файла следует, что все данные хранятся в папке data, файл users.txt хранит логин и пароль через пробел.
Список мороженых хранится в результате работы функции Createmd5(login) + ".txt".
В crypto.pas реализована функция Createmd5(login).
В handlers.pas описан обработчик входящих соединений.
В router.pas описан распределитель этих соединений.
И в файле session.pas описана работа с сессией пользователя.
Здесь стоит обратить внимание на константу secret и то, где она используется. А точнее, на передаваемые аргументы для функции Createmd5().

Становится понятно, что сессия генерируется следующим образом:
Берётся логин пользователя, добавляется символ ":", а после добавляется результат функции Createmd5(user + secret). Секретная константа прописана статически, и поэтому мы можем "угадывать" сессию пользователя, т.е. осуществлять захват флага.
Фиксим уязвимость с константой
Достаточно сменить значение переменной secret на что-нибудь другое, например: 'itsmoresecretconst'.
В принципе, этого достаточно, чтобы закрыть уязвимость.
Эксплуатируем найденную уязвимость
Для начала напишем функцию, которая будет генерировать нам сессию для пользователя:

Переменная login будет отвечать за логин пользователя, а переменная secret - за секретную константу.
Теперь необходимо получить список пользователей сервиса. Напомню, что делается это запросом в /lastusers.

Попробуйте сами понять, почему в r.text.split("<br>") мы рассматриваем с первого (не нулевого!) по предпоследний элемент.
Мы знаем логин, мы знаем константу, мы умеем генерировать куки. Что осталось сделать? Правильно, получить список флагов:

И проверим список мороженого на наличие флага:

Итоговый код сплоита по константе:

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

И код, который возвращает нашего пользователя с паролем:

Уже нашли уязвимость? Нет? Ничего страшного, заметить сразу её проблематично.
Уязвимость с пробелом
В чем её смысл?
Мы можем зарегистрировать пользователя с пробелом в конце логина. Функция s.ToWords разбивает нашу строку по пробелам.
Допустим, у нас есть пользователь "lucky624" с паролем "ctfthebest". Когда функция GetUser() начнёт свою работу, она разобьет строку "lucky624 ctfthebest" и получит его логин и пароль.
Но если добавить запись "lucky624 myeasypass" (логин имеет вид "lucky624 "), то после разбивки этой строки, функция s.ToWords вернет логин "lucky624" и пароль "myeasypass". Т.е., из-за того, что функция GetUser() прочитывает весь файл, она проигнорирует первую запись (логин без пробела) и вернет пароль на вторую запись (с пробелом). Таким образом, можно получить легко доступ к аккаунту пользователя.
Фиксим проблему с пробелом
Я считаю, что можно придумать много разных способов для решения этого бага. Но мы (Bulba Hackers) остановились на варианте с хранением данных в Base64.
Для реализации фикса необходимо исправить три функции: AddUser(), GetUser() и GetLastUsers().
Подключим нужные библиотеки:

И начинаем с функции GetUser():

GetLastUsers():

AddUser():

Важно! Необходимо удалить users.txt и создать его заново (или сделать поддержку старого формата).
Если вы сделали всё правильно, дальнейший эксплоит не сработает.
Эксплуатируем уязвимость с пробелом
Будем писать код на базе предыдущего сплоита.
Получаем пользователей:

Регистрируем пользователя с пробелом:

Входим под пользователем без пробела:

И вытягиваем флаги:

Полный код сплоита для пробела:

Последняя и самая сложная уязвимость
Скорее всего, кто-то задерживал внимание на паре строчек кода, но так и не смог ничего найти. Да, найти эту уязвимость без наличия большого опыта и внимательности в .NET крайне проблематично.
Итак, последняя бага в том, что мы можем получать доступ к любому файлу в нашем сервисе.


Как видно из кода выше, процедура HandleRoot() проверяет, что текст после последней точки ведёт на файл *.jpg, *.html, *.css или *.ico формата.
Но в чём именно проблема?
Проблема в том, что функция ctx.Request.Url.ToString() возвращает полный адрес с параметрами. Поэтому мы можем сделать запрос вида:
URL + '/data/users.txt?a=.html'
Этот запрос будет отвечать требованиям case ext of, и нам загрузится файл users.txt, откуда мы сможем вытащить всю интересующую нас информацию.
Как правильно эксплуатировать такую уязвимость
В первую очередь, мы можем вытащить список пользователей и просто входить под каждым и забирать флаги.
Кстати, с учетом фикса проблемы с пробелом, атакующий вряд ли бы заметил, что запись в файл изменена. Поэтому мы бы обошли эту проблему стороной. Но если у нас ручками проверят, как мы записывали файл - всё будет плохо.
Во вторых, мы можем вытаскивать исходные код сервиса противников. Исходный код любого файла противника.
Догадались? Именно!
Мы можем вытащить файл session.pas, узнать секретное слово для MD5 хеширования сессии (только если не переписывали полностью создание сессии!) и снова эксплуатировать первый сплоит (известное секретное значение).
Код достаточно простенький:

Писать полный сплоит я не буду, так как в большинстве случаев требуется ручками просмотреть файлы противника и придумать к каждому свой подход.
UPD от Автора сервиса:
На самом деле, данные пользователей хранятся по пути "/data/Createmd5(login)". Поэтому мы могли получить содержимое файла кодом выше.
В качестве домашнего задания попробуйте написать такой сплоит самостоятельно.
Защищаемся от вытаскивания файлов
И первая идея.....
Поменять названия у файлов. Тогда противнику придется слепо искать файлы и то, как их называли. Но идея недостаточно хороша, поэтому идём к следующей, более полной идее:

serveStatic() использует функцию ctx.Request.Url.Localpath (именно поэтому, удаётся вытаскивать файлы).
Тогда что нужно сделать? Правильно!

В HandleRoot() меняем строчку
var parts := ctx.Request.Url.ToString().Split('.');
На строчку:
var parts := ctx.Request.Url.LocalPath.ToString().Split('.')
Всё, этого хватит для фикса.
Заключение
На сегодня всё. Если вы хотите больше полных разборов сервисов - вступайте в канал @ctfby.