Microsoft Sentinel Analytic Rules
cloudbrothers.infoAzure Sentinel RepoToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeToggle Dark/Light/Auto modeBack to homepage

AWSCloudTrail - Successful brute force attack on S3 Bucket

Back
Id31b9e94b-0df6-4a3d-a297-3457b53c5d86
RulenameAWSCloudTrail - Successful brute force attack on S3 Bucket
DescriptionDetects repeated failed GetObject attempts against an S3 bucket followed by a successful access from the

same source, which can indicate brute-force-style object discovery or access attempts.
SeverityHigh
TacticsCredentialAccess
TechniquesT1110
Required data connectorsAWS
KindScheduled
Query frequency1h
Query period1h
Trigger threshold0
Trigger operatorgt
Source Urihttps://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Amazon Web Services/Analytic Rules/AWS_S3BruteForce.yaml
Version1.0.2
Arm template31b9e94b-0df6-4a3d-a297-3457b53c5d86.json
Deploy To Azure
let timeframe = 1h;
let failed_attempts = AWSCloudTrail
| where TimeGenerated >= ago(timeframe)
| where EventName == "GetObject" and isnotempty(ErrorMessage) and isnotempty(ErrorCode)
| where UserIdentityAccountId == "ANONYMOUS_PRINCIPAL" or UserIdentityAccessKeyId <> RecipientAccountId
| 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]), "")
| extend bucketName = tostring(parse_json(RequestParameters).bucketName), keyName = tostring(parse_json(RequestParameters).key)
| summarize time_min_failed=arg_min(TimeGenerated, *), failed_keys = dcount(keyName) by RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, SourceIpAddress, bucketName
| where failed_keys > 20;
let success_attempts = AWSCloudTrail
| where TimeGenerated >= ago(timeframe)
| where EventName == "GetObject" and isempty(ErrorMessage) and isempty(ErrorCode)
| where UserIdentityAccountId == "ANONYMOUS_PRINCIPAL" or UserIdentityAccessKeyId <> RecipientAccountId
| 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]), "")
| extend bucketName = tostring(parse_json(RequestParameters).bucketName), keyName = tostring(parse_json(RequestParameters).key)
| summarize time_min_success=arg_min(TimeGenerated, *), success_keys = dcount(keyName) by RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, SourceIpAddress, bucketName
| where success_keys >= 1;
failed_attempts
| join kind=inner success_attempts on SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, bucketName
| where time_min_success > time_min_failed
| project-away keyName
queryPeriod: 1h
entityMappings:
- entityType: Account
  fieldMappings:
  - columnName: AccountName
    identifier: Name
  - columnName: AccountUPNSuffix
    identifier: UPNSuffix
  - columnName: RecipientAccountId
    identifier: CloudAppAccountId
- entityType: IP
  fieldMappings:
  - columnName: SourceIpAddress
    identifier: Address
relevantTechniques:
- T1110
triggerOperator: gt
triggerThreshold: 0
status: Available
name: AWSCloudTrail - Successful brute force attack on S3 Bucket
requiredDataConnectors:
- connectorId: AWS
  dataTypes:
  - AWSCloudTrail
id: 31b9e94b-0df6-4a3d-a297-3457b53c5d86
version: 1.0.2
description: |
  Detects repeated failed GetObject attempts against an S3 bucket followed by a successful access from the
  same source, which can indicate brute-force-style object discovery or access attempts.  
alertDetailsOverride:
  alertDisplayNameFormat: AWS S3 brute force pattern detected from {{SourceIpAddress}}
  alertDescriptionFormat: Detected repeated failed and then successful S3 GetObject access for bucket {{bucketName}} in account {{RecipientAccountId}}.
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Amazon Web Services/Analytic Rules/AWS_S3BruteForce.yaml
query: |
  let timeframe = 1h;
  let failed_attempts = AWSCloudTrail
  | where TimeGenerated >= ago(timeframe)
  | where EventName == "GetObject" and isnotempty(ErrorMessage) and isnotempty(ErrorCode)
  | where UserIdentityAccountId == "ANONYMOUS_PRINCIPAL" or UserIdentityAccessKeyId <> RecipientAccountId
  | 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]), "")
  | extend bucketName = tostring(parse_json(RequestParameters).bucketName), keyName = tostring(parse_json(RequestParameters).key)
  | summarize time_min_failed=arg_min(TimeGenerated, *), failed_keys = dcount(keyName) by RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, SourceIpAddress, bucketName
  | where failed_keys > 20;
  let success_attempts = AWSCloudTrail
  | where TimeGenerated >= ago(timeframe)
  | where EventName == "GetObject" and isempty(ErrorMessage) and isempty(ErrorCode)
  | where UserIdentityAccountId == "ANONYMOUS_PRINCIPAL" or UserIdentityAccessKeyId <> RecipientAccountId
  | 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]), "")
  | extend bucketName = tostring(parse_json(RequestParameters).bucketName), keyName = tostring(parse_json(RequestParameters).key)
  | summarize time_min_success=arg_min(TimeGenerated, *), success_keys = dcount(keyName) by RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, SourceIpAddress, bucketName
  | where success_keys >= 1;
  failed_attempts
  | join kind=inner success_attempts on SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, bucketName
  | where time_min_success > time_min_failed
  | project-away keyName  
queryFrequency: 1h
customDetails:
  bucketName: bucketName
  RecipientAccountId: RecipientAccountId
  success_keys: success_keys
  failed_keys: failed_keys
kind: Scheduled
tactics:
- CredentialAccess
severity: High