Detection rules › Kusto
AWSCloudTrail - S3 bucket suspicious ransomware activity
Detects a ransomware-like sequence where objects are read from an S3 bucket and then overwritten using an external KMS key. This pattern can indicate malicious encryption and potential data denial in the bucket.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Impact |
Telemetry coverage
Rules detecting the same action
These rules filter on the same operation.
- AWS Exfiltration via Anomalous GetObject API Activity (Splunk)
- AWSCloudTrail - S3 Object Exfiltration from Anonymous User (Kusto)
- AWSCloudTrail - Successful brute force attack on S3 Bucket (Kusto)
- AWSCloudTrail - Suspicious command sent to EC2 (Kusto)
- S3 Access Via VPC Endpoint From External IP (Panther)
- Suspicious access of BEC related documents in AWS S3 buckets (Kusto)
Rule body
id: b442b9e2-5cc4-4129-a85b-a5ef38a9e5f0
name: AWSCloudTrail - S3 bucket suspicious ransomware activity
description: |
Detects a ransomware-like sequence where objects are read from an S3 bucket and then overwritten using an
external KMS key. This pattern can indicate malicious encryption and potential data denial in the bucket.
severity: High
status: Available
requiredDataConnectors:
- connectorId: AWS
dataTypes:
- AWSCloudTrail
queryFrequency: 1h
queryPeriod: 1h
triggerOperator: gt
triggerThreshold: 0
tactics:
- Impact
relevantTechniques:
- T1486
query: |
let timeframe = 1h;
let lookback = 2h;
// The attacker downloads the object(s) from the compromised bucket
let GetObject = AWSCloudTrail
| where TimeGenerated >= ago(lookback)
| where EventName == "GetObject" and isempty(ErrorCode) and isempty(ErrorMessage)
| extend bucketName = tostring(parse_json(RequestParameters).bucketName), keyName = tostring(parse_json(RequestParameters).key)
| project-rename StartTime = TimeGenerated;
// Then, the attacker overwrites the same object(s) but encrypted with his own key
let PutObject = AWSCloudTrail
| where TimeGenerated >= ago(timeframe)
| where EventName == "PutObject" and isempty(ErrorCode) and isempty(ErrorMessage)
| extend bucketName = tostring(parse_json(RequestParameters).bucketName), keyName = tostring(parse_json(RequestParameters).key)
| extend kmsId = tostring(parse_json(RequestParameters).["x-amz-server-side-encryption-aws-kms-key-id"])
| where tostring(kmsId) !has tostring(RecipientAccountId) and kmsId <> "";
PutObject
| join kind=inner
(
GetObject
)
on $left.bucketName == $right.bucketName, $left.keyName == $right.keyName
| where TimeGenerated > StartTime
| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
entityMappings:
- entityType: Account
fieldMappings:
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- identifier: CloudAppAccountId
columnName: RecipientAccountId
- entityType: IP
fieldMappings:
- identifier: Address
columnName: SourceIpAddress
customDetails:
bucketName: bucketName
keyName: keyName
kmsId: kmsId
RecipientAccountId: RecipientAccountId
alertDetailsOverride:
alertDisplayNameFormat: 'AWS S3 ransomware-like overwrite activity by {{AccountName}}'
alertDescriptionFormat: 'Detected GetObject followed by encrypted PutObject activity in bucket {{bucketName}} from {{SourceIpAddress}} for account {{RecipientAccountId}}.'
version: 1.0.2
kind: Scheduled
Stages and Predicates
Parameters
let timeframe = 1h;
let lookback = 2h;
let PutObject is inlined into the numbered stages below.
Let binding: GetObject
let GetObject = AWSCloudTrail
| where TimeGenerated >= ago(lookback)
| where EventName == "GetObject" and isempty(ErrorCode) and isempty(ErrorMessage)
| extend bucketName = tostring(parse_json(RequestParameters).bucketName), keyName = tostring(parse_json(RequestParameters).key)
| project-rename StartTime = TimeGenerated;
Stages 1 to 6 define let PutObject (the rule's main pipeline source); stages 7 to 9 run on it.
Stage 1: source
AWSCloudTrail
Stage 2: where
| where TimeGenerated >= ago(timeframe)
Stage 3: where
| where EventName == "PutObject" and isempty(ErrorCode) and isempty(ErrorMessage)
Stage 4: extend
| extend bucketName = tostring(parse_json(RequestParameters).bucketName), keyName = tostring(parse_json(RequestParameters).key)
Stage 5: extend
| extend kmsId = tostring(parse_json(RequestParameters).["x-amz-server-side-encryption-aws-kms-key-id"])
Stage 6: where
| where tostring(kmsId) !has tostring(RecipientAccountId) and kmsId <> ""
Stage 7: join
PutObject
| join kind=inner
(
GetObject
)
on $left.bucketName == $right.bucketName, $left.keyName == $right.keyName
Stage 8: where
| where TimeGenerated > StartTime
Stage 9: extend (4 consecutive steps)
| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)
| extend UserName = tostring(split(UserIdentityArn, '/')[-1])
| extend AccountName = case( UserIdentityPrincipalid == "Anonymous", "Anonymous", isempty(UserIdentityUserName), UserName, UserIdentityUserName)
| extend AccountName = iif(AccountName contains "@", tostring(split(AccountName, '@', 0)[0]), AccountName),
AccountUPNSuffix = iif(AccountName contains "@", tostring(split(AccountName, '@', 1)[0]), "")
Exclusions
The rule actively suppresses these predicates.
| Field | Kind | Excluded values | Search |
|---|---|---|---|
kmsId | match | RecipientAccountId | excludes:kmsId field:"kmsId" value:"RecipientAccountId" |
Indicators
These rows show field, operator, and value matches.
| Field | Kind | Values | Search |
|---|---|---|---|
ErrorCode | is_null | field:"aws::errorCode" kind:is_null | |
ErrorMessage | is_null | field:"aws::errorMessage" kind:is_null | |
EventName | eq |
| field:"aws::eventName" kind:eq |
TimeGenerated | cross_field_compare |
| field:"TimeGenerated" kind:cross_field_compare value:"StartTime" |
Output fields
These fields are emitted when the rule matches.
| Field | Source |
|---|---|
bucketName | extend |
keyName | extend |
kmsId | extend |
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |