AnalysisType: rule
Filename: aws_imds_credential_exfiltration.py
RuleID: "AWS.CloudTrail.IMDSCredentialExfiltration"
DisplayName: "AWS IMDS Credential Usage Outside Expected Services"
Enabled: true
LogTypes:
- AWS.CloudTrail
Tags:
- AWS
- AWS STS
- Privilege Escalation:Valid Accounts
- Defense Evasion:Valid Accounts
Reports:
MITRE ATT&CK:
- TA0004:T1078.004
- TA0005:T1078.004
Status: Experimental
Severity: High
Description: >
Detects when an EC2 instance identity (credentials obtained via IMDS) is used to
make API calls outside of expected internal AWS services like SSM. This indicates
that IMDS credentials may have been exfiltrated from a compromised instance and
are being used externally for lateral movement or privilege escalation.
Runbook: |
1. Query CloudTrail for all API calls by userIdentity:arn in the 24 hours before and after this alert to identify the full scope of actions taken with the exfiltrated instance credentials
2. Check if sourceIPAddress is external to AWS or outside the expected VPC CIDR ranges for the EC2 instance associated with this role
3. Find all other alerts associated with this userIdentity:arn or sourceIPAddress in the past 7 days to assess whether this is part of a broader compromise campaign
Reference: https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/configuring-instance-metadata-service.html
SummaryAttributes:
- userAgent
- sourceIpAddress
- recipientAccountId
- p_any_aws_arns
DedupPeriodMinutes: 60
Threshold: 1
Tests:
- Name: IMDS Credentials Used From Public IP
ExpectedResult: true
Log:
{
"awsRegion": "us-east-1",
"eventName": "DescribeInstances",
"eventSource": "ec2.amazonaws.com",
"eventTime": "2024-01-15T10:30:00Z",
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"sourceIPAddress": "45.33.32.156",
"userAgent": "aws-cli/2.15.0",
"userIdentity": {
"accessKeyId": "ASIAIOSFODNN7EXAMPLE",
"accountId": "123456789012",
"arn": "arn:aws:sts::123456789012:assumed-role/MyAppRole/i-0abcdef1234567890",
"principalId": "AROAEXAMPLE:i-0abcdef1234567890",
"type": "AssumedRole",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"arn": "arn:aws:iam::123456789012:role/MyAppRole"
}
}
}
}
- Name: Normal EC2 Workload From Private IP
ExpectedResult: false
Log:
{
"awsRegion": "us-east-1",
"eventName": "PutObject",
"eventSource": "s3.amazonaws.com",
"eventTime": "2024-01-15T10:30:00Z",
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"sourceIPAddress": "10.0.1.50",
"userAgent": "aws-sdk-java/2.20.0",
"userIdentity": {
"arn": "arn:aws:sts::123456789012:assumed-role/MyAppRole/i-0abcdef1234567890",
"type": "AssumedRole"
}
}
- Name: AWS Service Hostname as Source IP
ExpectedResult: false
Log:
{
"awsRegion": "us-east-1",
"eventName": "Decrypt",
"eventSource": "kms.amazonaws.com",
"eventTime": "2024-01-15T10:30:00Z",
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"sourceIPAddress": "cloudformation.amazonaws.com",
"userIdentity": {
"arn": "arn:aws:sts::123456789012:assumed-role/MyAppRole/i-0abcdef1234567890",
"type": "AssumedRole"
}
}
- Name: Legitimate SSM Activity
ExpectedResult: false
Log:
{
"awsRegion": "us-east-1",
"eventName": "UpdateInstanceInformation",
"eventSource": "ssm.amazonaws.com",
"eventTime": "2024-01-15T10:30:00Z",
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"sourceIPAddress": "10.0.1.50",
"userAgent": "amazon-ssm-agent/3.2.0",
"userIdentity": {
"arn": "arn:aws:sts::123456789012:assumed-role/MyAppRole/i-0abcdef1234567890",
"type": "AssumedRole"
}
}
- Name: AWS Internal Source IP
ExpectedResult: false
Log:
{
"awsRegion": "us-east-1",
"eventName": "SendHeartbeat",
"eventSource": "ec2messages.amazonaws.com",
"eventTime": "2024-01-15T10:30:00Z",
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"sourceIPAddress": "AWS Internal",
"userAgent": "amazon-ssm-agent/3.2.0",
"userIdentity": {
"arn": "arn:aws:sts::123456789012:assumed-role/MyAppRole/i-0abcdef1234567890",
"type": "AssumedRole"
}
}
- Name: AWS Service InvokedBy on behalf of instance
ExpectedResult: false
Log:
{
"awsRegion": "us-east-1",
"eventName": "DescribeInstances",
"eventSource": "ec2.amazonaws.com",
"eventTime": "2024-01-15T10:30:00Z",
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"sourceIPAddress": "45.33.32.156",
"userIdentity": {
"arn": "arn:aws:sts::123456789012:assumed-role/MyAppRole/i-0abcdef1234567890",
"type": "AssumedRole",
"invokedBy": "eks.amazonaws.com"
}
}
- Name: SSM Messages Service Source
ExpectedResult: false
Log:
{
"awsRegion": "us-east-1",
"eventName": "CreateControlChannel",
"eventSource": "ssmmessages.amazonaws.com",
"eventTime": "2024-01-15T10:30:00Z",
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"sourceIPAddress": "45.33.32.156",
"userIdentity": {
"arn": "arn:aws:sts::123456789012:assumed-role/MyAppRole/i-0abcdef1234567890",
"type": "AssumedRole"
}
}
- Name: RegisterManagedInstance Event
ExpectedResult: false
Log:
{
"awsRegion": "us-east-1",
"eventName": "RegisterManagedInstance",
"eventSource": "ec2.amazonaws.com",
"eventTime": "2024-01-15T10:30:00Z",
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"sourceIPAddress": "10.0.1.50",
"userIdentity": {
"arn": "arn:aws:sts::123456789012:assumed-role/MyAppRole/i-0abcdef1234567890",
"type": "AssumedRole"
}
}
- Name: Non-Instance Role
ExpectedResult: false
Log:
{
"awsRegion": "us-east-1",
"eventName": "DescribeInstances",
"eventSource": "ec2.amazonaws.com",
"eventTime": "2024-01-15T10:30:00Z",
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"sourceIPAddress": "45.33.32.156",
"userAgent": "aws-cli/2.15.0",
"userIdentity": {
"arn": "arn:aws:sts::123456789012:assumed-role/MyCustomRole/session",
"type": "AssumedRole"
}
}
- Name: IMDS Credentials Used From Amazon.com ASN Source IP
ExpectedResult: true
Log:
{
"awsRegion": "us-east-1",
"eventName": "DescribeInstances",
"eventSource": "ec2.amazonaws.com",
"eventTime": "2024-01-15T10:30:00Z",
"eventType": "AwsApiCall",
"recipientAccountId": "123456789012",
"sourceIPAddress": "52.42.146.205",
"userAgent": "aws-cli/2.15.0",
"userIdentity": {
"accessKeyId": "ASIAIOSFODNN7EXAMPLE",
"accountId": "123456789012",
"arn": "arn:aws:sts::123456789012:assumed-role/MyAppRole/i-0abcdef1234567890",
"principalId": "AROAEXAMPLE:i-0abcdef1234567890",
"type": "AssumedRole",
"sessionContext": {
"sessionIssuer": {
"type": "Role",
"arn": "arn:aws:iam::123456789012:role/MyAppRole"
}
}
},
"p_enrichment":
{
"ipinfo_asn":
{
"sourceIPAddress":
{
"asn": "AS16509",
"domain": "amazon.com",
"name": "Amazon.com, Inc.",
"p_match": "52.42.146.205",
"route": "52.40.0.0/14",
"type": "hosting",
},
},
},
}
# ------ paired body: aws_imds_credential_exfiltration.py ------
import ipaddress
import re
from panther_aws_helpers import aws_rule_context
from panther_ipinfo_helpers import get_ipinfo_asn
# IMDS credentials appear as assumed-role sessions where the session name
# is an EC2 instance ID (i-xxxxxxxxxxxxxxxxx)
INSTANCE_SESSION_PATTERN = re.compile(r":assumed-role/.+/i-[0-9a-f]+$")
# Legitimate internal services and actions that use instance identity
INTERNAL_SOURCES = {
"ssm.amazonaws.com",
"ec2messages.amazonaws.com", # SSM agent heartbeat/polling channel
"ssmmessages.amazonaws.com", # SSM Session Manager channel
}
INTERNAL_EVENTS = {"RegisterManagedInstance"}
INTERNAL_IPS = {"AWS Internal"}
def _is_valid_ip(ip_str):
try:
ipaddress.ip_address(ip_str)
return True
except ValueError:
return False
def _is_private_ip(ip_str):
try:
return ipaddress.ip_address(ip_str).is_private
except ValueError:
return False
def _is_amazon_domain(event):
"""Check if the source IP's ipinfo ASN enrichment resolves to an amazon.com domain."""
ipinfo_asn = get_ipinfo_asn(event)
if not ipinfo_asn:
return False
return ipinfo_asn.domain("sourceIPAddress") == "amazon.com"
def rule(event):
arn = event.deep_get("userIdentity", "arn", default="")
if not INSTANCE_SESSION_PATTERN.search(arn):
return False
# Exclude calls made by an AWS service on behalf of the instance (e.g. EKS, ECS, CodeDeploy)
if event.deep_get("userIdentity", "invokedBy", default="").endswith(".amazonaws.com"):
return False
# Exclude legitimate internal services
if event.get("eventSource") in INTERNAL_SOURCES:
return False
if event.get("eventName") in INTERNAL_EVENTS:
return False
# Only alert when credentials are used from a public IP.
# Filters out "AWS Internal", service hostnames (e.g. "eks.amazonaws.com"), and private VPC IPs.
source_ip = event.get("sourceIPAddress", "")
if source_ip in INTERNAL_IPS or source_ip.endswith(".amazonaws.com"):
return False
return _is_valid_ip(source_ip) and not _is_private_ip(source_ip)
def severity(event):
# Source IPs that resolve to an amazon.com ASN domain are lower risk
# (e.g. AWS-managed infrastructure making calls on the instance's behalf)
if _is_amazon_domain(event):
return "INFO"
return "DEFAULT"
def title(event):
arn = event.deep_get("userIdentity", "arn", default="<unknown>")
ip_addr = event.get("sourceIPAddress", "<unknown>")
action = event.get("eventName", "<unknown>")
return (
f"IMDS instance credential [{arn}] used from [{ip_addr}] "
f"to call [{event.get('eventSource', '')}:{action}]"
)
def alert_context(event):
return aws_rule_context(event)