AWS IMDS Credential Usage Outside Expected Services


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.

Query · python

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)

Analyst notes

  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
Raw source AWS IMDS Credential Usage Outside Expected Services · Panther Python
Esc
Published by panther-labs/panther-analysis ↗, licensed under Apache 2.0 ↗. Reproduced here unmodified.
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)

Detection rules belong to the projects that publish them and remain under their own licenses. This site indexes and links to them; it claims no rights in them.