
В марте состоялся второй эпизод DevSecOps-митапа CyberCamp, в программу которого вошли киберучения на платформе Jet CyberCamp. Три задания, максимально приближенных к реальным задачам специалиста по безопасности приложений — в этом материале разберем их все. Вы узнаете, какие идеи закладывались в задания, где авторы расставили ловушки, и какие реальные кейсы из практики за ними стоят. P.S. А пока вы с головой уходите в разбор этих кейсов, мы уже вовсю готовимся к новому формату — 17 июля в Москве на свежем воздухе пройдёт первый опен-эйр-фестиваль «Лето в Киберкэмпе 2026» с новыми киберучениями, докладами про ИИ, DFIR и живым общением у воды. Не пропустите!
Задание 1. Профиль небезопасности
Вам как специалистам по безопасности контейнерных сред поручено изучить текущий профиль /etc/apparmor.d/task-b-66 , выявить недочёты и исправить их так, чтобы веб-сервер заработал корректно, а вредоносная активность внутри контейнера была заблокирована.
Вам предоставлен доступ к тестовому кластеру Kubernetes с развёрнутым веб-сервером Apache HTTP Server, защищённым профилем AppArmor. Тем не менее, политика компании близка к zero-trust, в связи с чем предоставленный вам доступ весьма ограничен: вы можете только наблюдать за состоянием рабочей нагрузки через k9s , управлять профилем AppArmor (перезагружать и удалять) через sudo apparmor_parser , а также перезапускать поды. Прямого доступа к контейнеру нет.
Вперед, к победе!
Проваливаемся в пользователя. Пытаемся сориентироваться. Время не ждёт!
Sudo-команды для нас недоступны. Все, что есть — управление одним-единственным профилем task-b-66 . На admin.conf надеяться не приходится, docker недоступен, skopeo тоже. Беда.
Благо, команда DSO благословила нас даром в виде k9s и доступным контекстом куба. Проваливаемся в контекст, открываем кластер, обнаруживаем себя всё такими же зажатыми, как и в ОС.
Находясь в namepace task-b-66 без возможности просмотра других, видим умирающий страшный CrashLoopBackOff под:
Начинаем разбираться. Первым делом обнаруживаем, что нам доступны describe, logs и delete, но запрещен exec.
Перезапуск пода, даже будучи ультимативным способом решения всех бед, в нынешних обстоятельствах не помогает.
Изучаем describe, но, увы, здесь мы видим базовый минимальный одноподовый deployment с образом httpd:2.4.66-alpine3.23 (запомните этот тег, он нам понадобится!) и подключенным профилем AppArmor task-b-66 :
Оставшись почти ни с чем, переходим к log и, наконец, натыкаемся на зацепку. Буквально в первой же строке журнала обнаруживаем виновника торжества — rm: Permission denied:
Вероятно, увидев ошибку, в моменте вы сбились с мысли и не понимаете, что это за дикий rm такой и откуда он там возник, или, скажем, с чего это в контейнере с веб-сервером пытается выполниться rm.
Если вы уже смешарик сакральные знания об источнике всех бед уже открыты для вас, то можете смело пропустить следующее пояснение и смотреть сразу в ответы.
Если же вы новичок и вам интересно узнать, что происходит, тогда добро пожаловать в краткий экскурс по httpd:
Откуда взялся rm в httpd?
Внутри контейнера httpd:2.4.66-alpine3.23 указана следующая команда запуска:
Мы понимаем, что binary для веб-сервера вообще-то называется просто httpd.
Начинаем копать — находим исходник httpd-foreground и узнаём, что это sh-скрипт следующего содержания:
Вот откуда ноги растут! Тот самый rm, сгубивший несчастный под, выполняется аккурат перед запуском самого httpd.
Разобраться подробнее с содержимым образа httpd:2.4.66-alpine3.23 и предварительной настройкой httpd при запуске вам помогут вот эти сорцы:
Заглядываем в профиль /etc/apparmor.d/task-b-66 :
Получается, что путь /usr/bin/** разрешен, /usr/bin/rm — тоже разрешен, но под не запускается? Произвол!
Вспоминаем, что имеем дело с образом httpd:2.4.66-alpine3.23. Ответ кроется в великом и ужасном имени базового дистрибутива alpine:
Так как образ собран на alpine, внутри контейнера httpd:2.4.66-alpine3.23 попросту отсутствует бинарный файл rm.
Вместо бинарного файла rm в файловой системе красуется ссылка на совершенно другой путь к совершенно иному файлу: /bin/busybox
Вносим исправление в профиль:
1. Заменяем разрешенный путь к бинарным файлам:
2. Разрешаем бинарный файл busybox для alpine:
Сохраняем изменения, применяем профиль и перезапускаем под.
Победа?
По возвращении в k9s нас вновь встречает треклятый CrashLoopBackOff.
Проваливаемся в log и снова Permission denied! Неужто не сработало? Может, ошибка? Ан нет, не ошибка, или, правильнее сказать, не наша:
Сервер не может инициализироваться: bug или getaddrinfo fail. В пору открывать bug bounty, звать спецов да пусть разбираются. Но спец, всё же, сегодня — мы.
Принимаем на себя риск ошибиться с выбором и идем по пути наименьшего сопротивления — преследуем нарушителя getaddrinfo.
Функция getaddrinfo () выделяет место и инициализирует связный список структур addrinfo , по одной на каждый сетевой адрес, который совпадает с node и service , в соответствии с любыми ограничениями, наложенными hints , и возвращает указатель на начало списка в res . Элементы в связном списке связаны через поле ai_next .
Существует несколько причин того, почему связный список может содержать более одной структуры addrinfo : сетевой узел имеет несколько адресов, доступен по нескольким протоколам (например, AF_INET и AF_INET6 ); служба доступна через несколько типов сокетов (например, один её адрес — SOCK_STREAM , а второй — SOCK_DGRAM ).
На небе только и разговоров, что об облаках... И о дебаге...
Ранее мы уже видели, что в профиле AppArmor определены не только правила доступа к файлом и файловой системе, но и правила доступа к сети и даже пустой блок capabilities.
Переходим к настройкам сетевых разрешений в профиле и видим два отдельных блока настроек:
В рамках каждого профиля AppArmor имеет функциональную возможность разделения общих правил для всех процессов, на которые будет «натравлен» профиль, и индивидуальных правил для конкретного процесса в системе или в контейнере.
Таким образом, первый блок есть блок общих настроек, а второй — частные разрешения для httpd. И что же мы видим?
Доступ к сети разрешен, пусть и только ipv4. Веб-серверу выданы права и на TCP (inet stream), и на UDP-сокеты (inet dgram).
Идеальный образ, минимальные права, на Ubuntu в Dev-контуре сервер запускается.
И снова этот злосчастный образ на alpine!
Сетевой стек Alpine устроен таким образом, что при инициализации сервера во время запросов на определение доступных DNS-записей одновременно в обязательном порядке выполняются и ipv4-, и ipv6-запросы.
Возникновение ошибки запроса, как, например, и происходит при блокировке AppArmor, приводит к ошибке инициализации.
Вносим еще одно исправление в профиль:
1. Разрешаем использование сети ipv6 в блоке общих настроек сети:
2. Разрешаем использование сети ipv6 по протоколу UDP в блоке частных настроек сети для httpd:
Сохраняем изменения, применяем профиль и перезапускаем под. Появился заветный статус Running:
Переходим в браузер и проверяем веб-сервер:
Но ведь все работает? И да, и нет.
Под запущен и Running, в логах благодатная строка [pid 1:tid 1] AH00094: Command line: 'httpd -D FOREGROUND' , сервер доступен и отзывается.
А вот страницы нет. Подозрительных логов в контейнере — тоже. Наводок и идей ноль, без палочки!
Вспоминаем задание:
«Судя по всему, внутри контейнера заложена некая вредоносная активность, подменяющая содержимое страницы по умолчанию. Определите, что за активность и какое действие она выполняет, и добавьте в профиль AppArmor блокирующее правило».
Используя все накопленные нами ранее знания, полагаем следующее: entrypoint-скрипт в контейнере модифицирован. А доступа внутрь контейнера нет.
AppArmor'ом единым!
Изучаем выданные права на доступ к файловой системе для httpd и httpd-foreground:
На первый взгляд, ничего противоестественного. Правда, мы уже совсем не первым взглядом всматриваемся в бездну безопасность, поэтому замечаем один неприметный момент: /usr/local/apache2/htdocs/** rw,
И еще одна минутка полезной информации
/usr/local/apache2/htdocs/ — директория для хранения веб-страниц httpd по умолчанию.
Вносим финальное исправление в профиль:
Запрещаем запись в директории /usr/local/apache2/htdocs:
Что это было?
Задание 2. В начале было слово F***
Что случилось?
Что делать?
Заходим на машину:
Нам дано:
файл с возможными флагами (flags.dat)
файл с возможными флагами (flags.dat)
папка с базовыми образами (images)
папка с базовыми образами (images)
контейнер для исследования (cybercamp:latest)
контейнер для исследования (cybercamp:latest)
В задании сказано провести SCA-анализ, выдан инструмент trivy. Воспользуемся этими данными и соберем SCA-отчет:
Посмотрим, сколько всего найдено уязвимостей:
Довольно много. И правильными будут только 8 из них.
Посмотрим, что лежит в папке images:
Там лежат базовые образа. В задании была подсказка: на маленьком клочке бумаги было выведено слово «червь». Очевидный кандидат на базовый образ: node:20.18.1-bookworm.tar . Сгенерируем отчет триви на него:
Уязвимостей найдено тоже довольно много.
В задании сказано, что надо найти те уязвимости, которые были установлены поверх базового образа. Значит, надо вычесть из скана по контейнеру cybercamp:latest скан по базовому образу. Можно сделать это разными способами. Я воспользуюсь утилитой jq (сохраню команду в файлик jq_ diff.sh , чтобы в случае чего проще менять):
Запускаем и смотрим результат:
Осталось 8 CVE, что магическим образом совпадает с количеством флагов. Теперь для каждой CVE надо вытащить флаг из файла flags.dat.
Итого мы получили наши флаги:
Задание 3. Block the Perfect Heist
Что случилось?
Что делать?
Вам в качестве привлеченного эксперта необходимо буквально предотвратить идеальное ограбление, проанализировать приложение банка и выстроить security pipeline, покрыв максимальное количество дефектов.
Воспользуйтесь инструментами, которые DevOps-инженер уже подготовил, но будьте внимательны: он спешил и мог ошибиться в настройках. Разверните приложение локально и настройте инструменты на эффективный поиск дефектов. Нахождение некоторых из них потребует ручного анализа исходного кода.
Так как задание необходимо полностью развернуть у себя, подготовим чистую виртуальную машину под рекомендуемые требования:
Можно приступать!
Распаковав архив, изучим README.md c инструкциями к сборке уязвимого приложения и инструментов.
Выделяем, с чем будем работать:
Уязвимое приложение: bank/app
Уязвимое приложение: bank/app
SAST/DAST/SCA-инструменты: tools/security
SAST/DAST/SCA-инструменты: tools/security
Vulnerability Management-решение : tools/secobserve
Vulnerability Management-решение : tools/secobserve
Начнем с приложения, к инструментам вернемся позже. Исходя из описания и структуры, работать мы будем в bank/app:
docker-compose.yaml — compose для сборки и запуска приложения
docker-compose.yaml — compose для сборки и запуска приложения
backend/... — API и бизнес-логика
backend/... — API и бизнес-логика
frontend/... — UI, в том числе клиентский рендер транзакций
frontend/... — UI, в том числе клиентский рендер транзакций
Ожидаемый формат ответа:
<строка ошибки для сервиса backend>-<строка ошибки для сервиса frontend>-<комментарий_из_dockercompose_над_последним_исправленным_участком_мисконфига> Пример флага: 3-5-comment_over_the_error
Посмотрим docker-compose.yaml, наша цель — найти misconfig, из-за которого приложение не поднимается. Затем уже определить строки и комментарий для флага.
Ошибка достаточно простая, внимательный читатель сразу её заметит, но попробуем запустить приложение, чтобы явно её определить.
Nginx сервиса frontend проксирует в http://backend:8000 и не может разрешить DNS-имя backend
Сервис backend не резолвится, так как они с frontend разведены по разным сетям в docker-compose.yaml. Чтобы исправить это, определим одну сеть для сервисов в блоке networks, например, на
Исправленный docker-compose.yaml (справа)
И перезапустим сборку: в логах больше нет ошибок, приложение стало доступно по адресу https://money-bank.local
Теперь определим флаг исходя из формата <строка ошибки для сервиса backend>-<строка ошибки для сервиса frontend>-<комментарий_из_dockercompose_над_последним_исправленным_участком_мисконфига>
Строки 24 и 39 соответствуют неверному определению блока networks для каждого сервиса, комментарий берем из строки 43. Получаем первый флаг 24-39-networks_backend_and_frontend
Другие вариации ответа, которые в итоге засчитывались "24-39-networks b󠇀󠆊󠇀󠆠󠇀󠆪󠇀󠆮󠇀󠆣󠇀󠆮󠄐󠇁󠅶󠇀󠆢󠇀󠆥󠇁󠅲󠇀󠆠󠄐󠇀󠆠󠇀󠆢󠇁󠅲󠇀󠆮󠇀󠆬󠇀󠆮󠇀󠆡󠇀󠆨󠇀󠆫󠇁󠅼󠄐󠇀󠆭󠇀󠆠󠄐󠇀󠆪󠇀󠆠󠇁󠅰󠇁󠅲󠇀󠆨󠇀󠆭󠇀󠆪󠇀󠆥󠄯ackend and frontend󠇀󠆓󠄐󠇁󠅲󠇀󠆥󠇀󠆡󠇁󠅿󠄐󠇀󠆪󠇀󠆮󠇀󠆣󠇀󠆤󠇀󠆠󠄝󠇀󠆭󠇀󠆨󠇀󠆡󠇁󠅳󠇀󠆤󠇁󠅼󠄐󠇀󠆡󠇁󠅻󠇀󠆫󠇀󠆨󠄐󠇀󠆯󠇁󠅰󠇀󠆮󠇀󠆧󠇀󠆢󠇀󠆨󠇁󠅹󠇀󠆠󠄯" с пробелами и "22-35-networks_backend_and_frontend" для docker-compose.yaml из архива задания с собранными контейнерами
Нам доступна страница входа, но УЗ нет, начнем с регистрации (SQL injection здесь не прошли :) )
После входа доступны страницы: Главная, Переводы, История, Уведомления, Профиль
О жидаемый формат ответа:
<Header>-<файл источник>.<расширение файла> Пример флага: Sec-Fetch-User-main.py
В вопросе достаточно сказано о нашей цели — найти заголовок запроса для bypass-аутентификации и файл-источник
Пройдемся по файлам bank/app/backend . Структура роутов API описана в одноименной routers/
Среди файлов есть дубли (относящиеся к старому API), кажется, разработчики переезжают на новую логику. Основные файлы, которые будем смотреть:
transfers.py — маршруты по счетам пользователей, истории транзакций, переводам
transfers.py — маршруты по счетам пользователей, истории транзакций, переводам
users.py, accounts.py, transactions.py, notifications.py, profiles.py — информация о профилях пользователей, транзакциях, уведомлениях и т.д.
users.py, accounts.py, transactions.py, notifications.py, profiles.py — информация о профилях пользователей, транзакциях, уведомлениях и т.д.
Начнем с transfers.py .
Упс, кажется, это забыли удалить
В helper get user_id сначала читается debug-заголовок — если он есть, то JWT уже не нужен.
Мало того, после перебора id пользователей в системе нам будут доступны счета без какой-либо аутентификации. Это Insecure Direct Object Reference (IDOR) — уязвимость, позволяющая получать несанкционированный доступ к данным или изменять их путём манипулирования идентификаторами объектов без должной проверки прав доступа.
Формируем флаг согласно о жидаемому формату ответа <Header>-<файл источник>.<расширение файла>
Получаем X-Debug-User-transfers.py
Вариации ответа , которые в итоге засчитывались — "X-Debug-User-transfers.py" и "X-Debug-User-routers/transfers.py"
О жидаемый формат ответа:
<endpoint (после /api/v1/)>-<файл источник>.<расширение файла> Пример флага: login/{user_id}-main.py
Ранее мы обнаружили IDOR при использовании небезопасного заголовка X-Debug-User на API эндпойнте accounts
На платформе CyberCamp нам также была доступна бесплатная подсказка:
Внимательнее смотрите в логику сервиса, в исходники, endpoint связан с юзерами.
Поэтому продолжим изучать этот endpoint в том же файле transfers.py
Обратим вниманием на условие на скриншоте. Проблема кроется в недостаточной проверке принадлежности текущему пользователю: нет проверки Accounts.user_id == user_id
Мы можем легко это проверить следующими запросами:
Первые два запроса идут на проверку доступа к ручкам accounts и accounts/v2 , но безуспешно
Первые два запроса идут на проверку доступа к ручкам accounts и accounts/v2 , но безуспешно
Третьим запросом получаем access_token , (УЗ admin:adminadmin мы зарегистрировали через веб-интерфейс при первом входе в приложение)
Третьим запросом получаем access_token , (УЗ admin:adminadmin мы зарегистрировали через веб-интерфейс при первом входе в приложение)
Четвертым и пятым запросом, имея токен только своей УЗ, мы можем подбирать account_id и тем самым читать чужие счета
Четвертым и пятым запросом, имея токен только своей УЗ, мы можем подбирать account_id и тем самым читать чужие счета
Формируем флаг согласно о жидаемому формату ответа <endpoint (после /api/v1/)>-<файл источник>.<расширение файла> Получаем accounts/{account_id}-transfers.py
Вариации ответа , которые в итоге засчитывались — "accounts/{account_id}-transfers.py" и "accounts/{account_id}-routers/transfers.py"
Среди «забытых» API также особый интерес представляет файл accounts.py с эндпойнтом entities/accounts/all
Нам даже авторизация не понадобилась для чтения счетов всех пользователей.
О жидаемый формат ответа:
<имя класса, где допускается вывод хеш пароля>-<файл источник>.<расширение файла> Пример флага: UsersUpdateData-main.py
Как и с файлом accounts.py , в users.py есть API-ручки, не требующие авторизации
Проверим запросом
Какие тут риски, помимо утекших id, username, email пользователей в запросе без аутентификации?
Утечка хэшей. Что позволяет провести оффлайн-подбор, это критично даже при использовании bcrypt.
Формируем флаг согласно о жидаемому формату ответа <имя класса, где допускается вывод хеш-пароля>-<файл источник>.<расширение файла>
Ответ: UsersResponse-users.py
Security pipeline, что нам доступен, находится в папке tools/security
tools/security/docker-compose.yaml — описывает запуск следующих инструментов SAST: OpenGrep, Bandit и CodeQL SCA: Trivy и OSV (оркестрируется в SecObserve) DAST: OWASP ZAP и Nuclei
tools/security/docker-compose.yaml — описывает запуск следующих инструментов
SAST: OpenGrep, Bandit и CodeQL
SAST: OpenGrep, Bandit и CodeQL
SCA: Trivy и OSV (оркестрируется в SecObserve)
SCA: Trivy и OSV (оркестрируется в SecObserve)
DAST: OWASP ZAP и Nuclei
DAST: OWASP ZAP и Nuclei
tools/security/<инструмент>/ — Dockerfile к инструментам, наборы правил и конфигурации
tools/security/<инструмент>/ — Dockerfile к инструментам, наборы правил и конфигурации
tools/security/upload_reports.sh — скрипт загрузки отчетов инструментов в SecObserve
tools/security/upload_reports.sh — скрипт загрузки отчетов инструментов в SecObserve
tools/security/nuclei/scan.sh, tools/security/zap/ — DAST-сценарии
tools/security/nuclei/scan.sh, tools/security/zap/ — DAST-сценарии
Начнем с того, что просто запустим все инструменты сканирования. Обратимся к README.md
Для удобного взаимодействия с docker-контейнерами будем использовать утилиту d4s (у неё схожий интерфейс со всем известной k8s).
Так, для быстрого просмотра лога каждой из джоб (контейнер с запуском инструмента) можно нажать L на выбранном контейнере.
При запуске пайплайна (docker compose) создалось 12 отчетов в папке security_reports
Также в README.md упомянут инструмент SecObserve для загрузки отчетов, запустим и его.
SecObserve успешно собрался.
Подгрузим отчеты в SecObserve по инструкции из README.md.
Авторизуемся в веб-интерфейсе, чтобы забрать API-токен для скрипта загрузки отчетов.
Запустим скрипт с токеном.
При первом запуске создает в SecObserve проект под приложение Bank и сервис app (правильнее было бы разделять app на 3 контейнера: backend, frontend, traefik и еще 1: postgres, если не используется внешняя БД).
Также видим, что все 12 отчетов были загружены, и мы можем ознакомиться с дефектами через удобный веб-интерфейс
Но некоторые отчеты пустые, как, например, для инструментов SAST: Opengrep и CodeQL. Если CodeQL нашел дефекты как минимум для python-кода, то с Opengrep надо разобраться, отсутствие дефектов вызывает подозрения в ошибках конфигурации инструмента.
Посмотрим лог контейнера Opengrep.
В сканировании применено всего одно правило, если проверим команду запуска, то убедимся в этом окончательно.
Перейдем к флагу, упоминающему Opengrep, и настроим инструмент на нужный набор правил
О жидаемый формат ответа:
<финальное кол-во примененных правил (исключая пропущенные)>-<название папки c правилами>-<найденное кол-во дефектов> Пример флага: 10-rules_pack_opengrep_example-47
Итого: нам необходимо найти как можно больше дефектов и указать количество примененных правил.
Если посмотрим на наборы в первом приближении, на их структуру, сразу отпадает набор правил custom_rules — всего два правила.
Чтобы сравнить два оставшихся набора правил, проще будет запустить сканирование.
Результат для набора правил rules — 34 правила, 28 примененных, но 3 дефекта.
Результат для набора правил standart_rules_pack — 608 примененных правил и 382 дефекта... будет что посмотреть.
Запуск Opengrep в docker-compose.yaml стал выглядеть так:
Загрузим финальный отчет в SecObserve.
Убеждаемся, что дефекты подгрузились и корректно отображаются.
Формируем флаг согласно о жидаемому формату ответа <финальное кол-во примененных правил (исключая пропущенные)>-<название папки c правилами>-<найденное кол-во дефектов>
Ответ: 608-standart_rules_pack-382
О жидаемый формат ответа:
<кол-во уникальных дефектов (не типов дефектов, не правил!), что не попали в анализ>-<номера CWE дефектов, что не попали в анализ (по возрастанию)> Пример флага: 10-230-259-610
Итого, нам нужно определить количество исключённых каким-то образом дефектов и их CWE. Как заковыристо...
В первую очередь проверим лог контейнера с инструментом.
Обращаем внимание на строки .bandit file: ./.bandit — это файл конфигурации bandit .
Чтобы изменить файл конфигурации, его необходимо найти ;) В docker-compose.yaml нет volumes с конфигурацией.
В Dockerfile тоже.
Вероятнее всего, файл конфигурации лежит в папке исходников уязвимого приложения. Находим конфигурационный файл .bandit
Видим внутри инструкцию — игнорировать дефекты из правил B104 , B105 , B608 .
Можем в документации подсмотреть, за что отвечают эти правила.
Удалим файл и перезапустим сканирование. Загрузим новый отчет в SecObserve.
Добавились 7 дефектов, 5 обновились (были загружены отчетом ранее и не исправлены). Это нам понадобится для флага. То же самое видим в веб-интерфейсе
Посмотреть CWE новых дефектов можно через веб-интерфейс
Чтобы определить только вновь загруженные дефекты, перейдем в один из них и посмотрим историю загрузки (время Created и Last change совпадает = дефект новый).
Быстро переключаться между дефектами можно по кнопкам «Вправо–влево» сверху (на скриншоте выделено синим).
Также CWE можно соотнести, открыв «сырой» sarif-отчет инструментом jq или почитав описание правил на GitHub :)
Итого — заново включенные правила:
B104 (bind all interfaces → CWE-605)
B104 (bind all interfaces → CWE-605)
B105 (hardcoded password string → CWE-259)
B105 (hardcoded password string → CWE-259)
B608 (SQL injection pattern → CWE-89)
B608 (SQL injection pattern → CWE-89)
Формируем флаг согласно ожидаемому формату ответа <кол-во уникальных дефектов (не типов дефектов, не правил!), что не попали в анализ>-<номера CWE дефектов, что не попали в анализ (по возрастанию)>
Ответ: 7-89-259-605
О жидаемый формат ответа:
<название функции endpoint`а в исходном файле>-<файл источник>.<расширение файла>-<номер строки, с которой начинается уязвимость согласно отчету opengrep> Пример флага: login_me-main.py-32
Для взятия этого флага необходимо убедиться, что корректно запускаются все SAST-инструменты, определить дефект, исходный файл и найти его в отчете opengrep.
Раз дефект найден тремя SAST, то он обладает наибольшим количеством дублей в SecObserve, используем фильтр Duplicates=Yes .
Дефект SQLi найден двумя инструментами Bandit, CodeQL в файле transfers.py , теперь попробуем поискать среди других инструментов в описании.
Попробуем найти еще сработки данного типа в этом файле (фильтром Source=transfers.py ) и искать по описанию (фильтр Title=sql ).
Теперь все SAST: Bandit, CodeQL, Opengrep указывают на transfers.py , если открыть дефекты, можно проверить, что это одна и та же SQLi.
Почему SecObserve сразу не отобразил дефект от Opengrep?
Дело в том, что Bandit и CodeQL указывают на строку 277, а Opengrep — на 276.
В коммерческих ASOC/ASPM решениях такое недопустимо, ну а мы работаем с open-source-платформой, поэтому все ей прощаем (или заводим Pull Request).
Для формирования флага у нас уже есть строка 276 из отчета Opengrep, но как автоматизировать поиск и подтверждение эксплуатации данной SQL-инъекции?
Можно попробовать инструментами ffuf и sqlmap .
Из любви к искусству, несмотря на то, что у нас есть исходники, пройдемся по словарю возможных API-эндпойнтов:
Swagger! Без аутентификации! В подарок спецификация openapi.
Вытащим URL-эндпойнты (только для GET) из схемы openapi.json с валидными тестовыми данными в отдельный файл, учитывая тип параметра и значение по умолчанию.
> разве что query-параметры типа array не будем использовать
Теперь пройдемся mass-сканированием с sqlmap .
Для подтверждения запустим прицельно на уязвимый эндпойнт.
Вывод sqlmap подтверждает, что атакующий может изменить условие выборки, получить чужие транзакции, влиять на SQL-логику.
Флаг dump выгрузит все таблички, их можно увидеть на скрине.
Почему это работает? SQL-запрос строится через f-string и text(...) c прямой подстановкой q и limit , то есть без параметризации
Теперь, когда все нашли и подтвердили, формируем флаг согласно ожидаемому формату ответа <название функции endpoint`а в исходном файле>-<файл источник>.<расширение файла>-<номер строки, с которой начинается уязвимость согласно отчету opengrep>
Получаем search_transactions_unsafe-transfers.py-276
Вариации ответа, которые в итоге засчитывались — "search_transactions_unsafe-transfers.py-276" и "search_transactions_unsafe-routers/transfers.py-276"
О жидаемый формат ответа
<значение CVSS_V3>-<artifactId>@<version> Пример флага: 10-log4j-core@2.3.1
Искать дефект с максимальным CVSS V3 будем в отчетах Trivy по образам frontend и backend, SBOM мы пока не обогащали информацией о уязвимостях, поэтому там не будет CVSSv3. Формат отчетов Trivy — SARIF, так как только его принимает SecObserve.
Смотреть сразу все дефекты Trivy можно, отфильтровав по Scanner=Trivy и критичности Critical , High .
Но, к сожалению, SecObserve не позволяет отсортировать по CVSSv3, можно либо провалиться в дефект и листать в поиске самого критичного, либо раскрывать по одному дефекту на главной странице, что одинаково медленно по скорости :)
Седьмым в списке находим jaraco.context
Данной информации нам достаточно, чтобы сформировать флаг.
Альтернативным способом можно изучить отчеты Trivy в консоли, используя jq , grep и sort .
Находим среди всех отчетов самый критичный CVSSv3.
Выведем с критичностью 8.6 по CVSSv3.
Данной информации нам достаточно, чтобы сформировать флаг согласно о жидаемому формату ответа <значение CVSS_V3>-<artifactId>@<version>
Вариации ответа , которые в итоге засчитывались — " 8.6-jaraco.context@5.3.0 ", " 8.6-jaraco.context@6.1.0 " и 9.8-zlib@1.3.1-r2 , если использовать предсобранный контейнер trivy с БД дефектов от 26 марта.
Почему так? Отвечаем в третьем пункте выводов.
SCA-инструменты сильно зависят от качества и актуальности vulnerability database. Результаты Trivy и OSV отличались в зависимости от версии базы и времени обновления: дефект может как появиться при повторном сканировании, так и исчезнуть — если неэксплуатабелен или переведен в статус «инфо». Для эффективной работы нужны инструменты с валидированной базой и данными об эксплойтах, ручная верификация критичных находок, проверка достижимости в коде. Это необходимо учитывать при приоритизации CVE и построении процессов управления уязвимостями зависимостей.
О жидаемый формат ответа:
<CVE>-<Component name из отчета>@<Component version из отчета> Пример флага: CVE-2026-10000-main@1.0~deb
Один из флагов возможно сдать, используя в анализе инструмент OSV, что использует загруженный в проект SBOM. OSV встроен и запускается через веб-интерфейс SecObserve как по расписанию, так и вручную. Обратимся к документации, чтобы запустить сканирование https://secobserve.github.io/SecObserve/integrations/osv_scan/ .
После сканирования доступны новые дефекты.
Выберем Critical.
Результаты сканера OSV нестабильны и зависят от БД, поэтому в рамках задания флаг засчитывался всем . Расхождения в результатах SCA-сканеров — действительно одна из главных проблем, с которыми сталкиваются специалисты по безопасности приложений. Разные сканеры, анализируя одно и то же приложение, очень часто выдают разные списки дефектов в зависимости от обновлений в своих БД. Например, сканер может показывать критические CVE там, где разработчики уже признали проблему неопасной, и в новом обновлении БД-сканера этой уязвимости может быть назначен низкий уровень критичности.
О жидаемый формат ответа:
<свойство в JavaScript, из-за которого уязвимость эксплуатируема>-<текст, выводимый в консоль лог> Пример флага: innerHTML-XSS_TRAINING_TRIGGERED
Дефектов DAST у нас не так уж и много.
Изучив отчеты ZAP и Nuclei, понимаем, что только последний указывает на Stored и Reflected XSS.
Рассмотрим подробнее Stored XSS.
Кастомные правила для nuclei ищут среди api/v1/transactions маркер XSS_NUCLEI_STORED_TEST .
Скрипт scan.sh автоматизирует создание пользователей и последующее внедрение инъекции в переводы, там мы видим тот же маркер.
И на главной странице приложения тоже будет он.
Выполнение произвольного JavaScript позволяет совершить действия от лица любого пользователя, который откроет заражённую страницу. Самое опасное — эксфильтрация данных других пользователей, включая сессионные cookies, токены авторизации, содержимое sessionStorage / localStorage , а также доступ к страницам, которые видит жертва.
Попробуем проэксплуатировать то же самое вручную.
Проверим историю транзакций.
При этом на главной странице в истории подобного поведения нет.
Также в флаге от нас ожидают указание «свойство в JavaScript, из-за которого уязвимость эксплуатируема», для этого обратимся к файлам исходников.
Известные свойства, приводящие к XSS, легко гуглятся, даже в примере оставлена подсказка.
Данная конструкция в History.tsx как раз и допускает XSS.
Формируем флаг согласно о жидаемому формату ответа <свойство в JavaScript, из-за которого уязвимость эксплуатируема>-<текст, выводимый в консоль лог>
Ответ: dangerouslySetInnerHTML-XSS_NUCLEI_STORED_TEST
Ручной анализ остаётся обязательным даже при наличии в пайплайне SAST/DAST/SCA-инструментов. Наиболее критичные дефекты в задании были связаны с логикой приложения: bypass аутентификации через X-Debug-User, IDOR и API без проверки авторизации. Подобные проблемы часто не определяются сканерами.
Ручной анализ остаётся обязательным даже при наличии в пайплайне SAST/DAST/SCA-инструментов. Наиболее критичные дефекты в задании были связаны с логикой приложения: bypass аутентификации через X-Debug-User, IDOR и API без проверки авторизации. Подобные проблемы часто не определяются сканерами.
Кастомизация правил SAST и DAST — обязательная практика, те, что «из коробки», дают много false-positive или вовсе ничего не находят. Стандартные правила ZAP и шаблоны Nuclei не покрывают blind boolean-инъекции в SQLite, поэтому для подтверждения эксплуатабельности дефектов приходится переходить в ручной режим, как в примере с sqlmap.
Кастомизация правил SAST и DAST — обязательная практика, те, что «из коробки», дают много false-positive или вовсе ничего не находят. Стандартные правила ZAP и шаблоны Nuclei не покрывают blind boolean-инъекции в SQLite, поэтому для подтверждения эксплуатабельности дефектов приходится переходить в ручной режим, как в примере с sqlmap.
SCA-инструменты сильно зависят от качества и актуальности vulnerability database. Результаты Trivy и OSV отличались в зависимости от версии базы и времени обновления: дефект может как появиться при повторном сканировании, так и исчезнуть — если неэксплуатабелен или переведен в статус «инфо». Для эффективной работы нужны инструменты с валидированной базой и данными об эксплойтах, ручная верификация критичных находок, проверка достижимости в коде. Это необходимо учитывать при приоритизации CVE и построении процессов управления уязвимостями зависимостей.
SCA-инструменты сильно зависят от качества и актуальности vulnerability database. Результаты Trivy и OSV отличались в зависимости от версии базы и времени обновления: дефект может как появиться при повторном сканировании, так и исчезнуть — если неэксплуатабелен или переведен в статус «инфо». Для эффективной работы нужны инструменты с валидированной базой и данными об эксплойтах, ручная верификация критичных находок, проверка достижимости в коде. Это необходимо учитывать при приоритизации CVE и построении процессов управления уязвимостями зависимостей.
Наибольшую ценность даёт централизованный security pipeline с агрегацией результатов от разных инструментов, нежели отдельный инструмент. SecObserve позволил сопоставить результаты разных SAST и быстрее определить действительно критичные дефекты среди большого количества сработок.
Наибольшую ценность даёт централизованный security pipeline с агрегацией результатов от разных инструментов, нежели отдельный инструмент. SecObserve позволил сопоставить результаты разных SAST и быстрее определить действительно критичные дефекты среди большого количества сработок.
Даже учебное приложение показало типичные проблемы реальных проектов: debug-функциональность, устаревшие API или вовсе без авторизации, небезопасная работа с SQL, ошибки контроля доступа и небезопасный frontend-рендеринг. Большинство таких дефектов возникают из-за недостатка secure-by-design практик и зрелых процессов DevSecOps (смотрите наш открытый фреймворк DAF ).
Даже учебное приложение показало типичные проблемы реальных проектов: debug-функциональность, устаревшие API или вовсе без авторизации, небезопасная работа с SQL, ошибки контроля доступа и небезопасный frontend-рендеринг. Большинство таких дефектов возникают из-за недостатка secure-by-design практик и зрелых процессов DevSecOps (смотрите наш открытый фреймворк DAF ).