S3 Object Encrypted with External KMS Key
Description
Detects when an S3 object is copied with a KMS key belonging to an account ID different than the bucket owner's account ID. This technique is used in S3 ransomware attacks where attackers encrypt objects with their own KMS key from an attacker-controlled AWS account, making the data inaccessible to the original owner. This is often a precursor to ransom demands or permanent data loss.
Query · python
from panther_aws_helpers import aws_cloudtrail_success, aws_rule_context
def rule(event):
if event.get("eventName") != "CopyObject" or not aws_cloudtrail_success(event):
return False
kms_key_arn = event.deep_get(
"requestParameters",
"x-amz-server-side-encryption-aws-kms-key-id",
default="<UNKNOWN_KEY_ID>",
)
if kms_key_arn.startswith("arn:aws:kms:"):
# Extract account ID from KMS key ARN (format: arn:aws:kms:region:account:key/key-id)
kms_parts = kms_key_arn.split(":")
if len(kms_parts) >= 5:
kms_account_id = kms_parts[4]
bucket_account_id = event.get("recipientAccountId", "")
# Alert on cross-account KMS key usage
if kms_account_id != bucket_account_id:
return True
return False
def title(event):
return (
f"[AWS.CloudTrail] User [{event.udm('actor_user')}] "
f"encrypted an object in bucket "
f"[{event.deep_get('requestParameters', 'bucketName')}] "
f"with a KMS key belonging to a different account ID "
f"than the account owner ID"
)
def alert_context(event):
context = aws_rule_context(event)
context["bucketName"] = event.deep_get(
"requestParameters", "bucketName", default="<UNKNOWN_BUCKET>"
)
context["objectKey"] = event.deep_get("requestParameters", "key", default="<UNKNOWN_KEY>")
kms_key_arn = event.deep_get(
"requestParameters",
"x-amz-server-side-encryption-aws-kms-key-id",
default="<UNKNOWN_KEY_ARN>",
)
context["kmsKeyId"] = kms_key_arn
# Add cross-account indicator
if kms_key_arn:
kms_parts = kms_key_arn.split(":")
if len(kms_parts) >= 5:
kms_account_id = kms_parts[4]
bucket_account_id = event.get("recipientAccountId", "")
context["isCrossAccountKms"] = kms_account_id != bucket_account_id
context["kmsAccountId"] = kms_account_id
context["bucketAccountId"] = bucket_account_id
context["encryption"] = event.deep_get(
"requestParameters", "x-amz-server-side-encryption", default="<UNKNOWN_ENCRYPTION>"
)
return context
Analyst notes
- Query CloudTrail for all CopyObject events by the userIdentity:arn in the 24 hours before and after the alert to identify all affected objects in the requestParameters:bucketName
- Check if the KMS key ARN from resources field belongs to an external account ID that appears in any legitimate cross-account operations in the past 90 days
- Find all S3 GetObject and ListBucket events by this user on the source bucket in the 1 hour before the first CopyObject to check if the attacker performed reconnaissance