Detection rules › Elastic

AWS EC2 Stop, Start, and User Data Modification Correlation

Status
production
Severity
high
Time window
20m
Group by
Esql.instance_id, Esql.time_bucket = DATE_TRUNC(5 minute, @timestamp), cloud.account.id, source.as.organization.name, source.geo.country_name, source.ip, user.name, user_agent.original
Author
Elastic
Source
github.com/elastic/detection-rules

Identifies a short sequence of EC2 management APIs against the same instance that is consistent with modifying instance user data and forcing it to run on the next boot: ModifyInstanceAttribute with user data, followed by stop and start. Adversaries may update userData and cycle instance state so malicious scripts execute as root on Linux or as the system context on Windows. This rule correlates successful StopInstances, StartInstances, and ModifyInstanceAttribute events that reference userData within a five-minute window, grouped by instance, user.name, account, source IP, and user agent. A hit requires exactly three distinct API names in that bucket.

MITRE ATT&CK coverage

Event coverage

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

Rule body elastic

[metadata]
creation_date = "2026/04/03"
integration = ["aws"]
maturity = "production"
updated_date = "2026/04/03"

[rule]
author = ["Elastic"]
description = """
Identifies a short sequence of EC2 management APIs against the same instance that is consistent with modifying instance
user data and forcing it to run on the next boot: `ModifyInstanceAttribute` with user data, followed by stop and start.
Adversaries may update `userData` and cycle instance state so malicious scripts execute as root on Linux or as the
system context on Windows. This rule correlates successful `StopInstances`, `StartInstances`, and
`ModifyInstanceAttribute` events that reference `userData` within a five-minute window, grouped by instance,
`user.name`, account, source IP, and user agent. A hit requires exactly three distinct API names in that bucket.
"""
false_positives = [
    """
    Legitimate automation or administrators may change user data and restart instances during maintenance, image
    baking, or configuration fixes. Review the caller identity, change tickets, and whether `user_agent.original` and
    `source.ip` match known tooling and networks (the rule groups on both together with `user.name`).
    """,
]
from = "now-20m"
interval = "5m"
language = "esql"
license = "Elastic License v2"
name = "AWS EC2 Stop, Start, and User Data Modification Correlation"
note = """## Triage and analysis

### Investigating AWS EC2 Stop, Start, and User Data Modification Correlation

This detection aggregates successful EC2 `StopInstances`, `StartInstances`, and `ModifyInstanceAttribute` (with
`userData` in request parameters) over **five-minute** windows. Rows are keyed by **instance ID** (`Esql.instance_id`
from the grok on `aws.cloudtrail.request_parameters`), **`user.name`**, **`cloud.account.id`**, **`user_agent.original`**,
and **`source.ip`**. The rule fires only when **`Esql.event_action_unique_count` is 3**, meaning all three API names
appear in the same bucket—consistent with changing user data and cycling the instance to run it.

The aggregated result does **not** include raw `request_parameters`; use the alert’s instance, account, user, IP, user
agent, and time bucket to query CloudTrail for the underlying events and payloads.

#### Possible investigation steps

- **Interpret the alert columns**: Review `Esql.event_action_values` to confirm the three actions are present (typically
  `ModifyInstanceAttribute`, `StopInstances`, `StartInstances`). Use `Esql.event_action_unique_count` to verify the
  rule logic (expect `3`).
- **Confirm the instance**: Use `Esql.instance_id` plus `cloud.account.id` in CMDB or AWS Resource Groups. Ensure the
  grok-derived ID matches the instance you expect (multi-instance API calls can affect extraction).
- **Identify the caller**: Tie `user.name` to an IAM user or role session name as shown in CloudTrail; for assumed roles,
  pivot in raw logs on `aws.cloudtrail.user_identity.arn` and session context in the same time window.
- **Validate client and origin**: Compare `user_agent.original` and `source.ip` to known admin workstations, bastions,
  or CI/CD egress. The rule intentionally groups by these fields so unrelated sessions do not merge into one bucket.
- **Recover user data context**: In CloudTrail (or the integration’s `aws.cloudtrail.request_parameters` on raw events),
  inspect the `ModifyInstanceAttribute` record for `userData` and whether values are base64 or placeholders.
- **Hunt for follow-on activity**: After the window, look for IAM changes, role assumption, or data access from the
  instance or the same principal.

### False positive analysis

- **Infrastructure as code**: Terraform, Ansible, and Pulumi user agents are excluded, but other automation may still
  match. Validate pipeline identity, change tickets, and whether stop/start is part of approved maintenance.
- **Break-glass or support workflows**: Some teams modify user data and restart instances during recovery; confirm with
  the workload owner.
- **Shared `user.name` or NAT**: If many callers share one identity or IP, bucketing may still separate sessions when IP
  or user agent differs; conversely, identical UA/IP across benign bulk operations can resemble this pattern—confirm
  intent.

### Response and remediation

- If unauthorized, isolate the instance, revoke or restrict the principal’s EC2 permissions, and rotate any credentials
  that may have been exposed in user data.
- Prefer Secrets Manager or Parameter Store over long-lived secrets in user data.

### Additional information

- [AWS EC2 User Data](https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/ec2-instance-metadata.html)
- [ModifyInstanceAttribute](https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifyInstanceAttribute.html)
- [Local EC2 privilege escalation through user data](https://hackingthe.cloud/aws/exploitation/local_ec2_priv_esc_through_user_data)
"""
references = [
    "https://docs.aws.amazon.com/AWSEC2/latest/APIReference/API_ModifyInstanceAttribute.html",
    "https://hackingthe.cloud/aws/exploitation/local_ec2_priv_esc_through_user_data",
]
risk_score = 73
rule_id = "7e5c0e5a-95a5-404e-a5b0-278d35dc3325"
severity = "high"
tags = [
    "Domain: Cloud",
    "Data Source: AWS",
    "Data Source: Amazon Web Services",
    "Data Source: AWS EC2",
    "Data Source: AWS CloudTrail",
    "Use Case: Threat Detection",
    "Tactic: Execution",
    "Resources: Investigation Guide",
]
timestamp_override = "event.ingested"
type = "esql"

query = '''
FROM logs-aws.cloudtrail-* 
| WHERE event.provider == "ec2.amazonaws.com" 
    and event.outcome == "success"
    and aws.cloudtrail.user_identity.type != "AWSService"
    and not (
      user_agent.original like "*Terraform*"
      or user_agent.original like "*Ansible*"
      or user_agent.original like "*Pulumi*"
    ) and not source.address in ("cloudformation.amazonaws.com", "servicecatalog.amazonaws.com")
    and
  (
   event.action in ("StopInstances", "StartInstances") or 
   (event.action == "ModifyInstanceAttribute" and aws.cloudtrail.request_parameters like "*userData=*")
   )
| grok aws.cloudtrail.request_parameters """instanceId=(?<Esql.instance_id>[^,}\]]+)"""
| STATS Esql.event_action_unique_count = COUNT_DISTINCT(event.action), 
        Esql.event_action_values = VALUES(event.action) by Esql.instance_id, user.name, cloud.account.id, Esql.time_bucket = DATE_TRUNC(5 minute, @timestamp) , user_agent.original, source.ip, source.as.organization.name, source.geo.country_name
| where Esql.event_action_unique_count == 3
| Keep Esql.*, user.name, cloud.account.id, user_agent.original, source.ip, source.as.organization.name, source.geo.country_name
'''


[[rule.threat]]
framework = "MITRE ATT&CK"

[[rule.threat.technique]]
id = "T1059"
name = "Command and Scripting Interpreter"
reference = "https://attack.mitre.org/techniques/T1059/"

[[rule.threat.technique.subtechnique]]
id = "T1059.009"
name = "Cloud API"
reference = "https://attack.mitre.org/techniques/T1059/009/"

[rule.threat.tactic]
id = "TA0002"
name = "Execution"
reference = "https://attack.mitre.org/tactics/TA0002/"

[[rule.threat]]
framework = "MITRE ATT&CK"

[[rule.threat.technique]]
id = "T1578"
name = "Modify Cloud Compute Infrastructure"
reference = "https://attack.mitre.org/techniques/T1578/"

[rule.threat.tactic]
id = "TA0005"
name = "Defense Evasion"
reference = "https://attack.mitre.org/tactics/TA0005/"

[rule.investigation_fields]
field_names = [
  "Esql.event_action_unique_count",
  "Esql.event_action_values",
  "Esql.instance_id",
  "user.name",
  "cloud.account.id",
  "user_agent.original",
  "source.ip",
  "Esql.time_bucket",
]

Stages and Predicates

Stage 1: from

FROM logs-aws.cloudtrail-*

Stage 2: where

| WHERE event.provider == "ec2.amazonaws.com" 
    and event.outcome == "success"
    and aws.cloudtrail.user_identity.type != "AWSService"
    and not (
      user_agent.original like "*Terraform*"
      or user_agent.original like "*Ansible*"
      or user_agent.original like "*Pulumi*"
    ) and not source.address in ("cloudformation.amazonaws.com", "servicecatalog.amazonaws.com")
    and
  (
   event.action in ("StopInstances", "StartInstances") or 
   (event.action == "ModifyInstanceAttribute" and aws.cloudtrail.request_parameters like "*userData=*")
   )

Stage 3: grok

| grok aws.cloudtrail.request_parameters """instanceId=(?<Esql.instance_id>[^,}\]]+)"""

Stage 4: stats

| STATS Esql.event_action_unique_count = COUNT_DISTINCT(event.action), 
        Esql.event_action_values = VALUES(event.action) by Esql.instance_id, user.name, cloud.account.id, Esql.time_bucket = DATE_TRUNC(5 minute, @timestamp) , user_agent.original, source.ip, source.as.organization.name, source.geo.country_name

Stage 5: where

| where Esql.event_action_unique_count == 3

Stage 6: keep

| Keep Esql.*, user.name, cloud.account.id, user_agent.original, source.ip, source.as.organization.name, source.geo.country_name

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

FieldKindExcluded values
user_agent.originalmatchAnsible
user_agent.originalmatchPulumi
user_agent.originalmatchTerraform
source.addressincloudformation.amazonaws.com, servicecatalog.amazonaws.com

Indicators

Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.

FieldKindValues
Esql.event_action_unique_counteq
  • 3
aws.cloudtrail.request_parameterswildcard
  • *userData=*
aws.cloudtrail.user_identity.typene
  • AWSService
event.actioneq
  • ModifyInstanceAttribute
event.actionin
  • StartInstances
  • StopInstances
event.outcomeeq
  • success
event.providereq
  • ec2.amazonaws.com

Output fields

Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.

FieldSource
Esql.*KEEP Esql.*
user.nameKEEP user.name
cloud.account.idKEEP cloud.account.id
user_agent.originalKEEP user_agent.original
source.ipKEEP source.ip
source.as.organization.nameKEEP source.as.organization.name
source.geo.country_nameKEEP source.geo.country_name