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
4 changes: 4 additions & 0 deletions src/SUMMARY.md
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,7 @@
- [GCP - Dataflow Privesc](pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-dataflow-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 - Firebase Privesc](pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-firebase-privesc.md)
- [GCP - Orgpolicy Privesc](pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-orgpolicy-privesc.md)
Expand Down Expand Up @@ -210,7 +211,10 @@
- [GCP - Vertex AI Enum](pentesting-cloud/gcp-security/gcp-services/gcp-vertex-ai-enum.md)
- [GCP - Workflows Enum](pentesting-cloud/gcp-security/gcp-services/gcp-workflows-enum.md)
- [GCP <--> Workspace Pivoting](pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/README.md)
- [GCP - Agent Identity Auth Manager Credential Access](pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/gcp-agent-identity-auth-manager-privesc.md)
- [GCP - Understanding Domain-Wide Delegation](pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/gcp-understanding-domain-wide-delegation.md)
- [GCP - Application Integration Credential Access](pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/gcp-application-integration-privesc.md)
- [GCP - Workspace Add-on Deployment Takeover](pentesting-cloud/gcp-security/gcp-to-workspace-pivoting/gcp-workspace-addons-privesc.md)
- [GCP - Unauthenticated Enum & Access](pentesting-cloud/gcp-security/gcp-unauthenticated-enum-and-access/README.md)
- [GCP - API Keys Unauthenticated Enum](pentesting-cloud/gcp-security/gcp-unauthenticated-enum-and-access/gcp-api-keys-unauthenticated-enum.md)
- [GCP - App Engine Unauthenticated Enum](pentesting-cloud/gcp-security/gcp-unauthenticated-enum-and-access/gcp-app-engine-unauthenticated-enum.md)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

{{#include ../../../banners/hacktricks-training.md}}

> [!CAUTION]
> 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).

## OIDC - Github Actions Abuse

### GCP
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,52 @@ gcloud app versions delete <VERSION_ID>
gcloud app services delete <SERVICE_NAME>
```

### Read secrets and deployment locations from version configuration

The `appengine.versions.get` and `appengine.versions.list` permissions independently expose the **full version configuration** when the Admin API request uses `view=FULL`. This can disclose literal `envVariables`, the runtime `serviceAccount`, and every deployment manifest entry with its Cloud Storage `sourceUrl`. Treat either permission as high impact when an application stores credentials in environment variables; the service-account and source locations are also useful for selecting later privilege-escalation or source-review paths.<sup>[[8]](#references)[[9]](#references)[[10]](#references)</sup>

> [!NOTE]
> A deployment `sourceUrl` identifies an object but does not bypass Cloud Storage authorization. Reading the object still requires `appengine.versions.getFileContents`, `storage.objects.get`, another applicable access path, or credentials obtained elsewhere.

<details><summary>Read a known version with <code>appengine.versions.get</code></summary>

This path does not require a list permission. Version and service names can come from application hostnames, logs, source configuration, error messages, or previous reconnaissance. `default` is the usual initial service name.

```bash
TOKEN="$(gcloud auth print-access-token)"
PROJECT_ID="<project-id>"
SERVICE_ID="<known-service-id>"
VERSION_ID="<known-version-id>"

curl -sS \
-H "Authorization: Bearer $TOKEN" \
"https://appengine.googleapis.com/v1/apps/$PROJECT_ID/services/$SERVICE_ID/versions/$VERSION_ID?view=FULL" \
| jq '{id, runtime, envVariables, serviceAccount, deployment: .deployment.files}'
```

</details>

<details><summary>Enumerate full configurations with <code>appengine.versions.list</code></summary>

The list method returns the same sensitive fields without requiring `appengine.versions.get`. Follow `nextPageToken` when the response is paginated.

```bash
TOKEN="$(gcloud auth print-access-token)"
PROJECT_ID="<project-id>"
SERVICE_ID="<known-service-id>"

curl -sS \
-H "Authorization: Bearer $TOKEN" \
"https://appengine.googleapis.com/v1/apps/$PROJECT_ID/services/$SERVICE_ID/versions?view=FULL&pageSize=200" \
| jq '.versions[] | {id, runtime, envVariables, serviceAccount, deployment: .deployment.files}'
```

</details>

If the identity cannot call either API but an App Engine workload is already compromised, inspect the process environment and locally deployed application files. That guest-side fallback needs no App Engine IAM enumeration permission and may reveal the same runtime configuration directly.

In an isolated live test, an identity holding only `appengine.versions.get` recovered a synthetic environment marker, service-account address, and three deployment object URLs through the known-version request while the list request returned HTTP 403. A separate identity holding only `appengine.versions.list` recovered the same fields through `view=FULL` while the direct get request returned HTTP 403. No application configuration was changed for the test.

### Read Source Code

App Engine's deployment metadata includes a manifest of files stored in Cloud Storage, and App Engine creates a temporary deployment bucket named **`staging.<proj-id>.appspot.com`**. Deployments may therefore leave source artifacts in that bucket, but **write access alone does not grant read access**. Use the App Engine Code Viewer permission `appengine.versions.getFileContents`, or Cloud Storage permissions that include object reads, to inspect retained source artifacts and search for **vulnerabilities** and **sensitive information**.<sup>[[1]](#references)[[7]](#references)[[8]](#references)</sup>
Expand All @@ -61,5 +107,7 @@ Modify source code to steal credentials if they are being sent or perform a defa
- [6] [gcloud app services delete](https://cloud.google.com/sdk/gcloud/reference/app/services/delete)
- [7] [Use Cloud Storage](https://cloud.google.com/appengine/docs/standard/using-cloud-storage)
- [8] [Package google.appengine.v1](https://cloud.google.com/appengine/docs/admin-api/reference/rpc/google.appengine.v1)
- [9] [App Engine Admin API: apps.services.versions.get](https://cloud.google.com/appengine/docs/admin-api/reference/rest/v1/apps.services.versions/get)
- [10] [App Engine Admin API: apps.services.versions.list](https://cloud.google.com/appengine/docs/admin-api/reference/rest/v1/apps.services.versions/list)

{{#include ../../../banners/hacktricks-training.md}}
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,91 @@ Mount the disk inside the VM:

If you **cannot give access to an external project** to the snapshot or disk, you might need to p**erform these actions inside an instance in the same project as the snapshot/disk**; the VM and disk must also be in compatible locations for attachment.<sup>[[7]](#references)</sup>

### Read literal configuration from Compute metadata

Several ordinary-looking read permissions return complete custom metadata values. Metadata is free-form and is commonly used for startup and shutdown scripts, environment configuration, bootstrap commands, package-repository credentials, and other deployment inputs. If an operator stored a password, token, private URL, or other secret directly in metadata, these permissions disclose it without SSH or OS Login.<sup>[[11]](#references)[[12]](#references)</sup>

| Permission | Scope and response field |
| --- | --- |
| `compute.projects.get` | Project-wide `commonInstanceMetadata.items[]` |
| `compute.instanceSettings.get` | Zonal `metadata.items` inherited by VMs in that zone |
| `compute.instances.get` | Metadata of one known VM in `metadata.items[]` |
| `compute.instances.list` | Full VM objects, including metadata, for a zone or every zone through `aggregatedList` |
| `compute.instanceTemplates.get` | Metadata in one known global or regional template's `properties.metadata.items[]` |
| `compute.instanceTemplates.list` | Full global, regional, or aggregated template objects, including metadata |
| `compute.machineImages.get` | Captured source-VM metadata in one known machine image's `instanceProperties.metadata.items[]` |
| `compute.machineImages.list` | Full machine-image objects, including captured metadata, across the project |

The `get` permissions do not require their corresponding `list` permissions when the resource name and location are already known. Obtain names from source code, Terraform files or state, deployment scripts, logs, monitoring labels, CI output, shell history, or application configuration. The list permissions are independently useful because their responses contain the full resource objects rather than only names.<sup>[[13]](#references)[[14]](#references)[[15]](#references)[[16]](#references)[[17]](#references)[[18]](#references)[[19]](#references)[[20]](#references)</sup>

<details>

<summary>CLI enumeration and direct REST fallbacks</summary>

```bash
PROJECT_ID="project-id"
ZONE="us-central1-a"
INSTANCE="known-instance"
TEMPLATE="known-template"
ACCESS_TOKEN="$(gcloud auth print-access-token)"

# Project and zonal metadata.
gcloud compute project-info describe --project "$PROJECT_ID"
gcloud compute project-zonal-metadata describe \
--project "$PROJECT_ID" --zone "$ZONE"

curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://compute.googleapis.com/compute/v1/projects/$PROJECT_ID"
curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://compute.googleapis.com/compute/v1/projects/$PROJECT_ID/zones/$ZONE/instanceSettings"

# A known VM needs get but not list.
gcloud compute instances describe "$INSTANCE" \
--project "$PROJECT_ID" --zone "$ZONE"
curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://compute.googleapis.com/compute/v1/projects/$PROJECT_ID/zones/$ZONE/instances/$INSTANCE"

# Listing returns metadata too. Follow nextPageToken when present.
curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://compute.googleapis.com/compute/v1/projects/$PROJECT_ID/zones/$ZONE/instances?maxResults=500"
curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://compute.googleapis.com/compute/v1/projects/$PROJECT_ID/aggregated/instances?maxResults=500"

# Global template get/list and the all-regions aggregated fallback.
gcloud compute instance-templates describe "$TEMPLATE" --project "$PROJECT_ID"
gcloud compute instance-templates list --project "$PROJECT_ID"
curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://compute.googleapis.com/compute/v1/projects/$PROJECT_ID/global/instanceTemplates/$TEMPLATE"
curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://compute.googleapis.com/compute/v1/projects/$PROJECT_ID/global/instanceTemplates?maxResults=500"
curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://compute.googleapis.com/compute/v1/projects/$PROJECT_ID/aggregated/instanceTemplates?maxResults=500"

# Machine images preserve the source VM's metadata.
MACHINE_IMAGE="known-machine-image"
curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://compute.googleapis.com/compute/v1/projects/$PROJECT_ID/global/machineImages/$MACHINE_IMAGE"
curl -sS -H "Authorization: Bearer $ACCESS_TOKEN" \
"https://compute.googleapis.com/compute/v1/projects/$PROJECT_ID/global/machineImages?maxResults=500"
```

</details>

If the compromised process is already running inside a Compute Engine guest, the metadata server is the no-IAM fallback. It does not require a Google access token; the required `Metadata-Flavor` header prevents accidental requests. Query both scopes because instance values and inherited project or zonal values can differ:<sup>[[11]](#references)[[12]](#references)</sup>

```bash
curl -sS -H 'Metadata-Flavor: Google' \
'http://metadata.google.internal/computeMetadata/v1/project/attributes/?recursive=true&alt=text'
curl -sS -H 'Metadata-Flavor: Google' \
'http://metadata.google.internal/computeMetadata/v1/instance/attributes/?recursive=true&alt=text'
```

{% hint style="info" %}
**Live validation (2026-09-08):** Eight isolated custom roles were tested against synthetic project, zonal, VM, instance-template, and machine-image metadata. Each role contained exactly one permission from the table. `compute.projects.get`, `compute.instanceSettings.get`, `compute.instances.get`, `compute.instanceTemplates.get`, and `compute.machineImages.get` returned the exact marker at their respective known-resource endpoints without any list permission. Separate `compute.instances.list`, `compute.instanceTemplates.list`, and `compute.machineImages.list` identities recovered it from list responses while their matching GET requests returned HTTP 403; the VM and template list roles also succeeded through their aggregated-list APIs. The VMs had no service accounts and all values were synthetic. Both VMs and boot disks, the template, machine image, metadata entries, identities, bindings, and roles were removed afterward.
{% endhint %}

Treat these permissions as **High only when metadata can contain sensitive material**. Empty metadata, ordinary feature flags, public startup code, resource names, and SSH public keys are not secret disclosure. As a separate negative control, `instanceSettings.patch` rejected a caller holding only `compute.instanceSettings.get` plus `compute.instanceSettings.update` and demanded `iam.serviceAccounts.actAs`; do not present `compute.instanceSettings.update` alone as a confirmed privilege-escalation path.

### Read serial-console output - `compute.instances.getSerialPortOutput`

Serial output can contain startup-script output, cloud-init diagnostics, boot-time configuration, service errors, tokens, or credentials accidentally printed by workloads. This permission reads the buffered output without requiring SSH, OS Login, or `compute.instances.get` when the project, zone, and instance name are already known.<sup>[[10]](#references)</sup>
Expand Down Expand Up @@ -216,5 +301,15 @@ This was validated using a custom role containing only `compute.instances.getSer
- [8] [Format and mount a non-boot disk on a Linux VM](https://cloud.google.com/compute/docs/disks/format-mount-disk-linux)
- [9] [gcloud compute ssh](https://cloud.google.com/sdk/gcloud/reference/compute/ssh)
- [10] [Method: instances.getSerialPortOutput](https://docs.cloud.google.com/compute/docs/reference/rest/v1/instances/getSerialPortOutput)
- [11] [About VM metadata](https://docs.cloud.google.com/compute/docs/metadata/overview)
- [12] [View and query VM metadata](https://docs.cloud.google.com/compute/docs/metadata/querying-metadata)
- [13] [Method: projects.get](https://docs.cloud.google.com/compute/docs/reference/rest/v1/projects/get)
- [14] [Method: instanceSettings.get](https://docs.cloud.google.com/compute/docs/reference/rest/v1/instanceSettings/get)
- [15] [Method: instances.get](https://docs.cloud.google.com/compute/docs/reference/rest/v1/instances/get)
- [16] [Method: instances.list](https://docs.cloud.google.com/compute/docs/reference/rest/v1/instances/list)
- [17] [Method: instanceTemplates.get](https://docs.cloud.google.com/compute/docs/reference/rest/v1/instanceTemplates/get)
- [18] [Method: instanceTemplates.list](https://docs.cloud.google.com/compute/docs/reference/rest/v1/instanceTemplates/list)
- [19] [Method: machineImages.get](https://docs.cloud.google.com/compute/docs/reference/rest/v1/machineImages/get)
- [20] [Method: machineImages.list](https://docs.cloud.google.com/compute/docs/reference/rest/v1/machineImages/list)

{{#include ../../../banners/hacktricks-training.md}}
Loading