-
Notifications
You must be signed in to change notification settings - Fork 612
Description
Summary
AWS SSM is an instance manager that deploys an SSM agent to EC2 instances, allowing for remote management. SSM documents are runbooks that allow administrators to perform specific operations on an EC2 host. AWS-managed documents that exist by default, such as AWS-RunShellScript and AWS-RunPowerShellScript allow arbitrary code to be executed remotely on these EC2 instances. To do so, an identity with the right privileges can perform an EC2 API operation to request the AWS-RunShellScript with parameters; these parameters are the arbitrary code that is to be executed on the target instance.
While no in-the-wild publicly reported use of abusing SSM has been reported; it is well documented by red teamers, in open-source tools and PoCs.
- https://www.mitiga.io/blog/abusing-the-amazon-web-services-ssm-agent-as-a-remote-access-trojan
- https://www.kali.org/tools/pacu/
- https://www.100daysofredteam.com/p/ghost-in-the-cloud-abusing-aws-ssm
Background
Related Rules
Elastic has several rules related to the SendCommand API operation by AWS listed below. All of these rules rely on AWS CloudTrail data only.
- SSM SendCommand API Used by EC2 Instance
- AWS SSM
SendCommandwith Run Shell Command Parameters - AWS SSM Command Document Created by Rare User
- AWS SSM
SendCommandExecution by Rare User
The Problem
However, we are unable to query on suspicious commands due to AWS natively redacting (HIDDEN_DUE_TO_SECURITY_REASONS) what arbitrary commands are sent to the target instance. We also have a lower fidelity on signals where SendCommand is used as additional context is needed.
{
"interactive": "false",
"comment": "Compress, chunk, and publish .txt files from /tmp to SNS",
"documentName": "AWS-RunShellScript",
"parameters": "HIDDEN_DUE_TO_SECURITY_REASONS",
"instanceIds": "i-0ce963f7a0221448b"
}
The Solution
Thus, if we can correlate the SendCommand API operation to the correct EC2 instance and determine what commands were executed, such as common LOLbins and GTFObins, then we have very high fidelity of nefarious activity and can also identify what commands are being sent in telemetry.
Query Analysis and Detection Logic
This detection correlates AWS CloudTrail SendCommand events with Linux endpoint process execution to identify suspicious LOLBin/GTFOBin usage initiated via SSM.
Key Correlation: SSM command ID from CloudTrail response matches the command ID in the endpoint process parent command line.
ESQL Query:
FROM logs-*
| WHERE
// CloudTrail SSM SendCommand with AWS-RunShellScript
(
event.dataset == "aws.cloudtrail"
AND event.action == "SendCommand"
AND aws.cloudtrail.request_parameters LIKE "*documentName=AWS-RunShellScript*"
)
// Linux endpoint process events, prefiltered to SSM shell runner OR LOLBins/GTFOBins
OR
(
event.dataset == "endpoint.events.process"
AND host.os.type == "linux"
AND (
// SSM shell (_script.sh) runner
process.command_line LIKE "%/document/orchestration/%/awsrunShellScript/%/_script.sh"
// LOLBins / GTFOBins
OR process.executable IN (
"/usr/bin/base64",
"/usr/bin/curl",
"/usr/bin/wget",
"/usr/bin/openssl",
"/usr/bin/nc", "/bin/nc",
"/usr/bin/ncat", "/usr/bin/netcat",
"/usr/bin/socat",
"/usr/bin/python", "/usr/bin/python3",
"/usr/bin/perl",
"/usr/bin/php",
"/usr/bin/ruby",
"/usr/bin/ssh",
"/usr/bin/scp",
"/usr/bin/sftp",
"/usr/bin/rsync"
)
OR process.name IN (
"base64",
"curl",
"wget",
"openssl",
"nc", "ncat", "netcat",
"socat",
"python", "python3",
"perl",
"php",
"ruby",
"ssh",
"scp",
"sftp",
"rsync"
)
)
)
// Endpoint leg: extract SSM command ID from parent command line
// .../document/orchestration/<ssm_command_id>/AWS-RunShellScript/...
| DISSECT process.parent.command_line
"%{}/document/orchestration/%{Esql.process_parent_command_line_ssm_command_id}/%{}"
// CloudTrail leg: extract SSM command ID from response_elements
// ...commandId=<ssm_command_id>,...
| DISSECT aws.cloudtrail.response_elements
"%{}commandId=%{Esql.aws_cloudtrail_response_elements_ssm_command_id},%{}"
// Normalize into a single SSM command id
| EVAL Esql.aws_ssm_command_id =
CASE(
event.dataset == "aws.cloudtrail", Esql.aws_cloudtrail_response_elements_ssm_command_id,
CASE(
event.dataset == "endpoint.events.process", Esql.process_parent_command_line_ssm_command_id,
null
)
)
| WHERE Esql.aws_ssm_command_id IS NOT NULL
// Role flags
| EVAL Esql.is_cloud_event = event.dataset == "aws.cloudtrail"
| EVAL Esql.is_endpoint_event = event.dataset == "endpoint.events.process"
// Identify the SSM shell processes (the _script.sh runners)
| EVAL Esql.is_ssm_shell_process =
Esql.is_endpoint_event
AND process.command_line LIKE "%/document/orchestration/%/awsrunShellScript/%/_script.sh"
// LOLBins / GTFOBins on Linux
| EVAL Esql.is_lolbin_process =
Esql.is_endpoint_event AND NOT Esql.is_ssm_shell_process
// Aggregate per SSM command ID
| STATS
// Core correlation counts & timing
Esql.aws_cloudtrail_event_count = SUM(CASE(Esql.is_cloud_event, 1, 0)),
Esql.endpoint_events_process_lolbin_count = SUM(CASE(Esql.is_lolbin_process, 1, 0)),
Esql.endpoint_events_process_ssm_shell_count = SUM(CASE(Esql.is_ssm_shell_process, 1, 0)),
Esql.aws_cloudtrail_first_event_ts = MIN(CASE(Esql.is_cloud_event, @timestamp, null)),
Esql.endpoint_events_process_first_lolbin_ts = MIN(CASE(Esql.is_lolbin_process, @timestamp, null)),
// AWS / CloudTrail identity & request context (PII → Esql_priv.)
Esql_priv.aws_cloudtrail_user_identity_arn_values =
VALUES(CASE(Esql.is_cloud_event, aws.cloudtrail.user_identity.arn, null)),
Esql_priv.aws_cloudtrail_user_identity_access_key_id_values =
VALUES(CASE(Esql.is_cloud_event, aws.cloudtrail.user_identity.access_key_id, null)),
Esql_priv.user_name_values =
VALUES(CASE(Esql.is_cloud_event, user.name, null)),
// AWS environment / request metadata
Esql.cloud_region_values = VALUES(CASE(Esql.is_cloud_event, cloud.region, null)),
Esql.source_ip_values = VALUES(CASE(Esql.is_cloud_event, source.ip, null)),
Esql.user_agent_original_values =
VALUES(CASE(Esql.is_cloud_event, user_agent.original, null)),
// Endpoint host & user context
Esql.host_name_values = VALUES(CASE(Esql.is_endpoint_event, host.name, null)),
Esql_priv.endpoint_user_name_values =
VALUES(CASE(Esql.is_endpoint_event, user.name, null)),
// SSM shell processes on endpoint
Esql.process_command_line_ssm_shell_values =
VALUES(CASE(Esql.is_ssm_shell_process, process.command_line, null)),
Esql.process_pid_ssm_shell_values =
VALUES(CASE(Esql.is_ssm_shell_process, process.pid, null)),
// LOLBin processes on endpoint
Esql.process_name_lolbin_values =
VALUES(CASE(Esql.is_lolbin_process, process.name, null)),
Esql.process_executable_lolbin_values =
VALUES(CASE(Esql.is_lolbin_process, process.executable, null)),
Esql.process_command_line_lolbin_values =
VALUES(CASE(Esql.is_lolbin_process, process.command_line, null)),
Esql.process_pid_lolbin_values =
VALUES(CASE(Esql.is_lolbin_process, process.pid, null)),
Esql.process_parent_command_line_lolbin_values =
VALUES(CASE(Esql.is_lolbin_process, process.parent.command_line, null))
BY Esql.aws_ssm_command_id
// Detection condition: SSM SendCommand + AWS-RunShellScript + LOLBin on endpoint
| WHERE Esql.aws_cloudtrail_event_count > 0
AND Esql.endpoint_events_process_lolbin_count > 0
AND DATE_DIFF(
"minutes",
Esql.endpoint_events_process_first_lolbin_ts,
Esql.aws_cloudtrail_first_event_ts
) <= 5
| SORT Esql.aws_cloudtrail_first_event_ts ASCData Requirements
- AWS CloudTrail:
logs-aws.cloudtrail-*with SSM API events - Endpoint Telemetry:
logs-endpoint.events.process-*for Linux hosts with Elastic Agent deployed
Query Logic Breakdown
Collect Data from Two Sources with Pre-filtering
- Retrieve AWS CloudTrail `SendCommand` events for `AWS-RunShellScript`
- Retrieve Linux endpoint process events, but pre-filtered to only:
- SSM shell runner processes (`_script.sh`)
- LOLBin/GTFOBin executables (by path or name)
- Reduces data volume by filtering at collection time
Extract SSM Command ID from Endpoint
- Parse parent process command line: `.../document/orchestration/<ssm_command_id>/...`
- Extract command ID into `Esql.process_parent_command_line_ssm_command_id`
- SSM agent embeds this ID when spawning processes
Extract SSM Command ID from CloudTrail
- Parse CloudTrail response: `...commandId=<ssm_command_id>,...`
- Extract command ID into `Esql.aws_cloudtrail_response_elements_ssm_command_id`
- AWS returns this ID when the command is submitted
Normalize Command ID
- Create single `Esql.aws_ssm_command_id` field from both sources
- Filter out events without a valid command ID
- Enables correlation in aggregation step
Set Data Source Flags
- `Esql.is_cloud_event`: Identifies CloudTrail events
- `Esql.is_endpoint_event`: Identifies endpoint events
- Used for conditional logic in aggregation
Classify Endpoint Processes
- `Esql.is_ssm_shell_process`: The SSM `_script.sh` runner process itself
- `Esql.is_lolbin_process`: Child processes that are LOLBins/GTFOBins (excludes the shell runner)
- Separates the SSM orchestration process from suspicious child processes
Aggregate by SSM Command ID
- Group all events by `Esql.aws_ssm_command_id`
- Counts: CloudTrail events, LOLBin processes, SSM shell processes
- Timestamps: First cloud event and first LOLBin execution
- CloudTrail context: User ARNs, access keys, source IPs, user agents, regions
- Endpoint context: Hostnames, user names, process details
- SSM shell details: Command lines and PIDs of `_script.sh` runners
- LOLBin details: Names, executables, command lines, PIDs, parent command lines
Apply Detection Logic
- Require CloudTrail `SendCommand` event (`aws_cloudtrail_event_count > 0`)
- Require LOLBin execution on endpoint (`endpoint_events_process_lolbin_count > 0`)
- Must share same SSM command ID
- Must occur within 5-minute window
- Sort results by command timestamp
Why This Works
- Bypasses AWS redaction: See actual executed commands via endpoint telemetry
- High fidelity: Correlation proves suspicious command was sent AND executed
- Full visibility: Know WHO sent the command, WHAT ran, WHERE it came from
- Efficient: Pre-filtering reduces data processing and improves performance
Example Telemetry
alert doc
{
"Esql.aws_cloudtrail_event_count": 1,
"Esql.endpoint_events_process_lolbin_count": 1,
"Esql.endpoint_events_process_ssm_shell_count": 0,
"Esql.aws_cloudtrail_first_event_ts": "2025-10-18T19:04:10.000Z",
"Esql.endpoint_events_process_first_lolbin_ts": "2025-10-18T19:04:10.430Z",
"Esql.cloud_region_values": "us-west-2",
"Esql.source_ip_values": "35.87.44.118",
"Esql.user_agent_original_values": "aws-cli/2.31.18 md/awscrt#0.27.6 ua/2.1 os/linux#6.8.0-1016-aws md/arch#x86_64 lang/python#3.13.7 md/pyimpl#CPython m/n,b,Z,E cfg/retry-mode#standard md/installer#exe md/distrib#ubuntu.24 md/prompt#off md/command#ssm.send-command",
"Esql.host_name_values": "ip-10-0-2-214",
"Esql.process_name_lolbin_values": "base64",
"Esql.process_executable_lolbin_values": "/usr/bin/base64",
"Esql.process_command_line_lolbin_values": "base64 -d",
"Esql.process_pid_lolbin_values": 4928,
"Esql.process_parent_command_line_lolbin_values": "/bin/sh /var/lib/amazon/ssm/i-0ce963f7a0221448b/document/orchestration/1a32b3e8-be1e-4c22-8a79-2046135603d5/awsrunShellScript/0.awsrunShellScript/_script.sh",
"Esql.aws_ssm_command_id": "1a32b3e8-be1e-4c22-8a79-2046135603d5",
"Esql_priv.aws_cloudtrail_user_identity_arn_values": "arn:aws:iam::REDACTED:user/paws-backdoor-user-7511",
"Esql_priv.aws_cloudtrail_user_identity_access_key_id_values": "AKIA_REDACTED",
"Esql_priv.user_name_values": "paws-backdoor-user-7511",
"Esql_priv.endpoint_user_name_values": "root"
}