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 | T1486 Data Encrypted for Impact |
Rules detecting the same action
Other rules on this platform that filter on the same API call or 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 kusto
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 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;
Derived from lookback.
The stages below define let PutObject (the rule's main pipeline source).
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 <> ""
The stages below run on PutObject (the outer pipeline).
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
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
kmsId | match | RecipientAccountId |
Indicators
Each row is a field, operator, and value that the rule matches. The corpus column counts how many other rules in the catalog look for the same combination: high numbers point to widely-used, community-vetted indicators. Blank or 1 shows that the indicator is specific to this rule.
| Field | Kind | Values |
|---|---|---|
ErrorCode | is_null | |
ErrorMessage | is_null | |
EventName | eq |
|
TimeGenerated | gt |
|
Output fields
Fields the rule emits when it matches. Chronicle authors list these in the outcome block; they appear on the detection and $risk_score drives alerting. Sentinel / Defender XDR rules build them up through project / summarize / extend stages. Sentinel maps these into alert fields via entityMappings and customDetails; Defender XDR custom detections surface them as alert fields directly.
| Field | Source |
|---|---|
bucketName | extend |
keyName | extend |
kmsId | extend |
UserIdentityArn | extend |
UserName | extend |
AccountName | extend |
AccountUPNSuffix | extend |