Self-hosted runner takeover → persistent CI compromise
A public repo with self-hosted GitHub runners accepts external PRs. First malicious PR runs on the runner; the workflow drops a runner-hook that fires before every future job.
§ Context
Assumed environment: target uses self-hosted runners on its own infra and forgot to restrict PRs from forks (the warning at runner registration time is often ignored).
§ Steps
- 01Wait for legitimate jobsInitial AccessT1078— Valid Accounts
- 02Workflow runs on the runnerExecutionT1059— Command and Scripting Interpreter
- 03Find a public repo with self-hosted runnerReconnaissanceW-RECON-GITHUB-DORK— GitHub / GitLab Dorking
- 04Open PR with a workflowPrivilege EscalationCI-RUNNER-TAKEOVER— Self-Hosted Runner Takeover
- 05Drop ACTIONS_RUNNER_HOOK_JOB_STARTED scriptPersistenceCI-RUNNER-PERSIST— Runner Persistence (custom hook)
- 06Exfil all future job secretsCredential AccessCI-SECRET-IN-LOG— Secret Echo to Build Log
§ References
§ Frequently asked
- What is the "Self-hosted runner takeover → persistent CI compromise" attack path?
- A public repo with self-hosted GitHub runners accepts external PRs. First malicious PR runs on the runner; the workflow drops a runner-hook that fires before every future job. It chains 6 steps drawn from real-world offensive-security techniques.
- What starting position does this attack require?
- The first step is Wait for legitimate jobs (T1078) — a initial access primitive. Assumed environment: target uses self-hosted runners on its own infra and forgot to restrict PRs from forks (the warning at runner registration time is often ignored).
- What is the final impact of this kill-chain?
- The final step lands on Exfil all future job secrets (CI-SECRET-IN-LOG), which falls under Credential Access. From here, an operator typically pivots into post-exploitation or maintains persistence.
- How can defenders detect or prevent this attack?
- Detection and prevention vary per step. Refer to each linked MITRE ATT&CK entry under "References" — every technique on that page lists defensive controls, detection telemetry, and known threat-actor usage.
§ Related dossiers
- Shared techniques3
pull_request_target injection → secrets → cloud takeover
A GitHub Actions workflow runs on pull_request_target and checks out the PR's head SHA. The attacker's PR injects code that runs with the base repo's secrets, including a cloud deploy role.
- Shared techniques3
Secret echoed to public build log → cloud takeover
A workflow accidentally runs `env` or `set -x` during debugging — the AWS access key is now in public CI logs and indexed by Google Cache / GitHub search.
- Shared techniques2
Dev workstation → cloud backup keys → encrypted vault store (LastPass 2022)
Attacker compromised a single LastPass DevOps engineer's home machine via outdated Plex Media Server, harvested AWS keys for the encrypted-vault backup bucket, exfiltrated production vault data.
- Shared techniques2
nf_tables UAF → kernel R/W → root
CVE-2024-1086-class nf_tables UAF reachable from a user namespace. Win the race with userfaultfd to land an attacker object in the freed slot, build a kernel R/W primitive, overwrite the current task's cred struct.
- Shared techniques2
io_uring UAF → modprobe_path overwrite → root
Use an io_uring UAF to land arbitrary kernel write, repoint /proc/sys/kernel/modprobe to an attacker binary, then trigger a kernel auto-modprobe — runs the binary as root.
- Shared techniques2
Mass SMS phish → Okta-style portal → SaaS sprawl (0ktapus)
Wide SMS phishing campaign targeting employees of ~130 organisations with a single phishlet that captures Okta credentials + push approval. Mass automated logins to Twilio, MailChimp, DoorDash et al.