diff --git a/docs/attack-techniques/GCP/gcp.execution.os-config-run-command.md b/docs/attack-techniques/GCP/gcp.execution.os-config-run-command.md
new file mode 100755
index 000000000..c49874c90
--- /dev/null
+++ b/docs/attack-techniques/GCP/gcp.execution.os-config-run-command.md
@@ -0,0 +1,84 @@
+---
+title: Execute Commands on GCE Instances via OS Config Agent
+---
+
+# Execute Commands on GCE Instances via OS Config Agent
+
+ slow
+
+
+Platform: GCP
+
+## Mappings
+
+- MITRE ATT&CK
+ - Execution
+
+
+
+## Description
+
+
+Executes an arbitrary shell command on GCE instances by creating an OS Config
+OSPolicyAssignment. The OS Config agent, which is pre-installed and
+enabled on modern GCP images, polls for policy assignments and executes the
+configured commands with root privileges. An attacker with
+osconfig.osPolicyAssignments.create permission can abuse this
+mechanism to achieve code execution on any instance in the project without
+needing SSH access.
+
+This is the GCP equivalent of AWS Systems Manager SendCommand.
+
+Warm-up:
+
+- Create a GCE instance (e2-micro, Debian 11) with the OS Config agent
+ enabled via instance metadata (enable-osconfig=TRUE)
+
+Detonation:
+
+- Create an OSPolicyAssignment targeting instances labelled
+ stratus-red-team=true that runs a shell command writing system
+ information to /tmp/stratus-output.txt
+
+Note: For commands to actually execute on targeted instances, OS Configuration
+Management must be enabled at the project level
+(see Enable VM Manager).
+If it is not enabled, GCP rejects the API call with a
+failedPrecondition error. From a defensive standpoint, this still
+generates the CreateOSPolicyAssignment audit event when Data Access
+logging is enabled, so the attempt remains detectable even if execution fails.
+
+Revert:
+
+- Delete the OSPolicyAssignment
+
+References:
+
+- https://cloud.google.com/compute/docs/os-configuration-management
+- https://cloud.google.com/compute/docs/osconfig/rest/v1/projects.locations.osPolicyAssignments
+- https://blog.raphael.karger.is/articles/2022-08/GCP-OS-Patching
+
+
+## Instructions
+
+```bash title="Detonate with Stratus Red Team"
+stratus detonate gcp.execution.os-config-run-command
+```
+## Detection
+
+
+Note: GCP does not emit Admin Activity audit logs for the OS Config API
+(osconfig.googleapis.com). CreateOSPolicyAssignment events
+are only logged if Data Access audit logging is explicitly enabled for
+osconfig.googleapis.com with log type DATA_WRITE, which is
+not enabled by default.
+
+When Data Access logging is enabled, identify when an OSPolicyAssignment
+is created or modified by monitoring for
+google.cloud.osconfig.v1.OsConfigZonalService.CreateOSPolicyAssignment
+and google.cloud.osconfig.v1.OsConfigZonalService.UpdateOSPolicyAssignment
+events. Alert on assignments whose policies include Exec resources with
+ENFORCEMENT mode, especially when the instance filter targets a broad set
+of instances.
+
+
diff --git a/docs/attack-techniques/GCP/index.md b/docs/attack-techniques/GCP/index.md
index bba57f11e..c0be097fb 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
+
+ - [Execute Commands on GCE Instances via OS Config Agent](./gcp.execution.os-config-run-command.md)
+
+
## Persistence
- [Register SSH public key to instance metadata](./gcp.lateral-movement.add-sshkey-instance-metadata.md)
diff --git a/docs/attack-techniques/list.md b/docs/attack-techniques/list.md
index 3f323b0f6..73d95c043 100755
--- a/docs/attack-techniques/list.md
+++ b/docs/attack-techniques/list.md
@@ -101,4 +101,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 |
+| [Execute Commands on GCE Instances via OS Config Agent](./GCP/gcp.execution.os-config-run-command.md) | [GCP](./GCP/index.md) | Execution |
| [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..5aa611476 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
OSPolicyAssignment. The OS Config agent, which is pre-installed and
+enabled on modern GCP images, polls for policy assignments and executes the
+configured commands with root privileges. An attacker with
+osconfig.osPolicyAssignments.create permission can abuse this
+mechanism to achieve code execution on any instance in the project without
+needing SSH access.
+
+This is the GCP equivalent of AWS Systems Manager SendCommand.
+
+Warm-up:
+
+- Create a GCE instance (e2-micro, Debian 11) with the OS Config agent
+ enabled via instance metadata (enable-osconfig=TRUE)
+
+Detonation:
+
+- Create an OSPolicyAssignment targeting instances labelled
+ stratus-red-team=true that runs a shell command writing system
+ information to /tmp/stratus-output.txt
+
+Note: For commands to actually execute on targeted instances, OS Configuration
+Management must be enabled at the project level
+(see Enable VM Manager).
+If it is not enabled, GCP rejects the API call with a
+failedPrecondition error. From a defensive standpoint, this still
+generates the CreateOSPolicyAssignment audit event when Data Access
+logging is enabled, so the attempt remains detectable even if execution fails.
+
+Revert:
+
+- Delete the OSPolicyAssignment
+
+References:
+
+- https://cloud.google.com/compute/docs/os-configuration-management
+- https://cloud.google.com/compute/docs/osconfig/rest/v1/projects.locations.osPolicyAssignments
+- https://blog.raphael.karger.is/articles/2022-08/GCP-OS-Patching
+`,
+ Detection: `
+Note: GCP does not emit Admin Activity audit logs for the OS Config API
+(osconfig.googleapis.com). CreateOSPolicyAssignment events
+are only logged if Data Access audit logging is explicitly enabled for
+osconfig.googleapis.com with log type DATA_WRITE, which is
+not enabled by default.
+
+When Data Access logging is enabled, identify when an OSPolicyAssignment
+is created or modified by monitoring for
+google.cloud.osconfig.v1.OsConfigZonalService.CreateOSPolicyAssignment
+and google.cloud.osconfig.v1.OsConfigZonalService.UpdateOSPolicyAssignment
+events. Alert on assignments whose policies include Exec resources with
+ENFORCEMENT mode, especially when the instance filter targets a broad set
+of instances.
+`,
+ Platform: stratus.GCP,
+ IsIdempotent: false,
+ IsSlow: true,
+ MitreAttackTactics: []mitreattack.Tactic{mitreattack.Execution},
+ PrerequisitesTerraformCode: tf,
+ Detonate: detonate,
+ Revert: revert,
+ })
+}
+
+func newOSConfigService(ctx context.Context, providers stratus.CloudProviders) (*osconfig.Service, error) {
+ svc, err := osconfig.NewService(ctx, providers.GCP().Options())
+ if err != nil {
+ return nil, fmt.Errorf("failed to create OS Config client: %w", err)
+ }
+ return svc, nil
+}
+
+func assignmentParent(projectId, zone string) string {
+ return fmt.Sprintf("projects/%s/locations/%s", projectId, zone)
+}
+
+func assignmentName(projectId, zone string) string {
+ return fmt.Sprintf("projects/%s/locations/%s/osPolicyAssignments/%s", projectId, zone, assignmentId)
+}
+
+// waitForOSConfigOperation polls an OS Config long-running operation until it completes.
+// OSPolicyAssignment rollouts can take up to 10 minutes depending on instance count.
+func waitForOSConfigOperation(ctx context.Context, svc *osconfig.Service, opName string) error {
+ const maxAttempts = 60
+ const pollInterval = 10 * time.Second
+
+ for attempt := range maxAttempts {
+ op, err := svc.Projects.Locations.OsPolicyAssignments.Operations.Get(opName).Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("failed to poll OS Config operation %s: %w", opName, err)
+ }
+ if op.Done {
+ if op.Error != nil {
+ return fmt.Errorf("OS Config operation %s failed: %s", opName, op.Error.Message)
+ }
+ return nil
+ }
+ log.Printf("Waiting for OS Config operation to complete (attempt %d/%d)\n", attempt+1, maxAttempts)
+ time.Sleep(pollInterval)
+ }
+ return fmt.Errorf("OS Config operation %s did not complete after %d attempts", opName, maxAttempts)
+}
+
+func detonate(params map[string]string, providers stratus.CloudProviders) error {
+ gcp := providers.GCP()
+ projectId := gcp.GetProjectId()
+ zone := params["zone"]
+ ctx := context.Background()
+
+ svc, err := newOSConfigService(ctx, providers)
+ if err != nil {
+ return err
+ }
+
+ parent := assignmentParent(projectId, zone)
+
+ log.Printf("Creating OSPolicyAssignment %s in %s to run a shell command on targeted instances\n",
+ assignmentId, parent)
+
+ _, err = svc.Projects.Locations.OsPolicyAssignments.Create(
+ parent,
+ &osconfig.OSPolicyAssignment{
+ // Target instances in the zone that carry the stratus-red-team label,
+ // as applied by the Terraform warmup.
+ InstanceFilter: &osconfig.OSPolicyAssignmentInstanceFilter{
+ InclusionLabels: []*osconfig.OSPolicyAssignmentLabelSet{
+ {
+ Labels: map[string]string{"stratus-red-team": "true"},
+ },
+ },
+ },
+ OsPolicies: []*osconfig.OSPolicy{
+ {
+ Id: "stratus-run-command",
+ Mode: "ENFORCEMENT",
+ ResourceGroups: []*osconfig.OSPolicyResourceGroup{
+ {
+ Resources: []*osconfig.OSPolicyResource{
+ {
+ Id: "run-command",
+ Exec: &osconfig.OSPolicyResourceExecResource{
+ // OS Config Exec resources use GCP-specific exit codes:
+ // 100 = already compliant (skip enforce), 101 = not
+ // compliant (run enforce). Exiting 101 ensures Enforce
+ // always fires regardless of prior state.
+ Validate: &osconfig.OSPolicyResourceExecResourceExec{
+ Interpreter: "SHELL",
+ Script: "exit 101",
+ },
+ // Enforce writes system information to a file on disk,
+ // producing observable evidence of remote execution.
+ // Must exit 100 to signal enforcement succeeded.
+ Enforce: &osconfig.OSPolicyResourceExecResourceExec{
+ Interpreter: "SHELL",
+ Script: "echo \"id=$(id) hostname=$(hostname)\" > /tmp/stratus-output.txt; exit 100",
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ Rollout: &osconfig.OSPolicyAssignmentRollout{
+ // Apply to all targeted instances at once.
+ DisruptionBudget: &osconfig.FixedOrPercent{Percent: 100},
+ MinWaitDuration: "0s",
+ },
+ },
+ ).OsPolicyAssignmentId(assignmentId).Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("failed to create OSPolicyAssignment: %w", err)
+ }
+
+ // The CreateOSPolicyAssignment LRO tracks rollout to instances, which requires
+ // instances to have internet access to phone home to the OS Config API. Rather
+ // than wait for rollout completion (which may time out in restricted environments),
+ // we verify the assignment was created by fetching it directly. The detection
+ // signal is the CreateOSPolicyAssignment audit event, which fires on creation.
+ name := assignmentName(projectId, zone)
+ assignment, err := svc.Projects.Locations.OsPolicyAssignments.Get(name).Context(ctx).Do()
+ if err != nil {
+ return fmt.Errorf("OSPolicyAssignment creation could not be confirmed: %w", err)
+ }
+ log.Printf("OSPolicyAssignment %s created (rollout state: %s) — OS Config agents on targeted instances will execute the command on next poll\n",
+ assignment.Name, assignment.RolloutState)
+ return nil
+}
+
+// waitForRolloutComplete polls the OSPolicyAssignment until its rollout state
+// is no longer IN_PROGRESS. GCP rejects Delete calls while a rollout is active.
+func waitForRolloutComplete(ctx context.Context, svc *osconfig.Service, name string) error {
+ const maxAttempts = 60
+ const pollInterval = 10 * time.Second
+
+ for attempt := range maxAttempts {
+ assignment, err := svc.Projects.Locations.OsPolicyAssignments.Get(name).Context(ctx).Do()
+ if err != nil {
+ if strings.Contains(err.Error(), "404") {
+ return nil
+ }
+ return fmt.Errorf("failed to get OSPolicyAssignment %s: %w", name, err)
+ }
+ if assignment.RolloutState != "IN_PROGRESS" {
+ return nil
+ }
+ log.Printf("Waiting for rollout to complete before deleting (state: %s, attempt %d/%d)\n",
+ assignment.RolloutState, attempt+1, maxAttempts)
+ time.Sleep(pollInterval)
+ }
+ return fmt.Errorf("rollout for %s did not complete after %d attempts", name, maxAttempts)
+}
+
+func revert(params map[string]string, providers stratus.CloudProviders) error {
+ gcp := providers.GCP()
+ projectId := gcp.GetProjectId()
+ zone := params["zone"]
+ ctx := context.Background()
+
+ svc, err := newOSConfigService(ctx, providers)
+ if err != nil {
+ return err
+ }
+
+ name := assignmentName(projectId, zone)
+
+ // Wait for any in-progress rollout to finish — GCP rejects Delete while IN_PROGRESS.
+ if err = waitForRolloutComplete(ctx, svc, name); err != nil {
+ return err
+ }
+
+ log.Printf("Deleting OSPolicyAssignment %s\n", name)
+ op, err := svc.Projects.Locations.OsPolicyAssignments.Delete(name).Context(ctx).Do()
+ if err != nil {
+ if strings.Contains(err.Error(), "404") {
+ log.Printf("OSPolicyAssignment %s not found — already deleted\n", name)
+ return nil
+ }
+ return fmt.Errorf("failed to delete OSPolicyAssignment %s: %w", name, err)
+ }
+
+ if err = waitForOSConfigOperation(ctx, svc, op.Name); err != nil {
+ return err
+ }
+
+ log.Printf("Successfully deleted OSPolicyAssignment %s\n", name)
+ return nil
+}
diff --git a/v2/internal/attacktechniques/gcp/execution/os-config-run-command/main.tf b/v2/internal/attacktechniques/gcp/execution/os-config-run-command/main.tf
new file mode 100644
index 000000000..03338cc04
--- /dev/null
+++ b/v2/internal/attacktechniques/gcp/execution/os-config-run-command/main.tf
@@ -0,0 +1,86 @@
+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-occr" # os config run command
+}
+
+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 - OS Config Run Command"
+}
+
+resource "google_compute_network" "vpc" {
+ name = "${local.resource_prefix}-vpc-${random_string.suffix.result}"
+ auto_create_subnetworks = false
+}
+
+# Private Google Access lets the instance reach Google APIs (including OS Config)
+# without a public IP, which is required for the OS Config agent to phone home.
+resource "google_compute_subnetwork" "subnet" {
+ name = "${local.resource_prefix}-subnet-${random_string.suffix.result}"
+ ip_cidr_range = "10.0.0.0/24"
+ region = "us-central1"
+ network = google_compute_network.vpc.self_link
+ private_ip_google_access = 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 {
+ subnetwork = google_compute_subnetwork.subnet.self_link
+ }
+
+ # enable-osconfig activates the OS Config agent on the instance.
+ # The OS Config agent is pre-installed on Debian 11 images but must be
+ # explicitly enabled via metadata for the OSPolicyAssignment API to work.
+ metadata = {
+ enable-osconfig = "TRUE"
+ }
+
+ service_account {
+ email = google_service_account.instance_sa.email
+ scopes = ["cloud-platform"]
+ }
+
+ labels = {
+ "stratus-red-team" = "true"
+ }
+}
+
+output "instance_name" {
+ value = google_compute_instance.instance.name
+}
+
+output "zone" {
+ value = google_compute_instance.instance.zone
+}
+
+output "display" {
+ value = format("GCE instance %s with OS Config agent enabled", google_compute_instance.instance.name)
+}
diff --git a/v2/internal/attacktechniques/main.go b/v2/internal/attacktechniques/main.go
index c93da6143..480baf0db 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/os-config-run-command"
_ "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"