Detection rules › Kusto

AWSCloudTrail - S3 bucket suspicious ransomware activity

Status
available
Severity
high
Time window
1h
Group by
bucketName, keyName
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
ImpactT1486 Data Encrypted for Impact

Rules detecting the same action

Other rules on this platform that filter on the same API call or operation.

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.

FieldKindExcluded values
kmsIdmatchRecipientAccountId

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.

FieldKindValues
ErrorCodeis_null
  • (no value, null check)
ErrorMessageis_null
  • (no value, null check)
EventNameeq
  • GetObject transforms: cased
  • PutObject transforms: cased
TimeGeneratedgt
  • StartTime transforms: cased

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.

FieldSource
bucketNameextend
keyNameextend
kmsIdextend
UserIdentityArnextend
UserNameextend
AccountNameextend
AccountUPNSuffixextend