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 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
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.
auth
cmd option is probably a package and we can see this in login.go.github.com/cli/cli/v2/internal/config
.authCfg
calling the config package.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.
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.
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.
GitHub CLI extensions can be viewed by the following command:
$ gh extensions browse
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:
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:
gh
and be followed by the extension name$ 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.
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
.
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.
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.