27.04.2025 16:35
71

T-CTF. Write-up соревнования по кибербезопасности

Capture The Flag (CTF) - соревнования для белых хакеров, которым за ограниченное время нужно "взломать" определенный сервис или приложение либо получить несанкционированный доступ к информации.

T-банк прокодит CTF третий год подряд, насколько я понимаю. Поучаствовать меня пригласил коллега, который принимал участие в прошлом году в CTF, еще работая в прошлой компании. Мне тема зашла - я интересовался различными баг-баунти давно, но только в теории.

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

Но CTF это непосредственно практика хакинга - одно дело знать что такое SQL инъекция и как от неё защититься, и совсем другое - успешно эксплуатировать её.

Собственно цели на участие у меня стояли следующие:

  • попробовать себя в этой сфере;
  • проверить насколько эффективными в таких задачах покажут себя ИИ;
  • лучше понять сферу эксплуатации уязвимостей, чтобы учесть при разработке.

Давайте пройдем по части задач, которые решить в ходе соревнования удалось. Легенда в рамках T-CTF такая, что действие происходит в вымышленном городе "Капибаровск", где живут капибары, так что названия заданий будут соответствовать. Так называемый write-up заданий в порядке их решения мной.

Капиблагость [средний уровень]

Дано: сайт, который дает при регистрации 1$ баланса, за 1$ можно купить 101 минуту воодушевляющих лекций. Также без потерь можно конвертировать и обратно 101 минуту в 1$. Флаг выдается, когда скуплены все минуты. Также есть исходники сайта на go.

Решение

Открываем исходники в Cursor - просим Sonnet 3.7 найти потенциальную уязвимость. Он быстренько дает ссылку на исходники:

user, exists := loadUser(username)

amountStr := r.FormValue("amount")
amountBaks, err := strconv.ParseUint(amountStr, 10, 64)

if err != nil || amountBaks <= 0 {
    http.Error(w, "Неверное количество минут", http.StatusBadRequest)
    return
}
dirStr := r.FormValue("direction")

if dirStr == "min_to_baks" {
    mins := amountBaks * rate_min_to_baks
    if user.BalanceMinutes < mins {
            http.Error(w, "Недостаточно минут", http.StatusBadRequest)
            return
    }
    user.BalanceMinutes -= mins
    user.FreeMinutesRest += mins
    user.BalanceKapibaks += amountBaks
} else if dirStr == "baks_to_min" {
    amount_mins := amountBaks * rate_min_to_baks
    if user.BalanceKapibaks < amountBaks {
            http.Error(w, "Недостаточно капибаксов", http.StatusBadRequest)
            return
    }
    user.BalanceMinutes += amount_mins
    user.FreeMinutesRest -= amount_mins
    user.BalanceKapibaks -= amountBaks
} else{
    http.Error(w, "Не поддерживается", http.StatusBadRequest)
    return
}
saveUser(user)

Первая очевидная мысль - race condition. Если одновременно у нас может выполниться 2 операции одного типа - в результате мы получим х2 минут либо баксов. Идея увенчалась неудачей - loadUser и saveUser хранили состояние в файле и даже если что-то и выполнялось параллельно - файл все равно перезаписывался корректными данными.

Ничего другого я не придумал, поэтому решил побрутить логин/пароль - здесь также Cursor написал мне скрипт на питоне и даже составил словарь с учетом входных данных. Причем скрипт не просто проверял подходят ли данные, но и баланс счета. Так я за минуту получил доступ к учетке с логином и паролем capibara123 / capibara123:

001-blagost-bruted

Это очевидная недоработка разработчика задания - нужно было делать отдельные БД для пользователей, чтобы такое исключалось. Здесь мы просто покупаем / продаем минуты и получаем флаг.

Какая уязвимость в коде была на самом деле:

type User struct {
    Username      string `json:"username"`
    Password      string `json:"password"`
    FreeMinutesRest      uint64 `json:"freeins"`
    BalanceMinutes uint64 `json:"minutes"`
    BalanceKapibaks uint64 `json:"kapibaks"`
}

Переполнение uint64 при корректно подобранном значении (max uint64 + 101) при продаже минут даст нам достаточное количество баксов для покупки всех свободных минут. Об этом я узнал уже из чата после окончания мероприятия.

Капитальный ремонт [средний уровень]

Дано: сайт для внесения показаний счетчика с фото. Есть архив с исходниками и начальным дампом сайта, где видно что id мэра = 1. По истории мэр потратил на свой дом кучу денег и нужно было найти куда они ушли. В таблице users есть поле address - флаг лежит в нем.

Решение

Опять просим Cursor найти уязвимость и он быстренько находит SQL инъекцию, здесь бэкенд уже на Java:

public void create(long id, String measurement) {
    jdbcTemplate.update(
            String.format(
                    "INSERT INTO measurements (account_id, measurement) VALUES (%d, '%s')",
                    id,
                    measurement
            )
    );
}

При этом сам сервис получает на вход фото счетчика, сначала проверяет есть ли на фото счетчик, дальше распознает показания. Насколько понял - запросами к api gpt-4o.

Т.е. нам нужно внедрить строку запроса в фото со счетчиком. Я с рисованием счетчика особо не страдал, Cursor это уже учел и написал скрипт на питоге, который рисовал мне картинку с "показаниями" уже с "счетчиком" на ней. Еще какое-то время ушло на то, чтобы понять как эксплуатировать SQL Injection правильно, практики у меня в этом не было. В итоге сработал такой вариант:

002-meter

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

Капибегущая строка [сложный уровень]

Дано: видео, на котором "злоумышленники" взломали освещение здания и запустили на нем бегущую строку. На видео видно начало - TCTF{ - это начало флага. Также есть дамп wireshark сигналов, передаваемых в этот момент.

Решение

Открываем дамп wireshark'ом, видим там zigbee трафик, в описании некоторых записей есть явное OnOff: On - т.е. включение/выключение устройств. Т.е. это сигналы для каждого отдельного пикселя, нужно только восстановить очередность и корректно прочесть.

Изначала пытался как-то сконвертировать все адреса конечных устройств в матрицу, но потом пришла гениальная идея - у нас же бегущая строка, т.е. нам достаточно получить только адреса одного столбца (5 пикселей в высоту - 5 адресов нам нужно). Первые символы мы знаем, разница между кадрами ~150мс - на питоне пишем скрипт, который переводит логи wireshark в последовательность кадров, адреса устройств и состояние, при этом отбрасывает лишнее. Вот так лог выглядел после выкидывания всего мусора (первые 5 кадров после очистки экрана):

2 143 7.373755 0x010c On
3 145 7.528859 0x010b On
4 147 7.683991 0x010a On
4 149 7.687106 0x0119 On
4 151 7.690137 0x0126 On
4 154 7.693926 0x0133 On
4 156 7.696955 0x0140 On
5 158 7.851796 0x0109 On
5 160 7.854978 0x0118 On
5 162 7.857955 0x0119 Off
5 165 7.861403 0x0125 On
5 167 7.864484 0x0126 Off
5 169 7.868092 0x0132 On
5 171 7.871094 0x0133 Off
5 173 7.874080 0x013f On
5 175 7.877072 0x0140 Off

Очевидно, что 0x010c - это правый верхний пиксель, в 4 кадре загорается весь правый столбец (палка от буквы T) - т.е. просто перебираем все эти адреса и покадрово скриптом рисуем матрицу, чтобы получить флаг (прогоняем строку программно по кадрам и восстанавливаем в консоли). Если верно определили адреса - получаем верный ответ (в консоли):

003-strings

Капибаксы [средний уровень]

Дано: биржа валют, у нас есть информация, что курсы установлены не совсем корректно и есть уязвимая цепочка конвертаций.

Решение

Cursor пытался получить цепочку перебором - бесполезно, была еще куча путаницы с курсами и математикой конвертации в сгенерированных скриптах, пришлось править руками.

Далее скормил ту же задачу o3 - и он написал скрипт поиска цепочки методом Беллмана - Форда, цепочка нашлась:

CAB → BTC → TJS → KPW → ETB → MNT → AOA → PHP → THB → JOD → GEL → STN → SHP → BAM → MYR → PLN → VND → MZN → NGN → AFN → SLL → AZN → BGN → HNL → MGA → CAB

Давала примерно 9% прибыли после прохождения, а нужно было из 100 баксов сделать 13300, после этого "купить франшизу" - тем самым получить флаг. о3 в своем скрипте сразу верно написал запрос и на покупку франшизы и парсинг флага, так что после запуска в консоли и ожидания, пока нужная сумма накрутится - я получил распарсенный флаг в консоли.

Капибарбадос [средний уровень]

Дано: исполняемый файл в формате elf с консольным приложением, в котором происходит регистрация на рейс и прохождение на посадку. Также ssh для подключения к этому сервису для регистрации и посадки.

Решение

Отправляем Cursor искать в чем дело - очень быстро находим, что:

  • при регистрации на рейс все получают место по порядку;
  • 104 место - место пилота, под ним тоже можно пройти на посадку;
  • для прохождения на посадку пилота нужен 8-значный пин-код;
  • после того как пилот проходит на посадку - нужно сыграть минуту в какую-то игру, после чего дают флаг.

Какое-то время ушло на написание скрипта для брутфорса пинкода, курсор пытался написать словарь и пробовал варианты вроде 12345678, но это не привело к успеху. o3 написал скрипт для полного перебора (напоминаю, все это по ssh происходит) - и при его запуске оказалось, что пинкод 00000000 подходит - возможно, была уязвимость в коде валидации, т.к. генерация явно была рандомной - здесь я глубже не копал.

Скрипт закрыл, подключился руками, нарегистрировал 104 пассажиров, прошел на посадку пилотом, ввел код и вуаля, играем в флаппи-берд:

004-flappy

Выводы

005-team

Команда решила 19 заданий из 30 и по окончании мероприятия занимает 42 место из 1783 прошедших вводное задание (в лиге разработки - это новички):

006-rating

Далее будут 2 месяца проверок следования командами правил - отсутствие шаринга флагов друг с другом, соблюдение количества человек в команде, критериев опытности. В прошлом году за нарушение правил выкинули из топа команд 15. Как будет в этом - неизвестно, но для получения самого минимального утешительного приза в виде плюшевой капибары - нужно получить хотя бы 20 место. Вряд ли с 42 места возможно попасть на 20, но и цель была не в получении игрушки.

Самое удивительное во всем этом - то, что ни разу ни одна из моделей ИИ не отказалась что-то сломать. Просто пишешь ей ключевое слово CTF - и все, она считает что генерировать SQL инъекции, брутфорсить пароли и взламывать исходники - это святое дело. Так что командам Alignment'а моделей еще есть куда стремиться.

Ну и главный вывод - сейчас уже не нужно досконально разбираться в аспектах дизассемблирования сборок, дешифровке паролей dpapi, sql- и других инъекций - ИИ снижает порог входа кардинально. Достаточно общих знаний, способности быстро ориентироваться в новом материале и какой-то природной сообразительности - и можно успешно решать задачи и попадать в топ-50 лучших команд.

Комментариев пока нет

Последние статьи