Analytic rule catalog
AWSCloudTrail - Successful brute force attack on S3 Bucket
Back
| Id | 31b9e94b-0df6-4a3d-a297-3457b53c5d86 |
| Rulename | AWSCloudTrail - Successful brute force attack on S3 Bucket |
| 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. |
| Severity | High |
| Tactics | CredentialAccess |
| Techniques | T1110 |
| Required data connectors | AWS |
| Kind | Scheduled |
| Query frequency | 1h |
| Query period | 1h |
| Trigger threshold | 0 |
| Trigger operator | gt |
| Source Uri | https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Amazon%20Web%20Services/Analytic%20Rules/AWS_S3BruteForce.yaml |
| Version | 1.0.2 |
| Arm template | 31b9e94b-0df6-4a3d-a297-3457b53c5d86.json |
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
requiredDataConnectors:
- dataTypes:
- AWSCloudTrail
connectorId: AWS
OriginalUri: https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Amazon%20Web%20Services/Analytic%20Rules/AWS_S3BruteForce.yaml
version: 1.0.2
status: Available
alertDetailsOverride:
alertDescriptionFormat: Detected repeated failed and then successful S3 GetObject access for bucket {{bucketName}} in account {{RecipientAccountId}}.
alertDisplayNameFormat: AWS S3 brute force pattern detected from {{SourceIpAddress}}
customDetails:
success_keys: success_keys
failed_keys: failed_keys
bucketName: bucketName
RecipientAccountId: RecipientAccountId
queryFrequency: 1h
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
tactics:
- CredentialAccess
relevantTechniques:
- T1110
id: 31b9e94b-0df6-4a3d-a297-3457b53c5d86
triggerThreshold: 0
triggerOperator: gt
name: AWSCloudTrail - Successful brute force attack on S3 Bucket
queryPeriod: 1h
severity: High
entityMappings:
- fieldMappings:
- columnName: AccountName
identifier: Name
- columnName: AccountUPNSuffix
identifier: UPNSuffix
- columnName: RecipientAccountId
identifier: CloudAppAccountId
entityType: Account
- fieldMappings:
- columnName: SourceIpAddress
identifier: Address
entityType: IP
kind: Scheduled
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.
{
"$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#",
"contentVersion": "1.0.0.0",
"parameters": {
"workspace": {
"type": "String"
}
},
"resources": [
{
"apiVersion": "2024-01-01-preview",
"id": "[concat(resourceId('Microsoft.OperationalInsights/workspaces/providers', parameters('workspace'), 'Microsoft.SecurityInsights'),'/alertRules/31b9e94b-0df6-4a3d-a297-3457b53c5d86')]",
"kind": "Scheduled",
"name": "[concat(parameters('workspace'),'/Microsoft.SecurityInsights/31b9e94b-0df6-4a3d-a297-3457b53c5d86')]",
"properties": {
"alertDetailsOverride": {
"alertDescriptionFormat": "Detected repeated failed and then successful S3 GetObject access for bucket {{bucketName}} in account {{RecipientAccountId}}.",
"alertDisplayNameFormat": "AWS S3 brute force pattern detected from {{SourceIpAddress}}"
},
"alertRuleTemplateName": "31b9e94b-0df6-4a3d-a297-3457b53c5d86",
"customDetails": {
"bucketName": "bucketName",
"failed_keys": "failed_keys",
"RecipientAccountId": "RecipientAccountId",
"success_keys": "success_keys"
},
"description": "Detects repeated failed GetObject attempts against an S3 bucket followed by a successful access from the\nsame source, which can indicate brute-force-style object discovery or access attempts.\n",
"displayName": "AWSCloudTrail - Successful brute force attack on S3 Bucket",
"enabled": true,
"entityMappings": [
{
"entityType": "Account",
"fieldMappings": [
{
"columnName": "AccountName",
"identifier": "Name"
},
{
"columnName": "AccountUPNSuffix",
"identifier": "UPNSuffix"
},
{
"columnName": "RecipientAccountId",
"identifier": "CloudAppAccountId"
}
]
},
{
"entityType": "IP",
"fieldMappings": [
{
"columnName": "SourceIpAddress",
"identifier": "Address"
}
]
}
],
"OriginalUri": "https://github.com/Azure/Azure-Sentinel/blob/master/Solutions/Amazon%20Web%20Services/Analytic%20Rules/AWS_S3BruteForce.yaml",
"query": "let timeframe = 1h;\nlet failed_attempts = AWSCloudTrail\n| where TimeGenerated >= ago(timeframe)\n| where EventName == \"GetObject\" and isnotempty(ErrorMessage) and isnotempty(ErrorCode)\n| where UserIdentityAccountId == \"ANONYMOUS_PRINCIPAL\" or UserIdentityAccessKeyId <> RecipientAccountId\n| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)\n| extend UserName = tostring(split(UserIdentityArn, '/')[-1])\n| extend AccountName = case( UserIdentityPrincipalid == \"Anonymous\", \"Anonymous\", isempty(UserIdentityUserName), UserName, UserIdentityUserName)\n| extend AccountName = iif(AccountName contains \"@\", tostring(split(AccountName, '@', 0)[0]), AccountName),\n AccountUPNSuffix = iif(AccountName contains \"@\", tostring(split(AccountName, '@', 1)[0]), \"\")\n| extend bucketName = tostring(parse_json(RequestParameters).bucketName), keyName = tostring(parse_json(RequestParameters).key)\n| summarize time_min_failed=arg_min(TimeGenerated, *), failed_keys = dcount(keyName) by RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, SourceIpAddress, bucketName\n| where failed_keys > 20;\nlet success_attempts = AWSCloudTrail\n| where TimeGenerated >= ago(timeframe)\n| where EventName == \"GetObject\" and isempty(ErrorMessage) and isempty(ErrorCode)\n| where UserIdentityAccountId == \"ANONYMOUS_PRINCIPAL\" or UserIdentityAccessKeyId <> RecipientAccountId\n| extend UserIdentityArn = iif(isempty(UserIdentityArn), tostring(parse_json(Resources)[0].ARN), UserIdentityArn)\n| extend UserName = tostring(split(UserIdentityArn, '/')[-1])\n| extend AccountName = case( UserIdentityPrincipalid == \"Anonymous\", \"Anonymous\", isempty(UserIdentityUserName), UserName, UserIdentityUserName)\n| extend AccountName = iif(AccountName contains \"@\", tostring(split(AccountName, '@', 0)[0]), AccountName),\n AccountUPNSuffix = iif(AccountName contains \"@\", tostring(split(AccountName, '@', 1)[0]), \"\")\n| extend bucketName = tostring(parse_json(RequestParameters).bucketName), keyName = tostring(parse_json(RequestParameters).key)\n| summarize time_min_success=arg_min(TimeGenerated, *), success_keys = dcount(keyName) by RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, SourceIpAddress, bucketName\n| where success_keys >= 1;\nfailed_attempts\n| join kind=inner success_attempts on SourceIpAddress, RecipientAccountId, AccountName, AccountUPNSuffix, UserIdentityAccountId, bucketName\n| where time_min_success > time_min_failed\n| project-away keyName\n",
"queryFrequency": "PT1H",
"queryPeriod": "PT1H",
"severity": "High",
"status": "Available",
"subTechniques": [],
"suppressionDuration": "PT1H",
"suppressionEnabled": false,
"tactics": [
"CredentialAccess"
],
"techniques": [
"T1110"
],
"templateVersion": "1.0.2",
"triggerOperator": "GreaterThan",
"triggerThreshold": 0
},
"type": "Microsoft.OperationalInsights/workspaces/providers/alertRules"
}
]
}