Почему CI/CD так важен? Он представляет собой автоматизированный конвейер сборки и доставки ПО и, если говорить в контексте безопасности, существует целый ряд критичных инцидентов безопасности, например внедрение бэкдоров в ПО, подмена артефактов (supply chain), утечка чувствительных данных клиентов, компрометация deployment environment и так далее.

А если посмотреть на проблемы в контексте разработки публичного облака?

Так, например, мультитенантность инфраструктуры приводит к экспоненциальному росту проблем, связанных с расширением экспансии злоумышленников, ведь, получив доступ к CI/CD и внедрив бэкдор в ПО, которое затем будет развёрнуто в продакшене, мы получим дыру в безопасности, причём импакт для облака усугубляется, так как полученная уязвимость:

  • Позволяет проводить горизонтальное перемещение (lateral movement) внутри облачной инфраструктуры.

  • Усиливает риски за счёт высокой связанности компонентов (общие сети, API или системы управления).

  • Может приводить к каскадной компрометации различных систем, поскольку облачные среды используют IAM, общие хранилища (S3, Blob Storage).

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

Ладно, ладно — для того, чтобы заинжектить бэкдор в ПО, нужно иметь доступ к self-hosted github/gitlab, то есть быть внутренним злоумышленником, имеющим доступ к коду, права на пуш в репозиторий, и пройти все ревью ответственных коллег — это очень сложная атака, успешность которой зависит от огромного количества факторов. Куда проще скомпрометировать внутренние режистри — например, harbor, nexus, artifactory, подменить там библиотеку или контейнер и получить такой же импакт за счёт автоматизации CI/CD и отсутствия контролей безопасности.

Примеры можно приводить долго, но мы считаем, что хорошо бы рассмотреть различные кейсы более детально, как это может быть реализовано, как этого избежать — чтобы предупредить вас о существующих рисках безопасности.

Мы — Алексей Федулаев, лид Cloud Native Security, и Андрей Моисеев, Cloud Native Security DevSecOps в MWS. И сегодня поговорим с вами об атаках на CI/CD — разберём их, начиная от misconfig и заканчивая supply chain, расскажем, как от них защищаться. Ведь если применять технологии без понимания, а особенно использовать CI/CD, не задумываясь о безопасности, можно получить ситуацию, когда всё горит, но непонятно где.

Бинго уязвимостей
Бинго уязвимостей

Нашли что-то знакомое? Тогда вы не зря потратите время на чтение.

Дисклеймер. Системы сборки и доставки ПО применяются практически всеми участниками разработки. Часть разработчиков может работать в формате аутсорса или аутстафа, некоторые работы отданы смежным подразделениям, иногда бывает, что разработчики меняют проекты, а есть ещё девопс-инженеры и тестировщики, которые могут иметь доступы ко множеству проектов. Такой формат накладывает определённые риски безопасности и предоставляет вектор атаки потенциальным злоумышленникам.

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

Поехали ломать.

PPE

PPE (Pipeline Policy Execution) — злоупотребление правами доступа к выполнению своего кода и команд.

Как правило, при написании конвейеров используются переменные, ведь это тоже код. В этих переменных могут храниться токены доступа к сервисам, API-токены CI/CD и прочие чувствительные данные, которые могут предоставить возможность нарушителю проникнуть за пределы CI/CD. Например, часто используются GItlab Variables, чтобы не хранить ключи в самих манифестах. Мы можем использовать базовый вывод через выполняющуюся джобу, которая выведет в свой лог нужные нам значения. А затем подсунуть этот код девопсу или разработчику в компании, который его выполнит. В нашем случае мы сами его выполним, ведь мы аутсорс-девопсы.

В данной джобе мы просто выводим секреты, которые хранятся как CI/CD Variables
В данной джобе мы просто выводим секреты, которые хранятся как CI/CD Variables
Так мы сможем видеть в логах джобы ключи
Так мы сможем видеть в логах джобы ключи

По умолчанию ключи выводятся в b64-кодировке. Если мы размаскируем Base64, мы получим сам секрет.

Интересное замечание — сам Gitlab понимает, что это секрет, сравнивая выводимые в лог джобы символы с фактическим значением секрета, поэтому, как [MASKED], будут отображаться все включения строки с секретом в логах джобы, даже если это просто текст.

Да, это база, и никого не удивить такими атаками. Но знать и предотвращать нужно, поэтому напомним методы предотвращения такой атаки:

●      изоляция окружений;

●      грамотное управление правами;

●      отсутствие выдачи лишних прав;

●      использование Protected Branches и Protected Runners.

AbuseFlow

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

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

Но что, если кто-то случайно добавит свой ключ в код? Или забудет убрать тестовые ключи? А может, закоммитит папку, где лежат другие чувствительные данные?

Вариантов, как секреты могут попасть в систему контроля версий SCM, множество. Но проблема не столько в том, что они туда попадут, а в том, что, получив к ним доступ, можно получить доступ к тестовым стендам. Если это не так критично, можно использовать найденные секреты для дальнейшего развития атаки, например, горизонтального продвижения по сети через ssh spray.

Если секреты попадают на этап сборки, они могут проникнуть в приложение и, как следствие, в production. Это создаёт серьёзные риски для безопасности. Поэтому важно предотвращать такие ситуации на этапе сборок, а не пытаться исправлять последствия уже после того, как секреты оказались в коде.

В нашем пайплайне используется джоба secret_detection, которая с помощью gitleaks позволяет находить секреты в коде:

Таких инструментов множество, например deepsecrets, semgrep.

Извлечь секреты из кода мы не сможем, их там попросту нет
Извлечь секреты из кода мы не сможем, их там попросту нет

Однако pipeline может быть настроен некорректно: реализована определённая логика через переменные или флаги или, как в нашем случае, используется популярный gitleaks, который может задавать игнор-лист для результатов анализа. Соответственно, используя механизм .gitleaksignore, который работает схожим образом с .gitignore, мы добавим ключ или вообще всю папку в игнор-лист. Останется только подождать, пока кто-то допустит ошибку и запушит секреты в SCM.

Джобы выполняются, деплой проходит успешно
Джобы выполняются, деплой проходит успешно

Но что, если изменение .gitleaksignore отслеживается и разрешено только owner проекта? Как тогда обойти защиту? Как правило, в крупных командах и репозиториях управление автоматизировано и кто-то использует шаблоны. Кстати, теперь им на смену пришли components.

PPE — схема атаки
PPE — схема атаки

К примеру, есть remote-шаблон и local-шаблон, при этом локальный шаблон имеет приоритет и всё, что было в remote, можно переопределить в локальном.

Так мы можем в нашем локальном gitlab-ci.yml определить джобу secret-detection, в которой просто добавим exit 0. В этом случае при сборке общего пайплайна значение поля script будет заменено на то, которое было указано в локальном файле, так как у него выше приоритет. Джобы выполнятся успешно без ошибок или предупреждений:

Но если у фирмы есть самописный CODEOWNERS, тогда что?

Пример с CODEOWNERS
Пример с CODEOWNERS

Или платный GitLab, где его можно использовать по умолчанию, или грамотно выстроенный процесс code review, включая approval rules:

Или используются сторонние проверки, например пайплайн «особое мнение», доступ к которому ограничен:

Как мы тогда их похакаем? Будем делать кое-что посерьёзнее. Для воспроизведения этих атак потребуется некий prepairment steps. Так что, если кто-то хочет повторить, начнём с регистрации GitLab Runner от разных пользователей:

●      с правами sudo (user with sudo);

●      базовая регистрация (user gitlab-runner);

●      ограниченного пользователя (noroot user).

Затем запустим простую джобу, чтобы убедиться, что всё настроено корректно:

Обычно базовая регистрация показывает пользователя GitLab Runner, созданного при регистрации раннера
Обычно базовая регистрация показывает пользователя GitLab Runner, созданного при регистрации раннера
Если использовать sudo-регистрацию от пользователя с правами sudo, раннеры будут зарегистрированы от этого пользователя с соответствующими привилегиями
Если использовать sudo-регистрацию от пользователя с правами sudo, раннеры будут зарегистрированы от этого пользователя с соответствующими привилегиями
Непривилегированный пользователь без прав sudo не сможет выполнять сборки, но это не критично
Непривилегированный пользователь без прав sudo не сможет выполнять сборки, но это не критично

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

Ну и что мы со всем этим будем делать? Перейдём к следующему виду атак.

PBAC

Атака PBAC связана с недостаточным управлением правами в рамках pipeline-based access control. Мы можем исследовать, что произойдёт с привилегированными раннерами, если использовать их в атаках.

Начнём с секретов, например попробуем вывести содержимое /etc/shadow:

Не получилось, потому что мы забыли добавить sudo в запуске команды
Не получилось, потому что мы забыли добавить sudo в запуске команды

Добавим sudo:

Получилось, потому что пользователь также был добавлен как NOPASSWD в SUDOERS
Получилось, потому что пользователь также был добавлен как NOPASSWD в SUDOERS

Мы получили доступ к /etc/shadow. Это мало что даст — просто зарегистрированный раннер с пользователем, так мы не сможем восстановить хэш. Что с этим делать?

Попробуем получить доступ к системе, например к раннеру. Если есть возможность заставить кого-то выполнить malicious code в рамках джобы, можно попробовать получить initial access к раннеру. Будем пробовать reverse shell. Это опасная, но эффективная техника, позволяющая открыть терминал и выполнять любые команды на хосте.

Напишем простой скрипт для reverse shell и отловим его на тестовой машине:

Но есть нюанс: когда джоба завершится, мы потеряем reverse shell. Если reverse shell активен, джоба будет выполняться до закрытия сессии. Это заметно, но, пока сессия открыта, у нас есть доступ.

Попробуем сделать более элегантно:

  1. Снова откроем reverse shell.

  2. Используем системный cron, который есть на большинстве систем.

  3. Добавим задачу в crontab:

4. Тогда reverse shell будет открываться из cron без необходимости зависания джобы и вообще вне контекста выполнения каких-либо джоб от CI/CD, по сути мы просто оставим бэкдор через cron:

Тут тоже есть нюанс: reverse shell будет запущен от пользователя, под которым выполнялась джоба. Если бы это был root, можно было бы сразу получить full access.

Lateral movement shell

Доступа к раннеру часто недостаточно, тогда можно применить lateral movement. Это любая техника, позволяющая исследовать систему и распространяться дальше. Например, украли раннер → украли хост → украли CI/CD → GitLab теперь под нашим контролем. Запустим майнеры, и всё прекрасно.

Но что искать на раннере? Очевидно, первое, что приходит в голову, — SSH-ключи. Поищем их:

Чтобы провести атаку, мы можем попробовать исследовать раннер и собрать доступные креды:

Это могут быть ключи, которые использовались при первичной настройке системы или были оставлены для дебага на раннерах. Часто встречаются случаи, когда не использовались vaults и на системе можно найти и извлечь такие данные вплоть до ключей от staging или даже production. Имея ключ от production, можно загрузить туда любой код и извлечь все пользовательские данные.

PBAC — схема атаки
PBAC — схема атаки

Однако если ошибок нет, зарегистрирован дефолтный раннер, всё настроено правильно — нет sudoers, nopassword, пользователи GitLab Runner не имеют лишних привилегий, образ захарднен, права установлены корректно, используются эфемерные раннеры в Kubernetes, которые поднимаются в отдельных подах и исчезают, или вообще не используются Shell-раннеры, то задача усложняется. Но применяете ли вы такие меры?

Lateral movement docker

Следующий вариант — мы можем внедрить код напрямую в сборочную инфраструктуру, раннер или файлы на нём. Раннер — это по сути образ, который где-то собирается и куда-то складывается. А куда? Правильно — в registry. Соответственно, где-то должны быть аутентификационные данные. Попробуем их найти:

А наш токен — это b64 encoded логин и пароль:

Имея на руках эти креды, мы можем просто подменить образы в registry. Например, собрать свой образ с произвольной вредоносной нагрузкой:

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

Lateral movement docker — схема атаки
Lateral movement docker — схема атаки

Что будет, если мы не найдём кредов от Docker, от registry? Сейчас посмотрим.

Code Injection

Попробуем внедрить вредоносный код в сборочную инфраструктуру, раннер или файлы на нём.

Сборка запускается на раннере, который принимает код из git-репозитория:

В папке раннера находится всё, что есть в репозитории, и, имея доступ, мы можем заменить код:

Поскольку в этом случае раннер — железная нода, на которой, скорее всего, выполняется несколько джоб параллельно, из одной джобы мы можем повлиять на код другой и, таким образом, выкатить произвольный код на production.

Code Injection — схема атаки
Code Injection — схема атаки

Но что делать, если нет Shell-раннера? Продолжим работать с Docker. Мы смогли угнать контейнер, теперь попробуем поработать с ним напрямую.

Например, представим, что есть Docker-сборка, который использует DinD. Скорее всего, проброшен Docker-сокет, чтобы сборка работала корректно, так как сборки иногда требуют привилегий или доступа к хостовой системе:

Подробнее про побеги из контейнера можно посмотреть в этом видео.

Reverse-shell via crontab

Наша цель — доступ к хосту. Для этого мы для начала сбежим из контейнера. Одна из известных техник — использование проброшенного Docker-сокета:

Итак, ревершелл успешно открылся, и мы внутри:

Попадание на ноду, которая используется для запуска docker-раннеров, позволяет внедриться в любую сборку, которая выполняется в контейнерах. Мы находимся на хосте и можем выполнять команды и взаимодействовать с кодом: прокинуть реверс хоста дальше, добавить свои ключи, найти чужие ключи, выполнить exec в любой контейнер, подменить файлы при сборке и много чего другого.

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

С помощью команды docker ps мы можем получить список всех контейнеров. Например, видим контейнер build:

Каждая джоба, которая прилетает, поднимается отдельным контейнером. Будем пытаться эскейпить:

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

Таким образом мы можем вытащить всё, что нужно:

Однако если на одной железной ноде находятся как обычные, так и protected-раннеры или если раннер может принимать обычные и protected-джобы, это не сработает. Чтобы предотвратить такие атаки, в компании должны быть отдельные раннеры для protected-веток и хосты для protected-раннеров. Тогда сбежать из них не получится, всё будет проходить через Code Review.

А если есть подпись? В лучших традициях контейнерной безопасности многие знают про SLSA, который включает подпись и другие меры. Однако если у нас есть прямой доступ к раннеру, мы можем вытащить ключ для подписи:

Используя этот ключ, мы можем подписать что угодно и наш код станет доверенным.

Таким образом, попав на раннер, мы можем собирать все переменные окружения с других джоб, которые прилетают на этот раннер. Даже если используется vault или ключи прокидываются хитро, в момент сборки этот ключ всё равно существует в сборке и его можно вытащить.

Reverse-shell via crontab — схема атаки
Reverse-shell via crontab — схема атаки

Но что, если в компании нет таких мисконфигураций и всё сделано правильно? Есть ли другой способ провести атаку? Здесь поможет кеширование — ускорение сборок.

Cache poisoning

Сборочные раннеры могут использовать кеши для ускорения работы. Мы можем попробовать атаковать кеш и отравить его.

Рассмотрим пример. Есть кеш, который используется джобой:

Эта джоба делает условную сборку. Например, просто собирает бинарь и кладёт его в кеш:

Позже этот бинарь подтягивается и используется в других джобах без необходимости его пересборки. У нас есть restore, сборка и push в кеш:

Мы можем написать джобу, и она будет подменять бинарь, который собирается. Мы пересоберём его в своей отдельной ветке или на раннере с другим тегом:

Если отравленный кеш попадёт на тот же раннер, где происходила сборка, то это вполне валидная схема. Сборки идут постоянно, поэтому постепенно все раннеры могут быть отравлены:

При перезапуске кеш уже будет отравлен и наш бинарь будет содержать вредоносный код. Таким образом, даже в мейн-ветке при переиспользовании кеша будет наш поддельный бинарь:

Управляя кешем, мы можем подделывать кеш для других джоб, подкладывая туда нужный нам инструментарий. Этот инструментарий соберётся, подпишется, пройдёт проверки и спокойно отправится туда, куда нужно.

Cache poisoning — схема атаки
Cache poisoning — схема атаки

GitLab недавно ввёл отдельный кеш для protected-раннеров, предназначенный для protected-веток. Что делать в этом случае? Дальше развивать атаку на кеш нет смысла, поэтому попробуем усложнить предыдущие атаки или рассмотреть другие варианты.

Настройки кеша в GitLab для protected-веток
Настройки кеша в GitLab для protected-веток

Advanced flow abuse

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

В этом примере мы добавляем условие, при котором будет игнорироваться весь стейдж security. А значит, будут пропущены все стадии безопасности. При этом все проверки успешно завершатся, но не будут выполнены. Так можно обойти любую защиту.

Этот подход подобен атаке для переопределения шаблонов. Только мы меняем настройки на самих раннерах, а не в пайплайне, при этом варианте уже не помогут ни Approval Rules, ни Code Review.

Лог пайплайна с отключением security на раннере
Лог пайплайна с отключением security на раннере

Избежать этого можно, используя всё тот же пайплайн «особое мнение» с отдельным раннером или прописать повторный запуск в after script:

Есть нюанс: даже если post-build скрипт выполнится, джоба может не завершиться с ошибкой, поскольку post-build скрипт заметит, что основной скрипт выполнен успешно. Он просто завершит работу:

Выполнение джобы с after-script
Выполнение джобы с after-script

И наконец, давайте просто подменим раннер на наш.

Runner hijacking

Если у нас есть доступ к конфигу раннера, мы можем извлечь его токен и сделать что-то пакостное. Мы можем зарегистрировать свой раннер с этим токеном, и GitLab будет считать его валидным:

Да, мы можем создать дубликаты раннера, но это будет заметно. Вместо этого можно сделать что-то поинтереснее — Runner spoofing.

При работающем раннере есть «окно», через которое он отчитывается в GitLab о своей активности. GitLab определяет, на какой адрес отправлять задачи. Мы можем заспуфить раннер, подделав сигнал раннера, и теперь все задачи будут отправляться на наш поддельный раннер. При этом реальная джоба не будет выполнена, но все переменные окружения попадут к атакующему. Это один из эффективных вариантов эксплойтов:

Лёгким движением руки мы получаем все переменные окружения совершенно другого раннера.

В заключение

Мы разобрали основные атаки на CI/CD и затронули способы митигации. Надеемся, это было интересно и полезно. Помните: если CI/CD ломаете не вы, это делают хакеры.

У себя, при разработке публичного облака MWS Cloud Platform, мы также используем собственную разработку — сервис Star Maker. Star Maker — многофункциональная утилита, позволяющая контролировать  настройки в GitLab для всех репозиториев, собирать различные метаданные, необходимые для аудита (не только в контексте gitlab).

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

Пример отчёта Star Maker
Пример отчёта Star Maker

P. S. Мы также решили оставить для вас небольшой threat-checklist, который может помочь вам оперативно провести ревью конфигурации gitlab в вашей команде.

Спасибо за внимание и до новых ресерчей про безопасность!


Читайте и смотрите другие материалы про MWS Cloud Platform:

Комментарии (0)