-
Notifications
You must be signed in to change notification settings - Fork 28
Google Workspace Enumeration
Enumerate a Google Workspace / Cloud Identity tenant (groups, users, admin roles, org units, domains, mobile devices, 3rd‑party OAuth grants) from GCPwn, and surface the GCP → Workspace takeover path (service‑account domain‑wide delegation) in OpenGraph.
Naming note: GCPwn's own workspace is your local session/credential container (see Workspace Instructions). This page is about Google Workspace, the SaaS tenant — a different thing that happens to share the word.
Google Workspace and GCP IAM are two independent privilege systems:
- A credential's GCP IAM role (e.g.
roles/viewerat the org) governs GCP resources — it has no bearing on Workspace. - Workspace access comes from either a Workspace admin user or a service account with domain‑wide delegation (DWD) impersonating an admin.
So a service account that is only a GCP viewer can, via DWD, act as a Workspace super‑admin — a takeover path that normal GCP IAM enumeration never shows. GCPwn models exactly this (see OpenGraph below).
Workspace is tenant‑scoped (by directoryCustomerId, e.g. C0xxxxxxx), not per‑project — so these modules run once, not per project.
| Path | Credential | Setup |
|---|---|---|
| Admin user | OAuth creds for a Workspace super‑admin user | None — works directly |
| Service account + DWD | A service account authorized for domain‑wide delegation | The steps below |
| A Workspace user's own OAuth | Any user's own token (incl. a Workspace‑only user with no GCP access) |
--token-file (load a user token) — see below |
The service‑account path is the common offensive scenario (you rarely hold an admin's live token). What a DWD service account can do is (the OAuth scopes authorized for it) × (the privileges of the user it impersonates). Read‑only scopes ⇒ read‑only; impersonate a super‑admin ⇒ full‑tenant reach.
Using a user's own token (no service account). Google has no
username/password API — you cannot POST an email+password and get a token
(ClientLogin/ROPC don't exist for Google identities). A built-in interactive
creds login browser flow is planned but not currently shipped; for now, mint an
authorized_user token out-of-band (e.g. gcloud auth application-default login in a
browser) and load it directly. This is the path for a Workspace‑only identity
(e.g. a super‑admin with no GCP roles):
creds add CRED_NAME --type oauth2 --token-file token.json # authorized-user json (refresh token, auto-renews)
creds add CRED_NAME --type oauth2 --token ya29.<...> # bare access token (expires ~1h, no refresh)
Whatever scopes the token consented to are what you get — request the Drive / Directory / Groups scopes when you mint it if you plan to run those modules.
You do this once, in the Google Workspace Admin console (only a Workspace super‑admin can). GCPwn cannot do this step for you.
-
Get the service account's OAuth client ID — the numeric
client_idin the SA key JSON ("client_id"field), or GCP Console → IAM & Admin → Service Accounts → the SA → Unique ID. -
Authorize that client ID at admin.google.com → Security → Access and data control → API controls → Domain‑wide delegation → Manage Domain Wide Delegation → Add new:
- Client ID: the numeric client ID from step 1
-
OAuth scopes (comma‑separated — the read‑only scopes GCPwn's modules request):
https://www.googleapis.com/auth/cloud-identity.groups.readonly, https://www.googleapis.com/auth/admin.directory.user.readonly, https://www.googleapis.com/auth/admin.directory.group.readonly, https://www.googleapis.com/auth/admin.directory.group.member.readonly, https://www.googleapis.com/auth/admin.directory.rolemanagement.readonly, https://www.googleapis.com/auth/admin.directory.domain.readonly, https://www.googleapis.com/auth/admin.directory.orgunit.readonly, https://www.googleapis.com/auth/admin.directory.device.mobile.readonly, https://www.googleapis.com/auth/admin.directory.user.security, https://www.googleapis.com/auth/apps.groups.settings, https://www.googleapis.com/auth/admin.datatransfer.readonly, https://www.googleapis.com/auth/drive.readonly
drive.readonlyis only needed for the Drive module (enum_drive, list + download) and also requires the Drive API enabled in step 3; omit it if you're not touching Drive. DWD is all‑or‑nothing on the token request: if GCPwn requests a scope you did not authorize here, that call fails. Authorize the whole list. Each directory sub‑API needs its own scope (e.g. domains ⇒admin.directory.domain.readonly), and GCPwn requests them per module. -
Enable the APIs in the service account's GCP project: Cloud Identity API and Admin SDK API.
-
Choose a Workspace admin to impersonate — the subject. DWD makes the SA act as this user, so for tenant‑wide directory listing it must be an admin (a super‑admin covers everything). Find one at admin.google.com → Directory → Users, filter by Admin role → Super Admin.
Load the service‑account key as a credential (creds add), then set the two Workspace settings:
configs set workspace_admin_subject admin@DOMAIN # the admin to impersonate (SA DWD)
configs set workspace_customer_id C0xxxxxxx # optional — GCPwn auto-resolves it from the org
-
workspace_admin_subject— the persistent DWD subject. Equivalent per‑run flag:--impersonate admin@DOMAIN(accepted by every Workspace module; overrides the config for that run). For a user credential this is a no‑op — an admin user already works directly. -
workspace_customer_id— thedirectoryCustomerId. GCPwn resolves it automatically from the GCP organization (organizations.get) when the SA hasresourcemanager.organizations.get; set it explicitly if it can't be derived.
Resolution order for the subject: --impersonate → configs set workspace_admin_subject → none.
Run everything once:
modules run enum_google_workspace
enum_google_workspace prints an up‑front status line (customer=..., impersonating=...) and, if no tenant resolves, one actionable pointer instead of a wall of 403s. Or run modules individually:
| Module | Surfaces | Attack‑path value |
|---|---|---|
enum_cloud_identity |
Groups, group memberships, users | Membership graph; who's in privileged groups |
enum_admin_roles |
Admin roles + who holds each (assignments) | Highest — finds super‑admins (full‑tenant control) |
enum_oauth_tokens |
Per‑user 3rd‑party OAuth app grants | Over‑privileged app grants (e.g. an app with Gmail/Drive scopes = data exfil) |
enum_group_settings |
Per‑group access/posting settings | Externally‑open / anyone‑can‑post / anyone‑can‑join groups |
enum_data_transfers |
Data‑ownership transfer requests | Offboarding data movement (who inherited a departed user's data) |
enum_org_units |
Organizational units | Tenant structure / policy scoping |
enum_domains |
Verified domains | Tenant footprint |
enum_mobile_devices |
Enrolled mobile devices | Device inventory |
Useful flags:
modules run enum_cloud_identity --groups --memberships --users # pick components
modules run enum_cloud_identity --query "…" --transitive # groups.search + nested memberships
modules run enum_admin_roles # roles + assignments (run --users first for readable emails)
modules run enum_oauth_tokens --user-key user@DOMAIN # one user (default: all cached workspace_users)
modules run enum_google_workspace --impersonate admin@DOMAIN # per-run subject
Ordering note: enum_oauth_tokens scans the cached workspace_users table — run enum_cloud_identity --users first (or pass --user-key). enum_admin_roles resolves assignee IDs to emails from workspace_users, so --users first makes "who holds super‑admin" readable.
Results land in the workspace‑scoped tables (workspace_groups, workspace_users, workspace_admin_roles, workspace_role_assignments, workspace_domains, workspace_org_units, workspace_mobile_devices, workspace_oauth_tokens) — query/export them via the data commands.
Drive is a per‑user data plane — you read one user's Drive at a time. With a
service account that means DWD impersonating the target user directly
(--caller-email is the DWD subject, not an admin subject); with a user's own
OAuth credential you read that user's own Drive. Needs the drive.readonly scope
(above) and the Drive API enabled. enum_drive is deliberately not part of
enum_google_workspace (it's per‑user and can be large) — run it explicitly.
1) Enumerate files + sharing (enum_drive) — records file metadata and an
exposure label (public / anyone_with_link / external / domain / private)
to workspace_drive_files, and every ACL entry to workspace_drive_permissions:
modules run enum_drive --caller-email user@DOMAIN # one user's Drive
modules run enum_drive --all-users # every cached workspace_users Drive (run enum_cloud_identity --users first)
modules run enum_drive --caller-email user@DOMAIN --query "mimeType!='application/vnd.google-apps.folder'" --max-files 500
2) Download content (enum_drive --download) — a modern, logged take on
GD‑Thief, folded into enum_drive. The same run lists the target Drive(s) and then
downloads from what it cached, so you target by exposure. Two switches:
-
--download-public— download only public / anyone‑with‑link files (readable by any identity, so they pull without special access). -
--download— download every listed candidate file.
Three download tiers (with --download):
| Tier | Flag | Behavior |
|---|---|---|
| Everything | (none) | download every candidate file |
| Focused | --focused |
only interesting extensions/types by NAME (docs, configs, keys, archives, …) |
| Secret‑scan | --secret-regex [PATTERN…] |
download, scan CONTENT, keep only files that hit a secret pattern — no value = built‑in set (AWS/GCP/Slack/GitHub keys, private keys, password= …); values add custom patterns |
Candidate filters: --only-external, --exposure <level>, --owner <email>, --limit <n>.
modules run enum_drive --caller-email user@DOMAIN --download-public # only anyone/anyone-with-link files
modules run enum_drive --all-users --download --focused --limit 50
modules run enum_drive --caller-email user@DOMAIN --download --secret-regex # keep only files containing a secret
modules run enum_drive --caller-email user@DOMAIN --download --secret-regex "AcmeCorp-[A-Z0-9]{20}" # + a custom pattern
Download as a different user. Google has no raw email/password auth, so an
"email/password user" is always a stored OAuth token (creds add --type oauth2 --token-file). By
default files are pulled as the identity you listed under (your current user, or
the --caller-email user via DWD). To use another identity:
modules run enum_drive --all-users --download --downloader-cred alice_login # download AS a stored credential
modules run enum_drive --all-users --download --downloader-creds-all # try EVERY stored credential per file
--downloader-cred <credname> downloads as one stored credential;
--downloader-creds-all tries every stored credential per file until one can read it
(and the [DOWNLOAD] line reports which one worked). Need a raw token instead? Store
it first — creds add <name> --type oauth2 --token ya29.<...> — then reference it.
Blue‑team note. Every download emits a greppable [DOWNLOAD] downloader=… file=… id=… exposure=… bytes=… -> <path> line, and --throttle <seconds> paces requests so
the Drive audit‑log pattern (a burst of drive.file.download / view events from
one actor) is easy to spot. Files land under the workspace downloads/drive/…
output directory. Use this in a lab to generate and study those detections.
When a service account successfully impersonates an admin, GCPwn records that it holds domain‑wide delegation to the tenant (the workspace_delegations table). OpenGraph then emits:
- a
GoogleWorkspaceTenantnode (workspace_tenant:C0xxxxxxx), -
DOMAIN_WIDE_DELEGedges from the service‑account node to every user it can impersonate, -
DELEGATES_INTO(SA → tenant) andWORKSPACE_MEMBER(user → tenant) edges,
on top of the existing workspace principal edges (GOOGLE_MEMBER_OF, and CAN_IMPERSONATE/CAN_RESET_PASSWORD from super‑admin role assignments). Build it with modules run process_og_gcpwn_data after enumerating.
Two more Workspace exposure paths land in the same graph:
-
Self‑join‑open groups (
enum_group_settings) → aCAN_JOINedge into each open group fromGCPAllUsers(anyone‑can‑join) or a per‑tenantPrincipalsInOrgnode (all‑in‑domain‑can‑join) — a principal who can self‑join inherits everything the group holds. -
Public / anyone‑with‑link Drive files (
enum_drive) → aGoogleDriveFilenode with aCAN_READedge fromGCPAllUsers(anonymous), tying Drive data exposure into the same graph as public buckets.
This makes the takeover chain a first‑class path in BloodHound: service account (maybe only a GCP viewer) → DOMAIN_WIDE_DELEG → super‑admin user → GCP resources — a pivot that never appears in GCP IAM alone. These additions are purely additive and only appear once a delegation is proven (graphs without Workspace data are unchanged).
-
Every module 403s / empty tables — the SA isn't authorized for DWD (or the wrong subject). Re‑check step 2 (client ID + scopes) and that the subject is a real admin.
enum_google_workspaceprints a prerequisites pointer when no tenant resolves. -
unauthorized_clienton the token request — a requested scope isn't authorized for the client ID; authorize the full scope list in step 2. -
Some modules work, others 403 — you authorized a partial scope set; each directory sub‑API needs its own
admin.directory.*.readonlyscope. -
No directoryCustomerId— set it explicitly:configs set workspace_customer_id C0xxxxxxx. -
Admin‑role assignees show numeric IDs — run
enum_cloud_identity --usersfirst so IDs resolve to emails (group/SA assignees stay numeric). -
OAuth tokens denied for every user —
admin.directory.user.securityis a separately elevated scope; authorize it for the DWD client (GCPwn stops after the first denial instead of spamming).
See also: Authentication Reference · CLI Module Reference · OpenGraph Overview
- Authentication Reference
- Workspace Instructions
- Google Workspace Enumeration
- CLI Module Reference
- Enumeration Module Reference
- Exploit Module Reference
- Downloads to Disk
- Logging & Verbosity
- Data View/Export
- IAM Enumeration and Analysis Workflow
- Troubleshooting and FAQ