Red Teaming GitHub: Part 2 GitHub CLI

Background

In my first post, I went through at a high level the GitHub attack surface and different areas for Red Teamers to research and look to exploit. In this post, I review the GitHub CLI (gh), looking at the functionality it provides and potential ways an adversary may look to abuse it.

But why would you care about the GitHub CLI? Recently authenticating into GitHub is becoming more challenging, specifically if you want to do development. It fundamentally comes down to some form of access token and there are a limited set of tools that can do that. For the CLI, GitHub directs you down the route of GitHub CLI as such we’ll likely see more developers turn to the tool to authenticate with their repositories and associated organisations.

Examining the GitHub CLI

Examining the CLI, the following options are presented back:

$ gh --help
Work seamlessly with GitHub from the command line.

USAGE
  gh <command> <subcommand> [flags]

CORE COMMANDS
  auth:        Authenticate gh and git with GitHub
  browse:      Open the repository in the browser
  codespace:   Connect to and manage codespaces
  gist:        Manage gists
  issue:       Manage issues
  org:         Manage organizations
  pr:          Manage pull requests
  project:     Work with GitHub Projects.
  release:     Manage releases
  repo:        Manage repositories

GITHUB ACTIONS COMMANDS
  cache:       Manage Github Actions caches
  run:         View details about workflow runs
  workflow:    View details about GitHub Actions workflows

ALIAS COMMANDS
  co:          Alias for "pr checkout"

ADDITIONAL COMMANDS
  alias:       Create command shortcuts
  api:         Make an authenticated GitHub API request
  completion:  Generate shell completion scripts
  config:      Manage configuration for gh
  extension:   Manage gh extensions
  gpg-key:     Manage GPG keys
  label:       Manage labels
  ruleset:     View info about repo rulesets
  search:      Search for repositories, issues, and pull requests
  secret:      Manage GitHub secrets
  ssh-key:     Manage SSH keys
  status:      Print information about relevant issues, pull requests, and notifications across repositories
  variable:    Manage GitHub Actions variables

HELP TOPICS
  actions:     Learn about working with GitHub Actions
  environment: Environment variables that can be used with gh
  exit-codes:  Exit codes used by gh
  formatting:  Formatting options for JSON data exported from gh
  mintty:      Information about using gh with MinTTY
  reference:   A comprehensive reference of all gh commands

FLAGS
  --help      Show help for command
  --version   Show gh version

EXAMPLES
  $ gh issue create
  $ gh repo clone cli/cli
  $ gh pr checkout 321

LEARN MORE
  Use `gh <command> <subcommand> --help` for more information about a command.
  Read the manual at https://cli.github.com/manual

To manage anything remotely will require authentication (auth). To confirm we can issue a command to manage an extension or attempt to make an authenticated call to the GitHub API.

$ gh extension list
To get started with GitHub CLI, please run:  gh auth login
Alternatively, populate the GH_TOKEN environment variable with a GitHub API authentication token.
$ gh api help
To get started with GitHub CLI, please run:  gh auth login
Alternatively, populate the GH_TOKEN environment variable with a GitHub API authentication token.

Let’s determine what we can do unauthenticated. Looking at the config command we can see a few interesting settings, specifically the git protocol and the web browser.

$ gh config list
git_protocol=https
editor=
prompt=enabled
pager=
http_unix_socket=
browser=

The two options available are https and ssh.

gh config set git_protocol help
failed to set "git_protocol" to "help": valid values are 'https', 'ssh'

If we use https we’ll be redirected to the user’s browser to authenticate with GitHub but if we can change the config to ssh and the user configured an ssh key with GitHub we can possibly avoid the use of a password and two factor authentication. Let’s try changing the configuration.

$ gh config set git_protocol ssh

$ gh config list
git_protocol=ssh
editor=
prompt=enabled
pager=
http_unix_socket=
browser=

Great, so we can achieve the first stage. Let’s try and see if we can use an ssh key for authentication.

$ ssh [email protected]
[email protected]: Permission denied (publickey).
$ ssh [email protected]
PTY allocation request failed on channel 0
Hi wakeward! You've successfully authenticated, but GitHub does not provide shell access.
Connection to github.com closed.

Now we’ve confirmed we have access to the key, let’s see if we can authenticate with GitHub with it.

$ gh auth login
? What account do you want to log into? GitHub.com
? What is your preferred protocol for Git operations on this host? SSH
? Upload your SSH public key to your GitHub account? /home/wakeward/.ssh/id_rsa.pub
? Title for your SSH key: gh-ssh-auth
? How would you like to authenticate GitHub CLI? Login with a web browser

! First copy your one-time code: 45GG-BJ69
Press Enter to open github.com in your browser...
Opening in existing browser session.

Unfortunately not. We are asked to authenticate and we can point to our ssh key but we are immediately asked for an existing token or to login with a web browser.

If we continue with the web browser we are greeted with the normal browser authentication flow.

Once we are finished we need to pass the one-time code provided.

Returning to the CLI, we are now authenticated.

$ gh auth login
? What account do you want to log into? GitHub.com
? What is your preferred protocol for Git operations on this host? SSH
? Upload your SSH public key to your GitHub account? /home/wakeward/.ssh/id_rsa.pub
? Title for your SSH key: gh-ssh-auth
? How would you like to authenticate GitHub CLI? Login with a web browser

! First copy your one-time code: 45GG-BJ69
Press Enter to open github.com in your browser...
Opening in existing browser session.
✓ Authentication complete.
- gh config set -h github.com git_protocol ssh
✓ Configured git protocol
✓ SSH key already existed on your GitHub account: /home/wakeward/.ssh/id_rsa.pub
✓ Logged in as wakeward

GitHub CLI Tokens

So what have we learned from this test?

GitHub is dependent on the browser for authentication. If we compromise a browser we can look to intercept the authentication flow or at least determine when an user has authenticated to GitHub. If we have an implant on the local device, we may not want to proxy connections and wait for the user to authenticate using the GitHub CLI. Interestingly we can change the configuration of the browser if we wanted to.

Using the ssh protocol for git does not further our access, only defaulting how we interact with git. If we want to leverage a user’s GitHub account, we need a token. It is a matter of whether the user has an active token and how long does the token last.

Before we delve into what we can do with this token, how do we obtain a token from an authenticated user and how long it lasts?

The GitHub CLI provides an option to retrieve the token via gh auth token.

$ gh auth token
gho_IMNOTPUTTINGMYTOKENVALUEHERE

To determine how long this token lasts, we can refer to the documentation. From this we know that a GitHub CLI token will last for one year:

GitHub will automatically revoke an OAuth token or personal access token when the token hasn’t been used in one year.

And it will be revoked if pushed to a public repository or gist

If a valid OAuth token, GitHub App token, or personal access token is pushed to a public repository or public gist, the token will be automatically revoked.

We’ve got a token but where is it actually stored in our system?

Fortunately the tool is open source and can be found here: https://github.com/cli/cli. Let’s go through the code.

  1. The tool is written in go so the auth cmd option is probably a package and we can see this in login.go.
  2. When a new login cmd is run, it sets a number of options including config which is imported as a package github.com/cli/cli/v2/internal/config.
  3. After executing some checks it eventually invokes loginRun.
  4. The authentication configuration is set as authCfg calling the config package.
  5. We eventually end up invoking the Login function within the config package which uses a keyring package to set the secure storage for the token.

For Linux we can validate this by using the keyring command.

$ keyring --help
usage: keyring [-h] [-p KEYRING_PATH] [-b KEYRING_BACKEND] [--list-backends] [--disable] [operation] [service] [username]

positional arguments:
  operation             get|set|del
  service
  username

options:
  -h, --help            show this help message and exit
  -p KEYRING_PATH, --keyring-path KEYRING_PATH
                        Path to the keyring backend
  -b KEYRING_BACKEND, --keyring-backend KEYRING_BACKEND
                        Name of the keyring backend
  --list-backends       List keyring backends and exit
  --disable             Disable keyring and exit

To execute keyring, we need an operation, which in this instance will be get, a service and a username. The username should be the GitHub username (wakeward) but we need to know the format for the service.

We know from walking through the code, that the service set for the keyring is keyringServiceName(hostname) which is returned as "gh:" + hostname. The hostname if not set will prompt the user for two options:

options := []string{"GitHub.com", "GitHub Enterprise Server"}

So based on this our keyring query should look like the following:

$ keyring get gh:github.com wakeward
gho_IMNOTPUTTINGMYTOKENVALUEHERE

Bingo!

One interesting finding is there is a parameter for the GitHub CLI which is a boolean for using InsecureStorage that if set will save the credentials in plain text. Although this is interesting, we are considering the case that we’ve compromised a local user’s environment so as we’ve discussed, if they are a developer with the GitHub CLI and have previously authenticated, can just invoke the gh auth token to grab the token value.

Token Reuse

Now we have a GitHub token, can we use it on another system? Let’s give it a try.

From the GitHub CLI we can pass in a token via:

$ gh auth login --with-token < mytoken.txt

So if we copy our Token over to another system and login using it, we should be able to retrieve private repositories.

┌──(kali㉿kali)-[/tmp]
└─$ echo “gho_IMNOTPUTTINGMYTOKENVALUEHERE” > token.txt


┌──(kali㉿kali)-[/tmp]
└─$ gh auth login --with-token < token.txt

┌──(kali㉿kali)-[/tmp]
└─$ gh repo list

Showing 18 of 18 repositories in @wakeward

NAME                               DESCRIPTION                                                             INFO     UPDATED
wakeward/wakeward-obsidian         Obsidian Knowledge Repository                                           private  about 8 days ago
wakeward/wakeward-blog             wakeward blog                                                      private  about 23 days ago

Nice! But why do all this work?

When you are building out a threat model it is so important to check ideas you may have or any assumptions you make about a system. In this instance, we have tried to access GitHub via the CLI without a token and found it is not possible. We have also determined the expiry of the token (although we haven’t checked whether it actually is valid for a year!), found out where it is stored and that it is transferable. Lastly, we’ve dug into how the GitHub CLI works, following the code flows and discovered that we can pass a flag to output the token as plain text.

GitHub CLI Extensions

We have exhausted our review of the auth command, so are there any others that look interesting? The remaining CORE COMMANDS seem to be regular methods of interacting with GitHub and GitHub Action commands seem to be focussed on auditing workflows which is likely limited to information disclosure. This leaves the ADDITIONAL COMMANDS section which a few catch the eye. The first is extensions which allow the developer to manage internal and external extensions to the GH CLI. The next commands are secret, ssh-key and variable which all allow the management of sensitive configuration settings such as GitHub secrets, SSH keys and GitHub Actions variables respectively. Let’s focus on extensions as they are a way for an adversary to load malicious code into a developers environment.

Creating an Extension

GitHub CLI extensions can be viewed by the following command:

$ gh extensions browse

Browsing GitHub CLI Extensions

If the extension is published by GitHub it is given the (official) label whilst community provided extensions, even those provided by organisations, do not. An extension can take many forms as indicated by the Official Documentation.

There are a couple of options here:

  • Interpreted extension - used to make an extension with python, ruby, javascript etc.
  • Go precompiled extension - used to make a go compiled extension (i know obvious right!)
  • Non-Go precompiled extension - used for languages with java but requires a build script to defined so it can support cross platform integration
  • Interpreted extension manually - define a script to execute extension functionality

Let’s create a simple extension and see what is required to get it available in the GitHub CLI extensions list. I’ve chosen golang for this extension as from a security perspective creating a random binary for execution on another user’s environment is one of the objectives for an adversary and I’m interested in the level of scrutiny they are given by the GitHub team.

The extension I’m going to create is provide a quick view of the current branch protection settings for a repository and provide an overview of the risks of not having those configured.

Based on the documentation, extensions must fulfil this criteria:

  • The name of the extension must begin with gh and be followed by the extension name
  • The compiled binary must be located in the top folder with the name of extension
$ gh extension create --precompiled=go gh-branch-auditor
✓ Created directory gh-branch-auditor
✓ Initialized git repository
✓ Made initial commit
✓ Set up extension scaffolding
✓ Downloaded Go dependencies
✓ Built gh-branch-auditor binary

gh-branch-auditor is ready for development!

Next Steps
- run 'cd gh-branch-auditor; gh extension install .; gh branch-auditor' to see your new extension in action
- run 'go build && gh branch-auditor' to see changes in your code as you develop
- run 'gh repo create' to share your extension with others

For more information on writing extensions:
https://docs.github.com/github-cli/github-cli/creating-github-cli-extensions

Running the command initialises a git repository with template files.

$ ls -la gh-branch-auditor/
total 8412
drwxrwxr-x  4 wakeward wakeward    4096 Aug  1 10:27 .
drwxr-x--- 25 wakeward wakeward    4096 Aug  1 10:39 ..
-rwxrwxr-x  1 wakeward wakeward 8575832 Aug  1 10:27 gh-branch-auditor
drwxrwxr-x  8 wakeward wakeward    4096 Aug  1 10:27 .git
drwxr-xr-x  3 wakeward wakeward    4096 Aug  1 10:27 .github
-rw-r--r--  1 wakeward wakeward      42 Aug  1 10:27 .gitignore
-rw-rw-r--  1 wakeward wakeward     828 Aug  1 10:27 go.mod
-rw-rw-r--  1 wakeward wakeward    4492 Aug  1 10:27 go.sum
-rw-r--r--  1 wakeward wakeward     514 Aug  1 10:27 main.go

The main.go is configured with a simple hello world example to be able to compile the binary.

$ cat gh-branch-auditor/main.go
package main

import (
	"fmt"

	"github.com/cli/go-gh/v2/pkg/api"
)

func main() {
	fmt.Println("hi world, this is the gh-branch-auditor extension!")
	client, err := api.DefaultRESTClient()
	if err != nil {
		fmt.Println(err)
		return
	}
	response := struct {Login string}{}
	err = client.Get("user", &response)
	if err != nil {
		fmt.Println(err)
		return
	}
	fmt.Printf("running as %s\n", response.Login)
}

// For more examples of using go-gh, see:
// https://github.com/cli/go-gh/blob/trunk/example_gh_test.go

I won’t go through the entire development process for the extension but If you want to review the code, it is located here: https://github.com/wakeward/gh-branch-auditor/.

Regarding branch protection rules and the risks I’ve provided as part of the tool output, I may return on that topic as part of this Red Teaming GitHub series.

Uploading to the GitHub CLI Marketplace

With a working extension created, how do I publish it to the GitHub CLI marketplace? Once a release is created, the repository must have the topic of gh-extension and this will make the extension searchable via the web interface or gh extension browse.

gh-branch-auditor

It is unclear what security auditing is performed on GitHub CLI extensions but there is a warning about using Extensions outside of GitHub and GitHub CLI are not certified by GitHub.

Extensions outside of GitHub and GitHub CLI are not certified by GitHub and are governed by separate terms of service, privacy policy, and support documentation. To mitigate this risk when using third-party extensions, audit the source code of the extension before installing or updating the extension. Based on this, I believe there is very little assessment happening to these extensions so tread carefully.

You may think that a source code review will allow a consumer to validate what the extension is doing. Be aware, there is nothing stopping an adversary presenting benign code within the repository and push a different malicious version as the release. The documentation provides an example to push a release manually.

Threats

There are few threats to consider for the GitHub CLI. Firstly, it is highly likely that a developer is authenticated with the application. So if you manage to obtain local access to the developers device, it is trivial to dump their GitHub Token which is transferable and unless revoked, will last for a year.

GitHub CLI extensions can be used to run arbitrary code on the developers environment. The primary issue is tricking the developer into downloading and running the extension. This goes back to existing techniques such as social engineering or typo squatting a popular extension in the hope a developer will accidentally use it. It is possible to sideload an extension via the GitHub CLI so a developer could open a malicious file from a phishing email which would install and execute the extension. This could be an interesting place to hide a malicious executable but you cannot avoid any security tool looking at the process tree or seeing an outbound connection.

A more sophisticated extension could be made which steals the GitHub Token, encrypts it and sends it to GitHub. The nice part of this attack is that we already know that GitHub will be allow listed and it’s rare for outbound gateways to restrict based on specific repositories. I could create a code sample to demonstrate this but as the GitHub CLI provides the majority of the functionality to do this, it seems superfluous.

I hope you have found this article insightful in how an adversary can leverage the GitHub CLI to obtain code execution or steal long lived credentials to potential sensitive source code repositories. If you are a Red Teamer and find it useful or it inspires a campaign, feel free to reach out and let me know.

Next up in Part 3, I’ll be reviewing VSCode Extensions and how they can be abused by adversaries.