DevSecOps на практике: райтапы заданий с митапа CyberCamp глазами тех, кто их придумал

от автора

В марте состоялся второй эпизод 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, а также перезапускать поды. Прямого доступа к контейнеру нет.

Вперед, к победе!

Флаг № 1: 

Проваливаемся в пользователя. Пытаемся сориентироваться. Время не ждёт!

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 указана следующая команда запуска:

CMD ["httpd-foreground"]

Мы понимаем, что binary для веб-сервера вообще-то называется просто httpd. 

Начинаем копать — находим исходник httpd-foreground и узнаём, что это sh-скрипт следующего содержания:

#!/bin/shset -e# Apache gets grumpy about PID files pre-existingrm -f /usr/local/apache2/logs/httpd.pidexec httpd -DFOREGROUND "$@"

Вот откуда ноги растут! Тот самый rm, сгубивший несчастный под, выполняется аккурат перед запуском самого httpd.

Разобраться подробнее с содержимым образа httpd:2.4.66-alpine3.23 и предварительной настройкой httpd при запуске вам помогут вот эти сорцы:

https://github.com/docker-library/httpd

https://hub.docker.com/layers/library/httpd/alpine3.23/images/sha256-57330b1c5247851264cf4bcd20f9d1857e86c3fbf7f838491d68117c05f8dd13

Заглядываем в профиль /etc/apparmor.d/task-b-66:

Получается, что путь /usr/bin/** разрешен, /usr/bin/rm — тоже разрешен, но под не запускается? Произвол!

Вспоминаем, что имеем дело с образом httpd:2.4.66-alpine3.23. Ответ кроется в великом и ужасном имени базового дистрибутива alpine:

Ох уж эти минималистичные образы, часть #1

Так как образ собран на alpine, внутри контейнера httpd:2.4.66-alpine3.23 попросту отсутствует бинарный файл rm.

Вместо бинарного файла rm в файловой системе красуется ссылка на совершенно другой путь к совершенно иному файлу: /bin/busybox

Вносим исправление в профиль:

1. Заменяем разрешенный путь к бинарным файлам:

#Было deny /bin/** x,#deny /usr/bin/** x,#Стало#deny /bin/** x,deny /usr/bin/** x,

2. Разрешаем бинарный файл busybox для alpine:

#Было/usr/bin/rm ix,#Стало#Указано следующее: "Укажите корректное разрешающее правило AppArmor для нужного бинарного файла".#Вот и наш первый флаг/bin/busybox ix, - флаг!#Примечание#Ввиду особенностей работы AppArmor засчитывался только флаг с параметром ix, т.к. px - избыточен

Сохраняем изменения, применяем профиль и перезапускаем под.

Победа?

Флаг № 2:

По возвращении в k9s нас вновь встречает треклятый CrashLoopBackOff.

Проваливаемся в log и снова Permission denied! Неужто не сработало? Может, ошибка? Ан нет, не ошибка, или, правильнее сказать, не наша: 

C:\fef3082e8a7b1a2849b1fcfd900ed71b4ad4a5414931051c7c70092ace3fb97d

Сервер не может инициализироваться: 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.

Переходим к настройкам сетевых разрешений в профиле и видим два отдельных блока настроек:

C:\30957105bd476a808409882f4ac3e826db9e6085033e4ef7e52194608bb86417

и

C:\d81517a8e4ea9a38654d5f6d5fe2e5951a6464c92aa369589362aa77adbe7920

В рамках каждого профиля AppArmor имеет функциональную возможность разделения общих правил для всех процессов, на которые будет «натравлен» профиль, и индивидуальных правил для конкретного процесса в системе или в контейнере.

Таким образом, первый блок есть блок общих настроек, а второй — частные разрешения для httpd. И что же мы видим?

Доступ к сети разрешен, пусть и только ipv4. Веб-серверу выданы права и на TCP (inet stream), и на UDP-сокеты (inet dgram).

Идеальный образ, минимальные права, на Ubuntu в Dev-контуре сервер запускается.

Ох уж эти минималистичные образы, часть #2 

И снова этот злосчастный образ на alpine!

Сетевой стек Alpine устроен таким образом, что при инициализации сервера во время запросов на определение доступных DNS-записей одновременно в обязательном порядке выполняются и ipv4-, и ipv6-запросы.

Возникновение ошибки запроса, как, например, и происходит при блокировке AppArmor, приводит к ошибке инициализации.

Вносим еще одно исправление в профиль:

1. Разрешаем использование сети ipv6 в блоке общих настроек сети:

#Было #deny network inet,deny network inet6,#Стало#deny network inet,#deny network inet6,

2. Разрешаем использование сети ipv6 по протоколу UDP в блоке частных настроек сети для httpd:

#Былоnetwork inet stream,network inet dgram,deny network inet6 stream,deny network inet6 dgram,#Стало#Указано следующее: "Укажите корректные настройки сети - разрешенное сетевое взаимодействие".#Второй флаг у нас в карманеnetwork inet stream,network inet dgram,deny network inet6 stream,network inet6 dgram, - флаг!#Примечание#Ввиду особенностей работы httpd в контейнере на alpine разрешение inet6 stream является избыточным

Сохраняем изменения, применяем профиль и перезапускаем под. Появился заветный статус Running:

C:\a5d1abdd7759f67a52fe90247a072d386def48e6df146bc820399316a6b8f4dc

Флаг № 3:

Переходим в браузер и проверяем веб-сервер:

C:\cc6d03b58e6be8e8536294c98ba0651e2c02135722df37d990cee5fab64505d7

Но ведь все работает? И да, и нет.

Под запущен и Running, в логах благодатная строка [pid 1:tid 1] AH00094: Command line: 'httpd -D FOREGROUND', сервер доступен и отзывается.

А вот страницы нет. Подозрительных логов в контейнере — тоже. Наводок и идей ноль, без палочки!

Вспоминаем задание:

«Судя по всему, внутри контейнера заложена некая вредоносная активность, подменяющая содержимое страницы по умолчанию. Определите, что за активность и какое действие она выполняет, и добавьте в профиль AppArmor блокирующее правило».

Используя все накопленные нами ранее знания, полагаем следующее: entrypoint-скрипт в контейнере модифицирован. А доступа внутрь контейнера нет.

AppArmor’ом единым!

Изучаем выданные права на доступ к файловой системе для httpd и httpd-foreground:

C:\943b36a668d4e91ec2e502b6769d236072109f8a36e7234e6e66a1a54143db53

и

C:\4cc119c1c14032d46aa889c04c373008f11d7e82935241ac1f6ccfd4f67930bc

На первый взгляд, ничего противоестественного. Правда, мы уже совсем не первым взглядом всматриваемся в бездну безопасность, поэтому замечаем один неприметный момент: /usr/local/apache2/htdocs/** rw,

И еще одна минутка полезной информации 

/usr/local/apache2/htdocs/ — директория для хранения веб-страниц httpd по умолчанию.

Вносим финальное исправление в профиль:

Запрещаем запись в директории /usr/local/apache2/htdocs:

#Было /usr/local/bin/httpd-foreground r,/usr/local/apache2/htdocs/** rw,/usr/local/apache2/logs/** rw,/usr/local/apache2/conf/** r,#Стало /usr/local/bin/httpd-foreground r,/usr/local/apache2/htdocs/** r, - флаг!/usr/local/apache2/logs/** rw,/usr/local/apache2/conf/** r,#Примечание 1#Блокирующее правило можно было реализовать и другим способом, а именно прописатьdeny /usr/local/apache2/htdocs/** w,#Однако, с логической точки зрения это правило будет являться моветоном, поскольку фактически запрещает то, что было "намеренно" разрешено (в правиле /usr/local/apache2/htdocs/** rw,)#Примечание 2#Так как заведомо неизвестно, какая именно вредоносная активность выполняется в контейнере и что именно происходит внутри директории /usr/local/apache2/htdocs, блокирующее правило deny /usr/local/apache2/htdocs/index.html w,#расценивалось как недостаточное для устранения вредоноса

Что это было?

Задание 2. В начале было слово F***

Что случилось?

C:\066e9a952d09493709efb970f4240ffe62a254e04578603de1fc6d0523b26c8b
C:\f53e4a70c3ba2263e863805b8331ed01e0be171ced0a426e3c94124c25971fc5

Что делать?

C:\b9745e226221ae733c3fd3a16aefe32514d5aa6ba103dd6b5b45991401a7f0fc
C:\666d7f1b346476a665b3066779191ce5764c294a979182acf6801c75989df7c4

Решение:

Заходим на машину:

C:\03c3ecbeafa3ead4372d1138ff53151d0dcc3286048b5d66f2c78ad6f571ec18

Нам дано:

  • файл с возможными флагами (flags.dat)

  • папка с базовыми образами (images)

  • контейнер для исследования (cybercamp:latest)

C:\6bf4b8ccf2bfaae99d406444dabde5024a467cbd4bb40cfaaa39e97216fd732a

В задании сказано провести SCA-анализ, выдан инструмент trivy. Воспользуемся этими данными и соберем SCA-отчет:

trivy image -f json -o scan_trivy.json cybercamp:latest
C:\f4b3f9f4946ac9bd4a702fe98c2d0e0d80a81bf6f3ad4239d43f83f1b957251d

Посмотрим, сколько всего найдено уязвимостей:

jq '.Result[].Vulnerabilities | length'  scan_trivy.json
C:\783131acb1b286031ea2d45d5457448f8016cbf7f191364ddf079c5358bfbf54

Довольно много. И правильными будут только 8 из них.

Посмотрим, что лежит в папке images:

C:\6b8884004bfbf3d5a3d22eced589bc866f4134bfbd370a78b76a76d12f85afb0

Там лежат базовые образа. В задании была подсказка: на маленьком клочке бумаги было выведено слово «червь». Очевидный кандидат на базовый образ: node:20.18.1-bookworm.tar.

Сгенерируем отчет триви на него:

trivy image -f json -o scan_trivy_bookworm.json --input images/node:20.18.1-bookworm.tar

Уязвимостей найдено тоже довольно много.

C:\23504e24453a4ad18f1efdb70474e742ae7d35ba29b592c45db94f6640b2ddda

В задании сказано, что надо найти те уязвимости, которые были установлены поверх базового образа. Значит, надо вычесть из скана по контейнеру cybercamp:latest скан по базовому образу.

Можно сделать это разными способами. Я воспользуюсь утилитой jq (сохраню команду в файлик jq_diff.sh, чтобы в случае чего проще менять):

jq --slurpfile file2 scan_trivy_bookworm.json '  # 1. Собираем массив CVE из второго файла  [ $file2[].Results[].Vulnerabilities[]?.VulnerabilityID ] as $exclude_list |  # 2. Собираем массив CVE из первого файла  [ .Results[].Vulnerabilities[]?.VulnerabilityID ] |  # 3. Убираем дубликаты из первого списка  unique |  # 4. Оставляем только те, которых нет во втором списке  map(select(. as $id | any($exclude_list[]; . == $id) | not))' scan_trivy.json > diff_report.json
C:\acdbc78c38aaa67c349cd77eaa4eab15ed4171138aa6ec0e20fca2f2e2c2d2ff

Запускаем и смотрим результат:

C:\4ab07ffab9aa1d995eff8c776c246759fc6524b8474b4a5f52a4e120e46b3290

Осталось 8 CVE, что магическим образом совпадает с количеством флагов.
Теперь для каждой CVE надо вытащить флаг из файла flags.dat.

grep -F -f <(jq -r '.[]' diff_report.json) flags.dat

Итого мы получили наши флаги:

C:\280b1e704b7aa92caddbc713f468d8dd8c70f214478dbd4af3a8631f413697f3

Задание 3. Block the Perfect Heist

Что случилось?

C:\522afa7e0bce2957190dc176501a66fa56bce095d1ede9e10c0f226848242093

Что делать? 

Вам в качестве привлеченного эксперта необходимо буквально предотвратить идеальное ограбление, проанализировать приложение банка и выстроить security pipeline, покрыв максимальное количество дефектов.

Воспользуйтесь инструментами, которые DevOps-инженер уже подготовил, но будьте внимательны: он спешил и мог ошибиться в настройках. Разверните приложение локально и настройте инструменты на эффективный поиск дефектов. Нахождение некоторых из них потребует ручного анализа исходного кода.

Так как задание необходимо полностью развернуть у себя, подготовим чистую виртуальную машину под рекомендуемые требования: 

C:\5820f5991d1350eaff23f2a1b504913322bd48bd1df1017cc5607401f052ccd9

Можно приступать!

Распаковав архив, изучим README.md c инструкциями к сборке уязвимого приложения и инструментов.

C:\dc74eea2adf2739d9dce7dd33bad13bb84541cf362448d8ad4e0d9026caa9673
C:\0c8c21a8791db4b9620434767fbaf56a48bd48387351170aa926bfd8a0e2fca7

Выделяем, с чем будем работать:

Начнем с приложения, к инструментам вернемся позже. Исходя из описания и структуры, работать мы будем в bank/app:

C:\ac573935a1432db3a9b02f3f065bb3a1df8d1430af16a3c797a0520caafc6b62
  • docker-compose.yaml — compose для сборки и запуска приложения

  • backend/… — API и бизнес-логика

  • frontend/… — UI, в том числе клиентский рендер транзакций

Первый флаг: DevOps спешил и передал приложение с ошибкой в docker compose. Исправьте ошибку и запустите приложение

Ожидаемый формат ответа:

<строка ошибки для сервиса backend>-<строка ошибки для сервиса frontend>-<комментарий_из_dockercompose_над_последним_исправленным_участком_мисконфига>
Пример флага: 3-5-comment_over_the_error

Посмотрим docker-compose.yaml, наша цель — найти misconfig, из-за которого приложение не поднимается. Затем уже определить строки и комментарий для флага.

C:\96b49bf85227ea362500598f351ff29e71904749da066692e6e0e8584fc00aab

Ошибка достаточно простая, внимательный читатель сразу её заметит, но попробуем запустить приложение, чтобы явно её определить.

C:\b0e7562e0e6ca10a13a7531abe07b75604af25dec17425786ff8eff32dfe0f72

C:\b0e7562e0e6ca10a13a7531abe07b75604af25dec17425786ff8eff32dfe0f72

Nginx сервиса frontend проксирует в http://backend:8000 и не может разрешить DNS-имя backend

C:\6047c33d1440cd13a4f473e2b79d0e054932b60cc9e32007e98999323296439b

Сервис backend не резолвится, так как они с frontend разведены по разным сетям в docker-compose.yaml. Чтобы исправить это, определим одну сеть для сервисов в блоке networks, например, на 

networks:  - net

Исправленный docker-compose.yaml (справа)

C:\9214035017250cadb08fe873539a44d5e0548039c1f0bf172feb8790243a0739

И перезапустим сборку: в логах больше нет ошибок, приложение стало доступно по адресу https://money-bank.local

C:\d77814e074238e0fc9e144d6718f53fabfe5c97d156822b873cdac2f32db6386

Теперь определим флаг исходя из формата <строка ошибки для сервиса 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 здесь не прошли 🙂 )

C:\27414c4f875aef31b9513d81d6a4bec9f7716acc50de34bb70e97357b57e9ed9

После входа доступны страницы: Главная, Переводы, История, Уведомления, Профиль

C:\3e651b832f50d491d863bb85a634d4fddfb30e11c53ebdb135f90c70f5a164e3

Второй флаг: Какой заголовок позволяет обходить аутентификацию в запросах?

Ожидаемый формат ответа: 

<Header>-<файл источник>.<расширение файла>
Пример флага: Sec-Fetch-User-main.py

В вопросе достаточно сказано о нашей цели — найти заголовок запроса для bypass-аутентификации и файл-источник

Пройдемся по файлам bank/app/backend. Структура роутов API описана в одноименной routers/

C:\2c87903f0d7e502515031a98c64d0c81d1f5059a7b9230797eff51191aba3c84

Среди файлов есть дубли (относящиеся к старому API), кажется, разработчики переезжают на новую логику. Основные файлы, которые будем смотреть:

  • transfers.py — маршруты по счетам пользователей, истории транзакций, переводам

  • users.py, accounts.py, transactions.py, notifications.py, profiles.py — информация о профилях пользователей, транзакциях, уведомлениях и т.д.

Начнем с transfers.py.

Упс, кажется, это забыли удалить

C:\62faa5dec1e7cbb04247741125e99899c5174cfae27b236589624f28d8cf53b1

В helper getuser_id сначала читается debug-заголовок — если он есть, то JWT уже не нужен.

C:\75aa13430812668ccb35c294541c4358cd3036c1ede93525fabfef27aa055c0c

Мало того, после перебора id пользователей в системе нам будут доступны счета без какой-либо аутентификации. Это Insecure Direct Object Reference (IDOR) — уязвимость, позволяющая получать несанкционированный доступ к данным или изменять их путём манипулирования идентификаторами объектов без должной проверки прав доступа.

C:\199e0b4c3ada3f442cb0f84113fddf19d262269926e6af59a50c200a789a3e4e

Формируем флаг согласно ожидаемому формату ответа <Header>-<файл источник>.<расширение файла> 

Получаем X-Debug-User-transfers.py

Вариации ответа, которые в итоге засчитывались — «X-Debug-User-transfers.py» и «X-Debug-User-routers/transfers.py»

Третий флаг: Какой API endpoint допускает IDOR в чтении счёта?

Ожидаемый формат ответа: 

<endpoint (после /api/v1/)>-<файл источник>.<расширение файла>
Пример флага: login/{user_id}-main.py

Ранее мы обнаружили IDOR при использовании небезопасного заголовка X-Debug-User на API эндпойнте accounts

На платформе CyberCamp нам также была доступна бесплатная подсказка: 

Внимательнее смотрите в логику сервиса, в исходники, endpoint связан с юзерами.

Поэтому продолжим изучать этот endpoint в том же файле transfers.py

Обратим вниманием на условие на скриншоте. Проблема кроется в недостаточной проверке принадлежности текущему пользователю: нет проверки Accounts.user_id == user_id

C:\04f548f1894ca692ac8088d550d58a2d52b3da4e292dea2b961a344758699bdf

Мы можем легко это проверить следующими запросами:

  • Первые два запроса идут на проверку доступа к ручкам accounts и accounts/v2, но безуспешно

  • Третьим запросом получаем access_token, (УЗ admin:adminadmin мы зарегистрировали через веб-интерфейс при первом входе в приложение) 

  • Четвертым и пятым запросом, имея токен только своей УЗ, мы можем подбирать account_id и тем самым читать чужие счета

C:\ada3eec20fa54f364d4ca0364a655302b3abbef95fc967b29756bca4034d58e3

Формируем флаг согласно ожидаемому формату ответа <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

C:\8d6c6e47cbc5f95e65848ab480b72fe6bff276b7635b0718f7da0df3632ce6cf

Нам даже авторизация не понадобилась для чтения счетов всех пользователей.

C:\9f41af68909b0f8f92f4757d60a4863b1308119e2c968556e121b3bff5ba73ea

Четвертый флаг: Укажите имя класса, который позволяет получить всех пользователей с хэшами паролей без авторизации

Ожидаемый формат ответа: 

<имя класса, где допускается вывод хеш пароля>-<файл источник>.<расширение файла>
Пример флага: UsersUpdateData-main.py

Как и с файлом accounts.py, в users.py есть API-ручки, не требующие авторизации

C:\46b8936be9dbe29735a5b7aabb81c8c853013f391919dd699c67654060d4e7bd

Проверим запросом 

C:\863569056e59b00df71a662e7b7b93eb08091c8eea54a1bb12e1c8b48d27f432

Какие тут риски, помимо утекших id, username, email пользователей в запросе без аутентификации?

Утечка хэшей. Что позволяет провести оффлайн-подбор, это критично даже при использовании bcrypt.

C:\f306dc2545e8f905ae31be69e580f1a9f52a38a80fce912af76b8e1e59953ab8

Формируем флаг согласно ожидаемому формату ответа <имя класса, где допускается вывод хеш-пароля>-<файл источник>.<расширение файла> 

Ответ: UsersResponse-users.py

Как находить дефекты при помощи сканеров?

Security pipeline, что нам доступен, находится в папке tools/security

C:\6951ffbcfeacffea9a4969bf4a6ff3bae3454c745a4b1b3232fdcb546e100659
  • tools/security/docker-compose.yaml — описывает запуск следующих инструментов 

    • SAST: OpenGrep, Bandit и CodeQL

    • SCA: Trivy и OSV (оркестрируется в SecObserve)

    • DAST: OWASP ZAP и Nuclei

  • tools/security/<инструмент>/ — Dockerfile к инструментам, наборы правил и конфигурации

  • tools/security/upload_reports.sh — скрипт загрузки отчетов инструментов в SecObserve

  • tools/security/nuclei/scan.sh, tools/security/zap/ — DAST-сценарии

Начнем с того, что просто запустим все инструменты сканирования. Обратимся к README.md

C:\9c11af2470c4ede3c12ea23e7c7beb9fe5cb95b554742b473c66e099aeb81101

Для удобного взаимодействия с docker-контейнерами будем использовать утилиту d4s (у неё схожий интерфейс со всем известной k8s).

Так, для быстрого просмотра лога каждой из джоб (контейнер с запуском инструмента) можно нажать L на выбранном контейнере.

C:\343e334dddde599728ae4074f2031309f6480fd59497c493c2323360bfc5e224
C:\7e8caf23cba54b1bf3dc279ebbe1aec1a4bb28bd84b078c4494e0b9709a69056

При запуске пайплайна (docker compose) создалось 12 отчетов в папке security_reports

C:\ac24ee5aeec90f55c2c1c1fcc6143bfafdf529530ba710bcda5878516fc7dc00

Также в README.md упомянут инструмент SecObserve для загрузки отчетов, запустим и его.

C:\a772429dfcb543c428088075aec5c474a9cfd86d096537eac5d74acabe4dd416

SecObserve успешно собрался.

C:\f39f4c086ad477a3810a8070b5b783ce0e00c1a102c73f1b381cd0b6e8f0de23

Подгрузим отчеты в SecObserve по инструкции из README.md.

C:\71bcd6a9f24f6c1681b0f6f556cb1cdf14bb55dab4363ee236ce35ffda898de5

Авторизуемся в веб-интерфейсе, чтобы забрать API-токен для скрипта загрузки отчетов.

C:\6409ab789e13570e4aeb3e6ec7371ea02b9ae190dfc6a00e947cb03331e5fc08
C:\2c728f18f9f49e022c53e1a7b436ebd824d8a9f652201aeecf560eb24a891193

Запустим скрипт с токеном.

При первом запуске создает в SecObserve проект под приложение Bank и сервис app (правильнее было бы разделять app на 3 контейнера: backend, frontend, traefik и еще 1: postgres, если не используется внешняя БД).

C:\97a02d401fc48aa5929698d6a42081cad0d03df4e32c6a925dc4d571351faeb1

Также видим, что все 12 отчетов были загружены, и мы можем ознакомиться с дефектами через удобный веб-интерфейс

C:\a70d6b14c6fa50c3cb66c9f1b18ca4dfd53919e378e386317ac99f7ede726ae2

Но некоторые отчеты пустые, как, например, для инструментов SAST: Opengrep и CodeQL. Если CodeQL нашел дефекты как минимум для python-кода, то с Opengrep надо разобраться, отсутствие дефектов вызывает подозрения в ошибках конфигурации инструмента.

Посмотрим лог контейнера Opengrep.

C:\55d6570d3743299460d08c46af6a5bcf32fbb0c028bcb81cc849398fd33e6e7a

В сканировании применено всего одно правило, если проверим команду запуска, то убедимся в этом окончательно.

C:\070d13527264f43fd045719ba9431559a5ea3bc998926387e7553340cbfe261a

Перейдем к флагу, упоминающему Opengrep, и настроим инструмент на нужный набор правил

Пятый флаг: Какой набор правил для opengrep охватывает большее количество дефектов?

Ожидаемый формат ответа: 

<финальное кол-во примененных правил (исключая пропущенные)>-<название папки c правилами>-<найденное кол-во дефектов>
Пример флага: 10-rules_pack_opengrep_example-47

Итого: нам необходимо найти как можно больше дефектов и указать количество примененных правил.

Если посмотрим на наборы в первом приближении, на их структуру, сразу отпадает набор правил custom_rules — всего два правила.

C:\38371d2e43f054d7e28aebae79ad06354728d93ffe1e273513eb72da19473a7f

Чтобы сравнить два оставшихся набора правил, проще будет запустить сканирование.

Результат для набора правил rules — 34 правила, 28 примененных, но 3 дефекта.

C:\678adafab86592dd7baaa08ae26683fd38105a24e2dfc01c96875b7cddf2cb55

Результат для набора правил standart_rules_pack — 608 примененных правил и 382 дефекта… будет что посмотреть.

C:\aec94675d1d89e6ccd75e9557829accbe3fdf2126e09e46705347956d31da598

Запуск Opengrep в docker-compose.yaml стал выглядеть так:

opengrep:     build: ./opengrep     volumes:         - *code-volumes         - ./security_reports:/reports         - ./opengrep/standart_rules_pack:/rules:ro     command: scan -f /rules --taint-intrafile --sarif-output=/reports/opengrep-rules.sarif ./ 

Загрузим финальный отчет в SecObserve.

C:\8f2aee42be6be16836d484d2c5ac812f0b073e81ddda57829049af1e63259e09

Убеждаемся, что дефекты подгрузились и корректно отображаются. 

C:\8fdf1a574915f0d1f13ac7cd6b7fd1583ea7a0170e98ff4cf30a994a4ba1f73a

Формируем флаг согласно ожидаемому формату ответа <финальное кол-во примененных правил (исключая пропущенные)>-<название папки c правилами>-<найденное кол-во дефектов>

Ответ: 608-standart_rules_pack-382

Шестой флаг: Проверьте конфигурацию bandit, возможно, мы что-то упускаем из анализа

Ожидаемый формат ответа: 

<кол-во уникальных дефектов (не типов дефектов, не правил!), что не попали в анализ>-<номера CWE дефектов, что не попали в анализ (по возрастанию)>
Пример флага: 10-230-259-610

Итого, нам нужно определить количество исключённых каким-то образом дефектов и их CWE. Как заковыристо…

В первую очередь проверим лог контейнера с инструментом.

Обращаем внимание на строки .bandit file: ./.bandit — это файл конфигурации bandit.

C:\93a6f20acc78c07e2608d0e8cfcbb8fdc093815ffa50b7e2fb3efb727e97849d

Чтобы изменить файл конфигурации, его необходимо найти 😉
В docker-compose.yaml нет volumes с конфигурацией.

C:\94d46f73d39ebff2b7e1c3ee81bf71a99eb733e6eab21f42a896dbc66ea82656

В Dockerfile тоже.

C:\401044f68359699f206f6186a44d582dc5892f0e0341f0d0bd5c0558e07be933

Вероятнее всего, файл конфигурации лежит в папке исходников уязвимого приложения. Находим конфигурационный файл .bandit

C:\848a720fe7486daadfd2db04f9d74e7c47bbf12157253d7673877d2f9b8b42bb

Видим внутри инструкцию — игнорировать дефекты из правил B104, B105, B608.

Можем в документации подсмотреть, за что отвечают эти правила.

C:\c355511583d51a60f581fdd18ce4de42153e5e5b8b2974ddab18703ea26d5140

Удалим файл и перезапустим сканирование. Загрузим новый отчет в SecObserve.

C:\63486a374db4052abff60f3e76c9d170936c0002e3a03aedea7d5dcedcf0e9b6

Добавились 7 дефектов, 5 обновились (были загружены отчетом ранее и не исправлены). Это нам понадобится для флага. То же самое видим в веб-интерфейсе 

C:\b6c37504c68b8dd6909f3246cc065186d560924bb313963213ea67766a54e60b

Посмотреть CWE новых дефектов можно через веб-интерфейс

C:\baeb20674a9efd34203a636abfcefe60b57ba2f6da3e3be2afae7542f823eb89

Чтобы определить только вновь загруженные дефекты, перейдем в один из них и посмотрим историю загрузки (время Created и Last change совпадает = дефект новый).

Быстро переключаться между дефектами можно по кнопкам «Вправо–влево» сверху (на скриншоте выделено синим). 

C:\4a6b0acb3ab18deb592140651fd02b683cf919425b3493d1435a67b073513ef4

Также CWE можно соотнести, открыв «сырой» sarif-отчет инструментом jq или почитав описание правил на GitHub 🙂

C:\bd7b3e2e8df4e9f0a6062c2371f74bef4828b25e7e4527138be91301ab317ff7

Итого — заново включенные правила:

  • B104 (bind all interfaces → CWE-605)

  • B105 (hardcoded password string → CWE-259)

  • B608 (SQL injection pattern → CWE-89)

Формируем флаг согласно ожидаемому формату ответа <кол-во уникальных дефектов (не типов дефектов, не правил!), что не попали в анализ>-<номера CWE дефектов, что не попали в анализ (по возрастанию)>

Ответ: 7-89-259-605

Седьмой флаг: Какая уязвимость была найдена тремя SAST? 

Ожидаемый формат ответа: 

<название функции endpoint`а в исходном файле>-<файл источник>.<расширение файла>-<номер строки, с которой начинается уязвимость согласно отчету opengrep>
Пример флага: login_me-main.py-32

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

Раз дефект найден тремя SAST, то он обладает наибольшим количеством дублей в SecObserve, используем фильтр Duplicates=Yes.

C:\c8524dcf628c7248330614d5281b9f177e91fbbda3ba4cee9c78c37631427df3

Дефект SQLi найден двумя инструментами Bandit, CodeQL в файле transfers.py, теперь попробуем поискать среди других инструментов в описании. 

Попробуем найти еще сработки данного типа в этом файле (фильтром Source=transfers.py) и искать по описанию (фильтр Title=sql).

C:\8e1b3aa9ab6c471acdeff2f417de355c3899d8dbbe8171a3119d882954f3f9cc

Теперь все SAST: Bandit, CodeQL, Opengrep указывают на transfers.py, если открыть дефекты, можно проверить, что это одна и та же SQLi. 

Почему SecObserve сразу не отобразил дефект от Opengrep?

Дело в том, что Bandit и CodeQL указывают на строку 277, а Opengrep — на 276.

В коммерческих ASOC/ASPM решениях такое недопустимо, ну а мы работаем с open-source-платформой, поэтому все ей прощаем (или заводим Pull Request).

C:\9a2ca0a93755d2e26d92b07c8e0bb47b5c00485752511be3cae13fa2a767da56

Для формирования флага у нас уже есть строка 276 из отчета Opengrep, но как автоматизировать поиск и подтверждение эксплуатации данной SQL-инъекции? 

Можно попробовать инструментами ffuf и sqlmap.

Из любви к искусству, несмотря на то, что у нас есть исходники, пройдемся по словарю возможных API-эндпойнтов:

https://github.com/danielmiessler/SecLists/blob/master/Discovery/Web-Content/api/api-endpoints.txt

C:\a33c6305cd201109e275e0e210d323a2c1ff22d18e51517c406567602340b4a8

Swagger! Без аутентификации! В подарок спецификация openapi.

C:\843eb11bffc34b65ca339f38da90a01636ebe335d367113efb0f2d294f235826

Вытащим URL-эндпойнты (только для GET) из схемы openapi.json с валидными тестовыми данными в отдельный файл, учитывая тип параметра и значение по умолчанию.

C:\a2d803f1b2b984dc09fdda2f18605bf88a22760a9090a40ee8c2e8a87b7abe29

> разве что query-параметры типа array не будем использовать

Теперь пройдемся mass-сканированием с sqlmap.

C:\a61d9598a828c18d5f08425686ea1629ef88c39ec67cbc04aafdabb4e58bf9e0
C:\870f0302559259fab064e34662f96a1478c3bb3dd4bdee4433af820731752556

Для подтверждения запустим прицельно на уязвимый эндпойнт.

C:\8af8c97a98b6835589d58e11ac0cc2b5cceaba6d02aeeecf8ecea31866dd9b24

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

C:\efe85968372c79bed6a6e72a4c6a739a2e39aa22d228b1388b50c304fd9bfc1d

Флаг dump выгрузит все таблички, их можно увидеть на скрине.

C:\9ebb2dfee38022b583b4cf6cbb4a67406a830829dc7f4da2b8efc8a8d433843c

Почему это работает?

SQL-запрос строится через f-string и text(...)c прямой подстановкой q и limit, то есть без параметризации

C:\ee04c8045e46a679428ef6588745dca097f9e1ac374152eade6a69a5f05ff9ba

Теперь, когда все нашли и подтвердили, формируем флаг согласно ожидаемому формату ответа <название функции endpoint`а в исходном файле>-<файл источник>.<расширение файла>-<номер строки, с которой начинается уязвимость согласно отчету opengrep>

Получаем search_transactions_unsafe-transfers.py-276

Вариации ответа, которые в итоге засчитывались — «search_transactions_unsafe-transfers.py-276» и «search_transactions_unsafe-routers/transfers.py-276»

Восьмой флаг: Какой дефект Trivy определил как самый критичный по CVSS?

Ожидаемый формат ответа 

<значение CVSS_V3>-<artifactId>@<version>
Пример флага: 10-log4j-core@2.3.1

Искать дефект с максимальным CVSS V3 будем в отчетах Trivy по образам frontend и backend, SBOM мы пока не обогащали информацией о уязвимостях, поэтому там не будет CVSSv3. Формат отчетов Trivy — SARIF, так как только его принимает SecObserve. 

C:\83deecbeb145905ab98507178b28937d06a3f47e5daab61dff4ac60838f196e2

Смотреть сразу все дефекты Trivy можно, отфильтровав по Scanner=Trivy и критичности Critical, High.

C:\25575e7d49bf76f0c9f9e5b41d24c9705d8a273867cc51c28f0dd643e3458eca

Но, к сожалению, SecObserve не позволяет отсортировать по CVSSv3, можно либо провалиться в дефект и листать в поиске самого критичного, либо раскрывать по одному дефекту на главной странице, что одинаково медленно по скорости 🙂

Седьмым в списке находим jaraco.context

C:\3d2b0ead0970ff1538f7c24a18f3fa5a3544df145b5135cd7a9940e4653fa45b

Данной информации нам достаточно, чтобы сформировать флаг.

Альтернативным способом можно изучить отчеты Trivy в консоли, используя jq, grep и sort.

Находим среди всех отчетов самый критичный CVSSv3.

C:\aa076e448c49ae26d662087a4737a77a8c714d3e4f86780da883855c0cdb5cd7

Выведем с критичностью 8.6 по CVSSv3.

C:\57d3c20ec20f0857c72dbee2f74beb385c5a1d9f27c7fe7046981ed8d84c7977

Данной информации нам достаточно, чтобы сформировать флаг согласно ожидаемому формату ответа <значение 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 и построении процессов управления уязвимостями зависимостей.

Девятый флаг: Укажите дефект с критичностью Critical в SecObserve (найденный OSV-сканером) 

Ожидаемый формат ответа: 

<CVE>-<Component name из отчета>@<Component version из отчета>
Пример флага: CVE-2026-10000-main@1.0~deb

Один из флагов возможно сдать, используя в анализе инструмент OSV, что использует загруженный в проект SBOM. OSV встроен и запускается через веб-интерфейс SecObserve как по расписанию, так и вручную. Обратимся к документации, чтобы запустить сканирование https://secobserve.github.io/SecObserve/integrations/osv_scan/.

C:\bd27a96587c985206bd9820fad14ec9952d6406e5b08e4f22e02258fbcdb0642

После сканирования доступны новые дефекты.

C:\98c89a633c74c5d293d69c5d6fa4afb389b0d34bdc1952504b94c8dbde0b29b7

Выберем Critical.

C:\57621229514637689b9ae5e60603d2d9341d5513970c84dab1e373e553d742cc

Результаты сканера OSV нестабильны и зависят от БД, поэтому в рамках задания флаг засчитывался всем. Расхождения в результатах SCA-сканеров — действительно одна из главных проблем, с которыми сталкиваются специалисты по безопасности приложений. Разные сканеры, анализируя одно и то же приложение, очень часто выдают разные списки дефектов в зависимости от обновлений в своих БД. Например, сканер может показывать критические CVE там, где разработчики уже признали проблему неопасной, и в новом обновлении БД-сканера этой уязвимости может быть назначен низкий уровень критичности.

Десятый флаг: Укажите payload, подгружаемый DAST при сканировании приложения

Ожидаемый формат ответа: 

<свойство в JavaScript, из-за которого уязвимость эксплуатируема>-<текст, выводимый в консоль лог>
Пример флага: innerHTML-XSS_TRAINING_TRIGGERED

Дефектов DAST у нас не так уж и много.

Изучив отчеты ZAP и Nuclei, понимаем, что только последний указывает на Stored и Reflected XSS. 

C:\f28239df6f62cfc9ee1ce291185747cb8d96b567ee4b72abad66293585fd3b2e

Рассмотрим подробнее Stored XSS.

C:\a1b77e6812c5e16d3dd9350a04dd7082ce7e2959da621f4f2171e8007aff72ea

Кастомные правила для nuclei ищут среди api/v1/transactions маркер XSS_NUCLEI_STORED_TEST.

C:\2fabde7972095149aeb7a053b75d53c54360626dedaefc2b61e8dc58d8ee002a

Скрипт scan.sh автоматизирует создание пользователей и последующее внедрение инъекции в переводы, там мы видим тот же маркер.

C:\0c0290ce77b182f589d950eff6818030f81736ed13aea7c14159792d0990bb96

И на главной странице приложения тоже будет он.

C:\d9016721bc580d81d8ad146650d10cf7f8756a6f5170e6138eb0265d9773fbc9

Выполнение произвольного JavaScript позволяет совершить действия от лица любого пользователя, который откроет заражённую страницу. Самое опасное — эксфильтрация данных других пользователей, включая сессионные cookies, токены авторизации, содержимое sessionStorage / localStorage, а также доступ к страницам, которые видит жертва.

Попробуем проэксплуатировать то же самое вручную.

C:\2480425936763c3ada21156ac3bfbe34d30135c25cb16e52aee0c8d5877007ec
C:\74e988d260fe917dde91ab271fd8766c47cfbfda517fee2e59f715bc6419f5bc

Проверим историю транзакций.

C:\3949778b0d4efc060e3ca5e084d809fa30109fb5f96cb58c3a5fc2c06f71e759

При этом на главной странице в истории подобного поведения нет.

C:\8bdfceef52e5d0a3fad0319ce53d6afd871609b2ea38a9bf04c737b32c7569dd

Также в флаге от нас ожидают указание «свойство в JavaScript, из-за которого уязвимость эксплуатируема», для этого обратимся к файлам исходников.

Известные свойства, приводящие к XSS, легко гуглятся, даже в примере оставлена подсказка.

C:\99c7930193024792f1aa125659b878065a576cf7c1216dbb0c37cd4957dff5ac

Данная конструкция в History.tsx как раз и допускает XSS.

C:\2555a45b6eb90d0afdab237ddb31f2477ebceb64e3c248c523b272b9011506bd

Формируем флаг согласно ожидаемому формату ответа <свойство в JavaScript, из-за которого уязвимость эксплуатируема>-<текст, выводимый в консоль лог>

Ответ: dangerouslySetInnerHTML-XSS_NUCLEI_STORED_TEST

Выводы

  1. Ручной анализ остаётся обязательным даже при наличии в пайплайне SAST/DAST/SCA-инструментов. Наиболее критичные дефекты в задании были связаны с логикой приложения: bypass аутентификации через X-Debug-User, IDOR и API без проверки авторизации. Подобные проблемы часто не определяются сканерами.

  2. Кастомизация правил SAST и DAST — обязательная практика, те, что «из коробки», дают много false-positive или вовсе ничего не находят. Стандартные правила ZAP и шаблоны Nuclei не покрывают blind boolean-инъекции в SQLite, поэтому для подтверждения эксплуатабельности дефектов приходится переходить в ручной режим, как в примере с sqlmap. 

  3. SCA-инструменты сильно зависят от качества и актуальности vulnerability database. Результаты Trivy и OSV отличались в зависимости от версии базы и времени обновления: дефект может как появиться при повторном сканировании, так и исчезнуть — если неэксплуатабелен или переведен в статус «инфо». Для эффективной работы нужны инструменты с валидированной базой и данными об эксплойтах, ручная верификация критичных находок, проверка достижимости в коде. Это необходимо учитывать при приоритизации CVE и построении процессов управления уязвимостями зависимостей. 

  4. Наибольшую ценность даёт централизованный security pipeline с агрегацией результатов от разных инструментов, нежели отдельный инструмент. SecObserve позволил сопоставить результаты разных SAST и быстрее определить действительно критичные дефекты среди большого количества сработок.

  5. Даже учебное приложение показало типичные проблемы реальных проектов: debug-функциональность, устаревшие API или вовсе без авторизации, небезопасная работа с SQL, ошибки контроля доступа и небезопасный frontend-рендеринг. Большинство таких дефектов возникают из-за недостатка secure-by-design практик и зрелых процессов DevSecOps (смотрите наш открытый фреймворк DAF).

ссылка на оригинал статьи https://habr.com/ru/articles/1057476/