Итоги T-CTF 2025
Всех православных с прошедшим праздником Светлой Пасхи!
Завершился конкурс T-CTF 2025.
Хотелось бы выразить благодарность организаторам за организацию такого масштабного мероприятия.
Всё было круто: интересные задания, и отдельно хотелось бы выделить общую концепцию, вокруг которой строился конкурс.
Всё происходило в вымышленном городе Капибаровск, где необходимо было помочь капибарам в решении их проблем.
Из наблюдений хочу отметить, что организаторы взяли курс на больший охват аудитории из сопутствующих сфер (разработка, тестирование, QA, аналитики) и действительно командную работу. Обеспечили деление на две лиги: безопасности и разработки.
Мне кажется, это очень правильное решение — нужно увеличивать общее понимание практик инфобеза в индустрии.
Решал задания преимущественно по направлению web, могу отметить следующее.
Уровень сложности заданий и порог входа снизился. С учётом данного фактора, когда сложность решения задач снизилась, начал серьёзно играть фактор количества решённых задач в турнирной таблице, и здесь уже имеют преимущество полные команды в 3 человека.
Понравилось разнообразие задач: веб, инфраструктурные, шифрование, разбор трафика, сети, разработка.
Очень часто встречались задания на техническую логику и смекалку. Немного смутило, что это в объёме начало превращаться в какой-то общий типаж заданий, которых было немало. Как понимаю, это задания для широкого круга, но тем не менее.
Из личных результатов: выполнил 10 из 30 заданий, но думаю, можно было показать лучший результат. Задания решал один, без команды.
Постараюсь дать краткий обзор заданий, которые решал и чем-то запомнились.
Капибальпы
Простая сложность
Пранк над сноубордистами... да, с него и началось великое противостояние лыж и сноуборда в Капивеле.
На сайте горнолыжки в Капибальпах появилась новая функция. Капибары могли переводить минуты катания со своего ски-пасса друзьям. Например, если не успевали потратить всё до отъезда. Лыжники решили подшутить над сноубордистами: похитить всё их время катания до последней минуты.
Помнишь, как ты помог им это провернуть?
Первое задание после старта соревнований. Есть виртуальная валюта — минуты катания на склоне. Можно создавать страницу донатов со сбором минут. Здесь и кроется недочёт в логике.
Создаём под сноубордистом сбор донатов, под лыжниками переводим отрицательное число. Фактически загоняем сноубордиста в минус, а у лыжника, который "задонатил" отрицательное число, появляется нужный баланс. В ачивках профиля наблюдаем ключ.
Капибординг
Простая сложность
А это то, чем всё закончилось.
Сноубордистам захотелось восстановить справедливость после пранка лыжников. Ты придумал способ вернуть их время.
Лыжники активно критиковали сноубордистов в комментариях на страницах сноубордических донатов, а ты смог обратить это против них.
Продолжение первого задания, нужно помочь сноубордистам капибарам вернуть минуты обратно.
На всех страницах донатов от сноубордистов приходят лыжники и пишут гадости в комментариях. Наша задача — воспользоваться этим моментом.
Путем небольшого изучения выяснилось, что при создании донатов в заголовке и описании отсутствует должная фильтрация. Это позволяет опубликовать донат с XSS-уязвимостью, который выполнится у лыжника, пишущего гадости.
Доступа к внешней сети у лыжников, как выяснилось, нет — пришлось немного хитрить.
У меня получился следующий код:
<img src="/" onerror='fetch("/api/donation-pages/{ID_ОТДЕЛЬНОГО_ДОНАТА}/comments", {"headers": { "authorization": "Bearer " + localStorage.getItem("token"),"content-type": "application/json"},"body": "{\"content\":\"my token: " + localStorage.getItem("token") + "\"}","method": "POST","mode": "cors","credentials": "include"});'>
Был создан отдельный донат, куда в комментарии будут отправляться сообщения от лыжника, посетившего страницу, с его токеном. Используем полученный токен, подменяем у себя, переводим баланс обратно сноубордисту и забираем ключ в профиле.
Капибасни
Средняя сложность
Новости Капибаровска! В Лицее 42 вместо учителей литературы работает нейросеть. В нее загрузили учебный план, а капибарятам выдали доступ к чату.
Но на уроке по басням Ивана Лапкова нейронка сломалась. Теперь она не преподает, а раздает житейские мудрости направо и налево.
Почините учебный план, чтобы лицеисты снова могли спокойно учиться.
Предоставляется исходник и сам стенд для тестирования.
Регистрируемся на стенде и нужно сформировать учебный план для ученика так, чтобы по каждой из дисциплин набиралось необходимое количество занятий.
При регистрации для пользователя создаются задания от имени учителей, которые ученик не может удалить. Загвоздка в том, что от имени учителя Литературы создано слишком много заданий, а просто удалить их нельзя.
Задача: удалить лишние предметы из учебного плана и набрать отсутствующие, затем запустить учебный план.
Путем анализа исходников можно заметить, что в методе BulkDeleteLessons некорректно проходит проверка владельца урока. Это позволяет удалять даже учительские уроки - хотя система выкидывает ошибку, фактически удаление происходит.
func BulkDeleteLessons(s storage.Store) gin.HandlerFunc {
return func(c *gin.Context) {
var req BulkDeleteRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, NewWrongJsonDataError())
return
}
if len(req.IDs) == 0 {
c.JSON(http.StatusBadRequest, NewNoIDsProvidedError())
return
}
userID, exists := c.Get("user_id")
if !exists {
c.JSON(http.StatusUnauthorized, NewUnauthorizedError())
return
}
lessons, err := s.Lessons().BulkFindByIDs(userID.(uint), req.IDs)
if err != nil {
logger.Errorf("api.BulkDeleteLessons: %v", err)
c.JSON(http.StatusInternalServerError, NewDbError())
return
}
for _, lesson := range lessons {
if lesson.Owner != model.LessonOwnerStudent {
//ЗДЕСЬ ПРОБЛЕМА! ПРОПУЩЕН RETURN ПОСЛЕ СТАТУСА ОБ ОШИБКЕ!
c.JSON(http.StatusForbidden, NewNoPermissionsError())
}
if err := s.Lessons().Delete(lesson.ID); err != nil {
logger.Errorf("api.BulkDeleteLessons: %v", err)
c.JSON(http.StatusInternalServerError, NewDbError())
}
}
c.JSON(http.StatusOK, NewLessonsDeletedMessage())
}
}
Формируем запрос к API через массовое удаление, включая предметы учителя литературы. Затем добавляем в освободившийся учебный план новые предметы и нажимаем кнопку начала плана для получения ключа.
Капибратство
Средняя сложность
Студенческое капибратство «Тета Каппа» устроило испытание для первокурсников: они должны похитить билеты к экзаменам с сервера института.
Подмените их на другие — вымышленные. Не только же студентам можно дурачиться!
Предоставляется сам стенд и исходник.
Задача — прочитать флаг. Путем изучения исходников понимаем, что происходит локальное чтение файлов билетов. Сам ключ лежит в файле: .env.dist.
В исходниках наблюдаем следующий код:
class HTTPTicketController(Controller):
path = "/ticket"
@Route(http_method=HttpMethod.GET)
async def get_ticket(
self,
course_id: Annotated[str, Body(description="Course ID", title="Course ID")],
ticket_id: Annotated[str, Body(description="Ticket ID", title="Ticket ID")],
get_ticket_interactor: Depends[GetTicketInteractor],
) -> TicketSchema:
try:
ticket = await get_ticket_interactor(
course_id=str(course_id), ticket_id=str(ticket_id)
)
if not ticket:
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Ticket not found"
)
return TicketSchema(text=ticket)
except (FileNotFoundError, PermissionError):
raise HTTPException(
status_code=HTTPStatus.NOT_FOUND, detail="Ticket not found"
)
Который ссылается на get_ticket_interactor, а тот в свою очередь проваливается в метод:
async def read_ticket_file(
self, base_path: str, course_id: str, ticket_id: str
) -> str | None:
file_path = Path(base_path) / (course_id + "/" + ticket_id).replace(
"../", "replaced"
)
try:
if not file_path.is_file():
raise FileNotFoundError("File not found")
return file_path.read_text()
except (FileNotFoundError, PermissionError):
raise FileNotFoundError("Error accessing file")
Как видим, происходит чтение файла и некоторая защита от локального чтения с заменой: ../ на replaced, но явно недостаточная.
Формируем запрос вида: /api/ticket?course_id=&ticket_id=/app/.env.dist
Получаем содержимое ключа.
Капибарбер
Средняя сложность
Капибарбершоп Лапы-ножницы выпустил свое приложение. Перед стрижкой клиенты загружают туда референсы, а капибарбер получает пошаговую схему работы.
Кто-то загрузил файл странного формата — и остальные референсы пропали. Теперь всем подряд стригут каре, капибарочки в шоке, а капибизнесмены гуглят магазины париков.
Разберитесь, что произошло, и по дороге забирайте файл flag.txt
Предоставляется исходник и сам стенд.
Задача — забрать файл flag.txt, который хранится в недоступном из веба месте.
При изучении работы приложения обращаем внимание, что перед отправкой изображения (как будет выглядеть капибара после стрижки) происходит сжатие файла в ZIP-архив. Этот архив затем распаковывается на сервере и выкладывается в директорию, доступную из паблика.
Локально формируем symlink с указанием пути к флагу на диске, после чего добавляем эту ссылку в ZIP-архив. Затем загружаем данный архив через API.
После распаковки архива на сервере мы получаем публичную ссылку, которая ведёт на файл с ключом.
Переходим по ссылке — получаем флаг.
—
В заключение хочу сказать, что часть заданий действительно была посильна для решения даже без глубоких знаний в инфобезе.
Поэтому, если у вас есть желание, рекомендую присоединиться и попробовать свои силы в следующем году — даже если вы далеки от тематики информационной безопасности!
P.S. Отдельно хочу подчеркнуть законность и цели этого мероприятия, так как в прошлом посте появились люди, по причине явного непонимания критикующие такую деятельность.
Главная цель — не обучение кражи данных (статью 272 УК РФ никто не отменял!), а понимание принципов работы уязвимостей. Заниматься кражей данных и воровством чужого это плохо. Денег которые стоят того, что вас посадят нет.
Разработчикам и людям задействованным в IT, критически важно разбираться в этих механизмах, знать уязвимости и способы их реализации. Только так мы сможем предотвратить ситуации, когда данные компаний в стране утекают с пугающей регулярностью. Именно для этого и рассказывают о такой активности широкой аудитории.
И конечно подписывайтесь на мой Телеграм канал: https://t.me/+LWoSqRG4ZRk0ZTcy