AnalysisType: rule
Filename: aws_ssm_distributed_command.py
RuleID: "AWS.SSM.DistributedCommand"
DisplayName: AWS SSM Distributed Command
Enabled: true
LogTypes:
- AWS.CloudTrail
Severity: Info
Reports:
MITRE ATT&CK:
- TA0002:T1203 # Execution: Exploitation for Client Execution
Stratus Red Team:
- aws.execution.ssm-send-command
Description: >
Detect an attacker utilizing AWS Systems Manager (SSM) to execute commands through SendCommand on multiple EC2 instances.
Reference: >
https://stratus-red-team.cloud/attack-techniques/AWS/aws.execution.ssm-send-command/
Runbook: >
Detetmine who issued the command, the command content and arguments, and which EC2 instances were affected. Determine the risk of an attacker creating a persistent point of access within one of the instances. Review behaviour logs for the EC2 instances (and their associated IAM roles).
SummaryAttributes:
- p_any_aws_account_ids
- p_any_aws_arns
- p_any_aws_instance_ids
- p_any_ip_addresses
- p_any_usernames
Tags:
- AWS CloudTrail
- AWS SSM
- AWS EC2
- 'Execution: Exploitation for Client Execution'
Status: Experimental
Tests:
- Name: Send Command to Many Instances at Once
ExpectedResult: true
Mocks:
- objectName: get_string_set
returnValue: '[]'
- objectName: put_string_set
returnValue: ''
Log:
{
"p_event_time": "2025-02-19 16:32:39.000000000",
"p_log_type": "AWS.CloudTrail",
"p_parse_time": "2025-02-19 16:35:54.509896629",
"awsRegion": "us-west-2",
"eventCategory": "Management",
"eventID": "ceaea85a-6db9-4595-9842-d904fce2f047",
"eventName": "SendCommand",
"eventSource": "ssm.amazonaws.com",
"eventTime": "2025-02-19 16:32:39.000000000",
"eventType": "AwsApiCall",
"eventVersion": "1.08",
"managementEvent": true,
"readOnly": false,
"recipientAccountId": "111122223333",
"requestID": "c41499e3-c04c-48e7-9da9-ef63d0868553",
"requestParameters": {
"documentName": "AWS-RunShellScript",
"instanceIds": [
"i-006e4c07b5fba8ad2",
"i-01bf673280e708b0d",
"i-020001cca1a2f2628"
],
"interactive": false,
"parameters": "HIDDEN_DUE_TO_SECURITY_REASONS"
},
"responseElements": {
"command": {
"alarmConfiguration": {
"alarms": [],
"ignorePollAlarmFailure": false
},
"clientName": "",
"clientSourceId": "",
"cloudWatchOutputConfig": {
"cloudWatchLogGroupName": "",
"cloudWatchOutputEnabled": false
},
"commandId": "f49a1fe5-d12b-4ac0-98bc-0e4bd83d70c0",
"comment": "",
"completedCount": 0,
"deliveryTimedOutCount": 0,
"documentName": "AWS-RunShellScript",
"documentVersion": "$DEFAULT",
"errorCount": 0,
"expiresAfter": "Feb 19, 2025, 6:32:39 PM",
"hasCancelCommandSignature": false,
"hasSendCommandSignature": false,
"instanceIds": [
"i-006e4c07b5fba8ad2",
"i-01bf673280e708b0d",
"i-020001cca1a2f2628"
],
"interactive": false,
"maxConcurrency": "50",
"maxErrors": "0",
"notificationConfig": {
"notificationArn": "",
"notificationEvents": [],
"notificationType": ""
},
"outputS3BucketName": "",
"outputS3KeyPrefix": "",
"outputS3Region": "us-west-2",
"parameters": "HIDDEN_DUE_TO_SECURITY_REASONS",
"requestedDateTime": "Feb 19, 2025, 4:32:39 PM",
"serviceRole": "",
"status": "Pending",
"statusDetails": "Pending",
"targetCount": 3,
"targets": [],
"timeoutSeconds": 3600,
"triggeredAlarms": []
}
},
"sourceIPAddress": "1.2.3.4",
"tlsDetails": {
"cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
"clientProvidedHostHeader": "ssm.us-west-2.amazonaws.com",
"tlsVersion": "TLSv1.2"
},
"userAgent": "sample-user-agent",
"userIdentity": {
"accessKeyId": "SAMPLE_ACCESS_KEY_ID",
"accountId": "111122223333",
"arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/bobson.dugnutt",
"principalId": "SAMPLE_PRINCIPAL_ID:bobson.dugnutt",
"sessionContext": {
"attributes": {
"creationDate": "2025-02-19T16:29:24Z",
"mfaAuthenticated": "false"
},
"sessionIssuer": {
"accountId": "111122223333",
"arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
"principalId": "SAMPLE_PRINCIPAL_ID",
"type": "Role",
"userName": "SampleRole"
},
"webIdFederationData": {}
},
"type": "AssumedRole"
}
}
- Name: Single Command Send, Many Cached IDs
ExpectedResult: true
Mocks:
- objectName: get_string_set
returnValue: '[1, 2, 3, 4, 5, 6, 7, 8, 9, 10]'
- objectName: put_string_set
returnValue: ''
Log:
{
"p_event_time": "2025-02-19 16:32:39.000000000",
"p_log_type": "AWS.CloudTrail",
"p_parse_time": "2025-02-19 16:35:54.509896629",
"awsRegion": "us-west-2",
"eventCategory": "Management",
"eventID": "ceaea85a-6db9-4595-9842-d904fce2f047",
"eventName": "SendCommand",
"eventSource": "ssm.amazonaws.com",
"eventTime": "2025-02-19 16:32:39.000000000",
"eventType": "AwsApiCall",
"eventVersion": "1.08",
"managementEvent": true,
"readOnly": false,
"recipientAccountId": "111122223333",
"requestID": "c41499e3-c04c-48e7-9da9-ef63d0868553",
"requestParameters": {
"documentName": "AWS-RunShellScript",
"instanceIds": [
"i-006e4c07b5fba8ad2"
],
"interactive": false,
"parameters": "HIDDEN_DUE_TO_SECURITY_REASONS"
},
"responseElements": {
"command": {
"alarmConfiguration": {
"alarms": [],
"ignorePollAlarmFailure": false
},
"clientName": "",
"clientSourceId": "",
"cloudWatchOutputConfig": {
"cloudWatchLogGroupName": "",
"cloudWatchOutputEnabled": false
},
"commandId": "f49a1fe5-d12b-4ac0-98bc-0e4bd83d70c0",
"comment": "",
"completedCount": 0,
"deliveryTimedOutCount": 0,
"documentName": "AWS-RunShellScript",
"documentVersion": "$DEFAULT",
"errorCount": 0,
"expiresAfter": "Feb 19, 2025, 6:32:39 PM",
"hasCancelCommandSignature": false,
"hasSendCommandSignature": false,
"instanceIds": [
"i-006e4c07b5fba8ad2"
],
"interactive": false,
"maxConcurrency": "50",
"maxErrors": "0",
"notificationConfig": {
"notificationArn": "",
"notificationEvents": [],
"notificationType": ""
},
"outputS3BucketName": "",
"outputS3KeyPrefix": "",
"outputS3Region": "us-west-2",
"parameters": "HIDDEN_DUE_TO_SECURITY_REASONS",
"requestedDateTime": "Feb 19, 2025, 4:32:39 PM",
"serviceRole": "",
"status": "Pending",
"statusDetails": "Pending",
"targetCount": 3,
"targets": [],
"timeoutSeconds": 3600,
"triggeredAlarms": []
}
},
"sourceIPAddress": "1.2.3.4",
"tlsDetails": {
"cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
"clientProvidedHostHeader": "ssm.us-west-2.amazonaws.com",
"tlsVersion": "TLSv1.2"
},
"userAgent": "sample-user-agent",
"userIdentity": {
"accessKeyId": "SAMPLE_ACCESS_KEY_ID",
"accountId": "111122223333",
"arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/bobson.dugnutt",
"principalId": "SAMPLE_PRINCIPAL_ID:bobson.dugnutt",
"sessionContext": {
"attributes": {
"creationDate": "2025-02-19T16:29:24Z",
"mfaAuthenticated": "false"
},
"sessionIssuer": {
"accountId": "111122223333",
"arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
"principalId": "SAMPLE_PRINCIPAL_ID",
"type": "Role",
"userName": "SampleRole"
},
"webIdFederationData": {}
},
"type": "AssumedRole"
}
}
- Name: Single Command Send, No Cached IDs
ExpectedResult: false
Mocks:
- objectName: get_string_set
returnValue: '[]'
- objectName: put_string_set
returnValue: ''
Log:
{
"p_event_time": "2025-02-19 16:32:39.000000000",
"p_log_type": "AWS.CloudTrail",
"p_parse_time": "2025-02-19 16:35:54.509896629",
"awsRegion": "us-west-2",
"eventCategory": "Management",
"eventID": "ceaea85a-6db9-4595-9842-d904fce2f047",
"eventName": "SendCommand",
"eventSource": "ssm.amazonaws.com",
"eventTime": "2025-02-19 16:32:39.000000000",
"eventType": "AwsApiCall",
"eventVersion": "1.08",
"managementEvent": true,
"readOnly": false,
"recipientAccountId": "111122223333",
"requestID": "c41499e3-c04c-48e7-9da9-ef63d0868553",
"requestParameters": {
"documentName": "AWS-RunShellScript",
"instanceIds": [
"i-006e4c07b5fba8ad2"
],
"interactive": false,
"parameters": "HIDDEN_DUE_TO_SECURITY_REASONS"
},
"responseElements": {
"command": {
"alarmConfiguration": {
"alarms": [],
"ignorePollAlarmFailure": false
},
"clientName": "",
"clientSourceId": "",
"cloudWatchOutputConfig": {
"cloudWatchLogGroupName": "",
"cloudWatchOutputEnabled": false
},
"commandId": "f49a1fe5-d12b-4ac0-98bc-0e4bd83d70c0",
"comment": "",
"completedCount": 0,
"deliveryTimedOutCount": 0,
"documentName": "AWS-RunShellScript",
"documentVersion": "$DEFAULT",
"errorCount": 0,
"expiresAfter": "Feb 19, 2025, 6:32:39 PM",
"hasCancelCommandSignature": false,
"hasSendCommandSignature": false,
"instanceIds": [
"i-006e4c07b5fba8ad2"
],
"interactive": false,
"maxConcurrency": "50",
"maxErrors": "0",
"notificationConfig": {
"notificationArn": "",
"notificationEvents": [],
"notificationType": ""
},
"outputS3BucketName": "",
"outputS3KeyPrefix": "",
"outputS3Region": "us-west-2",
"parameters": "HIDDEN_DUE_TO_SECURITY_REASONS",
"requestedDateTime": "Feb 19, 2025, 4:32:39 PM",
"serviceRole": "",
"status": "Pending",
"statusDetails": "Pending",
"targetCount": 3,
"targets": [],
"timeoutSeconds": 3600,
"triggeredAlarms": []
}
},
"sourceIPAddress": "1.2.3.4",
"tlsDetails": {
"cipherSuite": "ECDHE-RSA-AES128-GCM-SHA256",
"clientProvidedHostHeader": "ssm.us-west-2.amazonaws.com",
"tlsVersion": "TLSv1.2"
},
"userAgent": "sample-user-agent",
"userIdentity": {
"accessKeyId": "SAMPLE_ACCESS_KEY_ID",
"accountId": "111122223333",
"arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/bobson.dugnutt",
"principalId": "SAMPLE_PRINCIPAL_ID:bobson.dugnutt",
"sessionContext": {
"attributes": {
"creationDate": "2025-02-19T16:29:24Z",
"mfaAuthenticated": "false"
},
"sessionIssuer": {
"accountId": "111122223333",
"arn": "arn:aws:iam::111122223333:role/aws-reserved/sso.amazonaws.com/us-west-2/SampleRole",
"principalId": "SAMPLE_PRINCIPAL_ID",
"type": "Role",
"userName": "SampleRole"
},
"webIdFederationData": {}
},
"type": "AssumedRole"
}
}
- Name: Unrelated Event
ExpectedResult: false
Log:
{
"p_event_time": "2025-02-19 18:00:45.000000000",
"p_log_type": "AWS.CloudTrail",
"p_parse_time": "2025-02-19 18:05:54.395474849",
"awsRegion": "us-west-2",
"eventCategory": "Management",
"eventID": "447414b9-8516-4470-9e3a-f55067602ff0",
"eventName": "DescribeInstances",
"eventSource": "ec2.amazonaws.com",
"eventTime": "2025-02-19 18:00:45.000000000",
"eventType": "AwsApiCall",
"eventVersion": "1.10",
"managementEvent": true,
"readOnly": true,
"recipientAccountId": "111122223333",
"requestID": "c44720ce-1853-41c8-96b0-06dac0284963",
"requestParameters": {
"filterSet": {},
"instancesSet": {
"items": [
{
"instanceId": "i-09c2e8f99d245cc69"
}
]
}
},
"sourceIPAddress": "eks.amazonaws.com",
"userAgent": "eks.amazonaws.com",
"userIdentity": {
"accountId": "111122223333",
"arn": "arn:aws:sts::111122223333:assumed-role/SampleRole/SessionId",
"invokedBy": "eks.amazonaws.com",
"principalId": "SAMPLE_PRINCIPAL_ID:SessionId",
"sessionContext": {
"attributes": {
"creationDate": "2025-02-19T17:55:44Z",
"mfaAuthenticated": "false"
},
"sessionIssuer": {
"accountId": "111122223333",
"arn": "arn:aws:iam::111122223333:role/SampleRole",
"principalId": "SAMPLE_PRINCIPAL_ID",
"type": "Role",
"userName": "SampleRole"
}
},
"type": "AssumedRole"
}
}
# ------ paired body: aws_ssm_distributed_command.py ------
import datetime as dt
import json
from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
from panther_core import PantherEvent
from panther_detection_helpers.caching import get_string_set, put_string_set
# Determine how separate instances need be commanded in order to trigger an alert
INSTANCE_THRESHOLD = 2
all_instance_ids = set()
def rule(event: PantherEvent) -> bool:
# Exclude events of the wrong type
if event.get("eventName") != "SendCommand":
return False
# Determine if this actor accessed any other params in this account
key = get_cache_key(event)
cached_ids = get_cached_instance_ids(key)
target_instance_ids = set(event.deep_get("requestParameters", "instanceIds", default=[]))
# Determine if the cache needs updating with new entries
global all_instance_ids # pylint: disable=global-statement
all_instance_ids = cached_ids | target_instance_ids
if all_instance_ids - cached_ids:
# Only set the TTL if this is the first time we're adding to the cache
# Otherwise we'll be perpetually extending the lifespan of the cached data every time we
# add more.
put_string_set(key, all_instance_ids, epoch_seconds=(3600 if not cached_ids else None))
# Check combined number of params
return len(all_instance_ids) > INSTANCE_THRESHOLD
def title(event: PantherEvent) -> str:
actor = event.udm("actor_user")
account_name = event.get("recipientAccountId")
return f"Commands distributed to many EC2 instances by [{actor}] in [{account_name}]"
def severity(event: PantherEvent) -> str:
# Demote to LOW if attempt was denied
if not aws_cloudtrail_success(event):
return "LOW"
return "DEFAULT"
def alert_context(event: PantherEvent) -> dict:
global all_instance_ids
context = aws_rule_context(event)
context.update({"instanceIds": list(all_instance_ids)})
return context
def get_cache_key(event) -> str:
"""Use the field values in the event to generate a cache key unique to this actor and
account ID."""
offset = (
dt.datetime.fromisoformat(event.get("p_event_time", "1970-01-01T00:00:00")).timestamp()
// 3600
* 3600
)
actor = event.udm("actor_user")
account = event.get("recipientAccountId")
rule_id = "AWS.SSM.DistributedCommand"
return f"{rule_id}-{account}-{actor}-{offset}"
def get_cached_instance_ids(key: str) -> set[str]:
"""Get any previously cached parameter names. Included automatic converstion from string in
the case of a unit test mock."""
cached_ids = get_string_set(key, force_ttl_check=True)
if isinstance(cached_ids, str):
# This is a unit test
cached_ids = set(json.loads(cached_ids))
return cached_ids