CI/CD Pipeline Exploitation — Category Overview
Routing skill for attacks against build / deployment pipelines. Pipelines run attacker-influenced code (PRs, dependencies, build scripts) with privileged tokens, cloud OIDC, and write access to artifacts that downstream consumers trust. Compromise of a single pipeline often equals compromise of every release built by it.
Sub-Skills
| Sub-Skill |
Covers |
When to Load |
| poisoned-pipeline-execution |
Direct + Indirect PPE: injecting commands via attacker-controllable build files (Makefile, package.json scripts, build.gradle, Dangerfile, .pre-commit-config.yaml), pull_request_target abuse, fork-PR build triggers, dependency/test-script execution. |
Target builds untrusted PR code; you can land a malicious file or dependency. |
| github-actions-injection |
${{ }} template injection via untrusted context (issue/PR title, body, branch name, commit message) into run: steps, pull_request_target + PR-head checkout, GITHUB_TOKEN permission abuse, artifact/cache poisoning, action pinning (tag vs SHA). |
Target uses GitHub Actions and accepts external contributors. |
| self-hosted-runner-abuse |
Non-ephemeral runner persistence, fork-PR job execution on self-hosted, runner-label targeting, secret theft from runner env, lateral movement into internal network / cloud metadata. |
Target advertises self-hosted runners (workflow runs-on: label or public job logs). |
| cicd-secrets-exfil |
Extracting CI secrets / OIDC tokens: echo / printenv exfil, masking bypass (base64, char-split), OIDC -> cloud role assumption, GITHUB_TOKEN / CI_JOB_TOKEN scope abuse, cache / artifact secret leakage. |
You already have code execution in a CI job; need to convert it into durable cloud / registry access. |
Provider fingerprinting
# GitHub Actions
ls -la <REPO>/.github/workflows/ 2>/dev/null
gh api "repos/<OWNER>/<REPO>/actions/workflows" --jq '.workflows[] | {name,path,state}'
# GitLab CI
test -f <REPO>/.gitlab-ci.yml && echo "GitLab CI"
ls <REPO>/.gitlab/ci/ 2>/dev/null
# Jenkins
test -f <REPO>/Jenkinsfile && echo "Jenkins"; find <REPO> -name 'Jenkinsfile*' -type f
# CircleCI / Buildkite / Drone / Azure / Travis
for f in .circleci/config.yml .buildkite/pipeline.yml .drone.yml azure-pipelines.yml .travis.yml; do
test -f "<REPO>/$f" && echo "$f"
done
# Provider hints in README / badges
grep -RhoE 'github\.com/[^/]+/[^/]+/actions|gitlab\.com/[^/]+/[^/]+/-/pipelines|circleci\.com/gh/|app\.travis-ci\.com' <REPO> 2>/dev/null | sort -u
Workflow file recon (GitHub Actions example)
# Local checkout
find <REPO>/.github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) -print
# Remote, no clone needed
gh api "repos/<OWNER>/<REPO>/contents/.github/workflows" --jq '.[].name'
# Pull every workflow at HEAD
for wf in $(gh api "repos/<OWNER>/<REPO>/contents/.github/workflows" --jq '.[].path'); do
echo "=== $wf ==="
gh api "repos/<OWNER>/<REPO>/contents/$wf" --jq '.content' | base64 -d
done
Risky-surface triage
| Signal |
Why it matters |
Grep |
pull_request_target: trigger |
Runs with write GITHUB_TOKEN + secrets while checking out attacker code |
grep -rE '^\s*pull_request_target\s*:' .github/workflows/ |
actions/checkout with ref: ${{ github.event.pull_request.head.sha }} under pull_request_target |
Pulls attacker code with privileged context |
grep -rE 'pull_request\.head' .github/workflows/ |
${{ github.event.* }} interpolated into a run: block |
Expression injection sink (issue/PR body, title, branch name, commit msg) |
grep -rE '\$\{\{\s*github\.event\.(issue|pull_request|comment|head_commit)' .github/workflows/ |
runs-on: [self-hosted, ...] on public repo |
Self-hosted runner reachable from fork PRs |
grep -rE 'runs-on:\s*(\[\s*self-hosted|self-hosted)' .github/workflows/ |
Third-party action pinned by tag (@v3) instead of SHA |
Action repo / tag retargeting → silent supply-chain |
grep -rE 'uses:\s*[^/]+/[^@]+@v?[0-9]' .github/workflows/ |
permissions: block missing or write-all |
Token over-scoped; abuse to push to protected branches, publish releases |
`grep -rE 'permissions: |
Secret-exposure surface
# Secrets and OIDC references — every line is a potential exfil target once you have RCE in a job
grep -rnE 'secrets\.|vars\.|env\.[A-Z_]+_TOKEN|env\.[A-Z_]+_KEY|id-token:\s*write|configure-aws-credentials|google-github-actions/auth' .github/workflows/
# Exposed in public job logs (replays)
gh run list --repo <OWNER>/<REPO> --limit 20 --json databaseId,name,status,conclusion
gh run view <RUN_ID> --repo <OWNER>/<REPO> --log | grep -iE 'token|secret|password|aws_|gcp_|azure_' | head
Tooling
| Tool |
Use |
gh (GitHub CLI) |
Recon, workflow listing, run/log inspection, PR creation |
gato / gato-x (praetorian-inc) |
GitHub Actions attack toolkit — self-hosted runner enum, PPE, secret exfil chains |
actionlint |
Local lint that flags many injection sinks — defenders use it; you can read its rules to find sinks |
octoscan / zizmor |
Static scanners for GitHub Actions security issues |
tj-actions-changed-files CVE class (CVE-2025-30066) |
Known compromised-action precedent; pattern to look for in any third-party action |
Burp Collaborator / interactsh |
DNS / HTTP beacon for non-destructive PoC of code execution on a runner |
Decision gate
Before any active test, confirm in writing:
- The target program explicitly allows CI/CD testing (many programs scope this out — "infrastructure", "third-party CI" exclusions are common).
- PoC will use a beacon-only payload (DNS / HTTP callback). Do not exfiltrate real secrets to attacker-controlled storage; print the first 4 chars of a token to prove access, then stop.
- Any malicious branch / PR / fork is clearly labeled
security-research and is deletable on request.
- You will not push to protected branches, publish artifacts, or assume cloud roles. Prove the capability, do not exercise it.
Cross-references
- Supply-chain catalog:
/skills/standard/exploit/supplychain/SKILL.md
- OPSEC + scope discipline:
/skills/shared/opsec/SKILL.md
- Cloud OIDC follow-on: cloud / IAM enumeration skills after a successful OIDC token exchange
1---2name: cicd3description: CI/CD pipeline attack category — poisoned pipeline execution, GitHub Actions expression injection, self-hosted runner abuse, secrets/OIDC exfil. Routing skill: fingerprint the CI provider + workflow surface, then load the matching leaf.4---56# CI/CD Pipeline Exploitation — Category Overview78Routing skill for attacks against build / deployment pipelines. Pipelines run attacker-influenced code (PRs, dependencies, build scripts) with privileged tokens, cloud OIDC, and write access to artifacts that downstream consumers trust. Compromise of a single pipeline often equals compromise of every release built by it.910## Sub-Skills1112| Sub-Skill | Covers | When to Load |13|---|---|---|14| **poisoned-pipeline-execution** | Direct + Indirect PPE: injecting commands via attacker-controllable build files (`Makefile`, `package.json` scripts, `build.gradle`, `Dangerfile`, `.pre-commit-config.yaml`), `pull_request_target` abuse, fork-PR build triggers, dependency/test-script execution. | Target builds untrusted PR code; you can land a malicious file or dependency. | `load_skill("/skills/standard/exploit/cicd/poisoned-pipeline-execution/SKILL.md")` |15| **github-actions-injection** | `${{ }}` template injection via untrusted context (issue/PR title, body, branch name, commit message) into `run:` steps, `pull_request_target` + PR-head checkout, `GITHUB_TOKEN` permission abuse, artifact/cache poisoning, action pinning (tag vs SHA). | Target uses GitHub Actions and accepts external contributors. | `load_skill("/skills/standard/exploit/cicd/github-actions-injection/SKILL.md")` |16| **self-hosted-runner-abuse** | Non-ephemeral runner persistence, fork-PR job execution on self-hosted, runner-label targeting, secret theft from runner env, lateral movement into internal network / cloud metadata. | Target advertises self-hosted runners (workflow `runs-on:` label or public job logs). | `load_skill("/skills/standard/exploit/cicd/self-hosted-runner-abuse/SKILL.md")` |17| **cicd-secrets-exfil** | Extracting CI secrets / OIDC tokens: `echo` / `printenv` exfil, masking bypass (base64, char-split), OIDC -> cloud role assumption, `GITHUB_TOKEN` / `CI_JOB_TOKEN` scope abuse, cache / artifact secret leakage. | You already have code execution in a CI job; need to convert it into durable cloud / registry access. | `load_skill("/skills/standard/exploit/cicd/cicd-secrets-exfil/SKILL.md")` |1819## Provider fingerprinting2021```bash22# GitHub Actions23ls -la <REPO>/.github/workflows/ 2>/dev/null24gh api "repos/<OWNER>/<REPO>/actions/workflows" --jq '.workflows[] | {name,path,state}'2526# GitLab CI27test -f <REPO>/.gitlab-ci.yml && echo "GitLab CI"28ls <REPO>/.gitlab/ci/ 2>/dev/null2930# Jenkins31test -f <REPO>/Jenkinsfile && echo "Jenkins"; find <REPO> -name 'Jenkinsfile*' -type f3233# CircleCI / Buildkite / Drone / Azure / Travis34for f in .circleci/config.yml .buildkite/pipeline.yml .drone.yml azure-pipelines.yml .travis.yml; do35 test -f "<REPO>/$f" && echo "$f"36done3738# Provider hints in README / badges39grep -RhoE 'github\.com/[^/]+/[^/]+/actions|gitlab\.com/[^/]+/[^/]+/-/pipelines|circleci\.com/gh/|app\.travis-ci\.com' <REPO> 2>/dev/null | sort -u40```4142## Workflow file recon (GitHub Actions example)4344```bash45# Local checkout46find <REPO>/.github/workflows -type f \( -name '*.yml' -o -name '*.yaml' \) -print4748# Remote, no clone needed49gh api "repos/<OWNER>/<REPO>/contents/.github/workflows" --jq '.[].name'5051# Pull every workflow at HEAD52for wf in $(gh api "repos/<OWNER>/<REPO>/contents/.github/workflows" --jq '.[].path'); do53 echo "=== $wf ==="54 gh api "repos/<OWNER>/<REPO>/contents/$wf" --jq '.content' | base64 -d55done56```5758## Risky-surface triage5960| Signal | Why it matters | Grep |61|---|---|---|62| `pull_request_target:` trigger | Runs with write `GITHUB_TOKEN` + secrets while checking out attacker code | `grep -rE '^\s*pull_request_target\s*:' .github/workflows/` |63| `actions/checkout` with `ref: ${{ github.event.pull_request.head.sha }}` under `pull_request_target` | Pulls attacker code with privileged context | `grep -rE 'pull_request\.head' .github/workflows/` |64| `${{ github.event.* }}` interpolated into a `run:` block | Expression injection sink (issue/PR body, title, branch name, commit msg) | `grep -rE '\$\{\{\s*github\.event\.(issue\|pull_request\|comment\|head_commit)' .github/workflows/` |65| `runs-on: [self-hosted, ...]` on public repo | Self-hosted runner reachable from fork PRs | `grep -rE 'runs-on:\s*(\[\s*self-hosted\|self-hosted)' .github/workflows/` |66| Third-party action pinned by tag (`@v3`) instead of SHA | Action repo / tag retargeting → silent supply-chain | `grep -rE 'uses:\s*[^/]+/[^@]+@v?[0-9]' .github/workflows/` |67| `permissions:` block missing or `write-all` | Token over-scoped; abuse to push to protected branches, publish releases | `grep -rE 'permissions:|write-all' .github/workflows/` |6869## Secret-exposure surface7071```bash72# Secrets and OIDC references — every line is a potential exfil target once you have RCE in a job73grep -rnE 'secrets\.|vars\.|env\.[A-Z_]+_TOKEN|env\.[A-Z_]+_KEY|id-token:\s*write|configure-aws-credentials|google-github-actions/auth' .github/workflows/7475# Exposed in public job logs (replays)76gh run list --repo <OWNER>/<REPO> --limit 20 --json databaseId,name,status,conclusion77gh run view <RUN_ID> --repo <OWNER>/<REPO> --log | grep -iE 'token|secret|password|aws_|gcp_|azure_' | head78```7980## Tooling8182| Tool | Use |83|---|---|84| `gh` (GitHub CLI) | Recon, workflow listing, run/log inspection, PR creation |85| `gato` / `gato-x` (praetorian-inc) | GitHub Actions attack toolkit — self-hosted runner enum, PPE, secret exfil chains |86| `actionlint` | Local lint that flags many injection sinks — defenders use it; you can read its rules to find sinks |87| `octoscan` / `zizmor` | Static scanners for GitHub Actions security issues |88| `tj-actions-changed-files` CVE class (CVE-2025-30066) | Known compromised-action precedent; pattern to look for in any third-party action |89| Burp Collaborator / `interactsh` | DNS / HTTP beacon for non-destructive PoC of code execution on a runner |9091## Decision gate9293Before any active test, confirm in writing:94951. The target program explicitly allows CI/CD testing (many programs scope this out — "infrastructure", "third-party CI" exclusions are common).962. PoC will use a **beacon-only** payload (DNS / HTTP callback). Do not exfiltrate real secrets to attacker-controlled storage; print the first 4 chars of a token to prove access, then stop.973. Any malicious branch / PR / fork is clearly labeled `security-research` and is deletable on request.984. You will not push to protected branches, publish artifacts, or assume cloud roles. Prove the *capability*, do not exercise it.99100## Cross-references101102- Supply-chain catalog: `/skills/standard/exploit/supplychain/SKILL.md`103- OPSEC + scope discipline: `/skills/shared/opsec/SKILL.md`104- Cloud OIDC follow-on: cloud / IAM enumeration skills after a successful OIDC token exchange