Detection rules › Kusto
High risk Office operation conducted by IP Address that recently attempted to log into a disabled account
'It is possible that a disabled user account is compromised and another account on the same IP is used to perform operations that are not typical for that user. The query filters the SigninLogs for entries where ResultType is indicates a disabled account and the TimeGenerated is within a defined time range. It then summarizes these entries by IPAddress and AppId, calculating various statistics such as number of login attempts, distinct UPNs, App IDs etc and joins these results with another set of results from SigninLogs, filtering for entries with less than normal number of successful sign-ins. It then filters out entries where there were no successful sign-ins or where successful sign-ins did not occur within the same lookback period as the failed sign-ins, later projecting relevant fields by the count of login attempts, and expands the set of successful sign-ins into individual events. Finally, it joins these results with entries from OfficeActivity where certain operations deemed rare and high risk have been performed, ensuring their occurrance within a certain time range of the successful sign-ins.'
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Initial Access | T1078 Valid Accounts |
| Persistence | T1078 Valid Accounts, T1098 Account Manipulation |
| Collection | T1114 Email Collection |
Rules detecting the same action
Other rules on this platform that filter on the same API call or operation.
- Account Disabled or Blocked for Sign in Attempts (Sigma)
- Anomalous sign-in location by user account and authenticating application (Kusto)
- Anomalous Single Factor Signin (Kusto)
- Authentications of Privileged Accounts Outside of Expected Controls (Kusto)
- Azure Many Failed SignIns (Panther)
- Azure Portal sign in from another Azure Tenant (Kusto)
- Azure Service Principal Sign-In Followed by Arc Cluster Credential Access (Elastic)
- Azure SignIn via Legacy Authentication Protocol (Panther)
Rule body kusto
id: 9adbd1c3-a4be-44ef-ac2f-503fd25692ee
name: High risk Office operation conducted by IP Address that recently attempted to log into a disabled account
description: |
'It is possible that a disabled user account is compromised and another account on the same IP is used to perform operations that are not typical for that user.
The query filters the SigninLogs for entries where ResultType is indicates a disabled account and the TimeGenerated is within a defined time range.
It then summarizes these entries by IPAddress and AppId, calculating various statistics such as number of login attempts, distinct UPNs, App IDs etc and joins these results with another set of results from SigninLogs, filtering for entries with less than normal number of successful sign-ins.
It then filters out entries where there were no successful sign-ins or where successful sign-ins did not occur within the same lookback period as the failed sign-ins, later projecting relevant fields by the count of login attempts, and expands the set of successful sign-ins into individual events.
Finally, it joins these results with entries from OfficeActivity where certain operations deemed rare and high risk have been performed, ensuring their occurrance within a certain time range of the successful sign-ins.'
severity: Medium
requiredDataConnectors:
- connectorId: AzureActiveDirectory
dataTypes:
- SigninLogs
- connectorId: Office365
dataTypes:
- OfficeActivity
queryFrequency: 1d
queryPeriod: 8d
triggerOperator: gt
triggerThreshold: 0
tactics:
- InitialAccess
- Persistence
- Collection
relevantTechniques:
- T1078
- T1098
- T1114
query: |
let threshold = 100;
let timeRange = ago(7d);
let timeBuffer = 1;
SigninLogs
| where TimeGenerated > timeRange
| where ResultType == "50057"
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), disabledAccountLoginAttempts = count(),
disabledAccountsTargeted = dcount(UserPrincipalName), applicationsTargeted = dcount(AppDisplayName), disabledAccountSet = make_set(UserPrincipalName),
applicationSet = make_set(AppDisplayName) by IPAddress, AppId
| order by disabledAccountLoginAttempts desc
| join kind=inner (
// IPs are considered suspicious - and any related successful sign-ins are detected
SigninLogs
| where TimeGenerated > timeRange
| where ResultType == 0
| summarize successSigninStart = min(TimeGenerated), successSigninEnd = max(TimeGenerated), successfulAccountSigninCount = dcount(UserPrincipalName), successfulAccountSigninSet = make_set(UserPrincipalName, 15) by IPAddress
// Assume IPs associated with sign-ins from 100+ distinct user accounts are safe
| where successfulAccountSigninCount < threshold
) on IPAddress
// IPs where attempts to authenticate as disabled user accounts originated, and had a non-zero success rate for some other account
| where successfulAccountSigninCount != 0
// Successful Account Signins occur within the same lookback period as the failed
| extend SuccessBeforeFailure = iff(successSigninStart >= StartTime and successSigninEnd <= EndTime, true, false)
| project StartTime, EndTime, IPAddress, disabledAccountLoginAttempts, disabledAccountsTargeted, disabledAccountSet, applicationSet,
successfulAccountSigninCount, successfulAccountSigninSet, successSigninStart, successSigninEnd, AppId
| order by disabledAccountLoginAttempts
// Break up the string of Succesfully signed into accounts into individual events
| mvexpand successfulAccountSigninSet
| extend JoinedOnIp = IPAddress
| join kind = inner (
OfficeActivity
| where TimeGenerated > timeRange
| where Operation in~ ( "Add-MailboxPermission", "Add-MailboxFolderPermission", "Set-Mailbox", "New-ManagementRoleAssignment", "New-InboxRule", "Set-InboxRule", "Set-TransportRule") and not(UserId has_any ('NT AUTHORITY\\SYSTEM (Microsoft.Exchange.ServiceHost)', 'NT AUTHORITY\\SYSTEM (w3wp)', 'devilfish-applicationaccount'))
// Remove port from the end of the IP and/or square brackets around IP, if they exist
| extend JoinedOnIp = case(
ClientIP matches regex @'\[((25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\]-\d{1,5}', tostring(extract('\\[([0-9]+\\.[0-9]+\\.[0-9]+)\\]-[0-9]+', 1, ClientIP)),
ClientIP matches regex @'\[((25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\]', tostring(extract('\\[([0-9]+\\.[0-9]+\\.[0-9]+)\\]', 1, ClientIP)),
ClientIP matches regex @'(((25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?))-\d{1,5}', tostring(extract('([0-9]+\\.[0-9]+\\.[0-9]+)-[0-9]+', 1, ClientIP)),
ClientIP matches regex @'((25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)', ClientIP,
ClientIP matches regex @'\[((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\d{1,3}(?:\.\d{1,3}){3})\]-\d{1,5}', tostring(extract('\\[((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\\d{1,3}(?:\\.\\d{1,3}){3})\\]-[0-9]+', 1, ClientIP)),
ClientIP matches regex @'\[((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\d{1,3}(?:\.\d{1,3}){3})\]', tostring(extract('\\[((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\\d{1,3}(?:\\.\\d{1,3}){3})\\]', 1, ClientIP)),
ClientIP matches regex @'((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\d{1,3}(?:\.\d{1,3}){3})-\d{1,5}', tostring(extract('((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\\d{1,3}(?:\\.\\d{1,3}){3})-[0-9]+', 1, ClientIP)),
ClientIP matches regex @'((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\d{1,3}(?:\.\d{1,3}){3})', ClientIP,
"")
| where isnotempty(JoinedOnIp)
| extend OfficeTimeStamp = ElevationTime, UserPrincipalName = UserId
) on JoinedOnIp
// Rare and risky operations only happen within a certain time range of the successful sign-in
| where OfficeTimeStamp >= successSigninStart and datetime_diff('day', OfficeTimeStamp, successSigninEnd) <= timeBuffer
| extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: UserPrincipalName
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: JoinedOnIp
- entityType: IP
fieldMappings:
- identifier: Address
columnName: IPAddress
- entityType: IP
fieldMappings:
- identifier: Address
columnName: ClientIP
- entityType: CloudApplication
fieldMappings:
- identifier: AppId
columnName: ApplicationId
version: 1.0.1
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Research
support:
tier: Community
categories:
domains: [ "Security - Threat Intelligence", "Security - Others", "Identity" ]
Stages and Predicates
Parameters
let threshold = 100;
let timeRange = ago(7d);
let timeBuffer = 1;
Stage 1: source
SigninLogs
Stage 2: where
| where TimeGenerated > timeRange
Stage 3: where
| where ResultType == "50057"
Stage 4: summarize
| summarize StartTime = min(TimeGenerated), EndTime = max(TimeGenerated), disabledAccountLoginAttempts = count(),
disabledAccountsTargeted = dcount(UserPrincipalName), applicationsTargeted = dcount(AppDisplayName), disabledAccountSet = make_set(UserPrincipalName),
applicationSet = make_set(AppDisplayName) by IPAddress, AppId
Stage 5: sort
| order by disabledAccountLoginAttempts desc
Stage 6: join
| join kind=inner (
SigninLogs
| where TimeGenerated > timeRange
| where ResultType == 0
| summarize successSigninStart = min(TimeGenerated), successSigninEnd = max(TimeGenerated), successfulAccountSigninCount = dcount(UserPrincipalName), successfulAccountSigninSet = make_set(UserPrincipalName, 15) by IPAddress
| where successfulAccountSigninCount < threshold
) on IPAddress
Stage 7: where
| where successfulAccountSigninCount != 0
Stage 8: extend
| extend SuccessBeforeFailure = iff(successSigninStart >= StartTime and successSigninEnd <= EndTime, true, false)
SuccessBeforeFailure =successSigninStart >= "StartTime" and successSigninEnd <= "EndTime"truefalseStage 9: project
| project StartTime, EndTime, IPAddress, disabledAccountLoginAttempts, disabledAccountsTargeted, disabledAccountSet, applicationSet,
successfulAccountSigninCount, successfulAccountSigninSet, successSigninStart, successSigninEnd, AppId
Stage 10: sort
| order by disabledAccountLoginAttempts
Stage 11: mv-expand
| mvexpand successfulAccountSigninSet
Stage 12: extend
| extend JoinedOnIp = IPAddress
Stage 13: join
| join kind = inner (
OfficeActivity
| where TimeGenerated > timeRange
| where Operation in~ ( "Add-MailboxPermission", "Add-MailboxFolderPermission", "Set-Mailbox", "New-ManagementRoleAssignment", "New-InboxRule", "Set-InboxRule", "Set-TransportRule") and not(UserId has_any ('NT AUTHORITY\\SYSTEM (Microsoft.Exchange.ServiceHost)', 'NT AUTHORITY\\SYSTEM (w3wp)', 'devilfish-applicationaccount'))
| extend JoinedOnIp = case(
ClientIP matches regex @'\[((25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\]-\d{1,5}', tostring(extract('\\[([0-9]+\\.[0-9]+\\.[0-9]+)\\]-[0-9]+', 1, ClientIP)),
ClientIP matches regex @'\[((25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\]', tostring(extract('\\[([0-9]+\\.[0-9]+\\.[0-9]+)\\]', 1, ClientIP)),
ClientIP matches regex @'(((25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?))-\d{1,5}', tostring(extract('([0-9]+\\.[0-9]+\\.[0-9]+)-[0-9]+', 1, ClientIP)),
ClientIP matches regex @'((25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]2[0-4][0-9]|[01]?[0-9][0-9]?)', ClientIP,
ClientIP matches regex @'\[((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\d{1,3}(?:\.\d{1,3}){3})\]-\d{1,5}', tostring(extract('\\[((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\\d{1,3}(?:\\.\\d{1,3}){3})\\]-[0-9]+', 1, ClientIP)),
ClientIP matches regex @'\[((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\d{1,3}(?:\.\d{1,3}){3})\]', tostring(extract('\\[((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\\d{1,3}(?:\\.\\d{1,3}){3})\\]', 1, ClientIP)),
ClientIP matches regex @'((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\d{1,3}(?:\.\d{1,3}){3})-\d{1,5}', tostring(extract('((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\\d{1,3}(?:\\.\\d{1,3}){3})-[0-9]+', 1, ClientIP)),
ClientIP matches regex @'((?:[0-9a-fA-F]{1,4}::?){1,8}[0-9a-fA-F]{1,4}|\d{1,3}(?:\.\d{1,3}){3})', ClientIP,
"")
| where isnotempty(JoinedOnIp)
| extend OfficeTimeStamp = ElevationTime, UserPrincipalName = UserId
) on JoinedOnIp
Stage 14: where where OfficeTimeStamp - successSigninEnd <= 1d
| where OfficeTimeStamp >= successSigninStart and datetime_diff('day', OfficeTimeStamp, successSigninEnd) <= timeBuffer
Stage 15: extend
| extend AccountName = tostring(split(UserPrincipalName, "@")[0]), AccountUPNSuffix = tostring(split(UserPrincipalName, "@")[1])
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
UserId | match | NT AUTHORITY\\SYSTEM (Microsoft.Exchange.ServiceHost), NT AUTHORITY\\SYSTEM (w3wp), devilfish-applicationaccount |
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 |
|---|---|---|
JoinedOnIp | is_not_null | |
OfficeTimeStamp | ge |
|
Operation | in |
|
ResultType | eq |
|
TimeGenerated | gt |
|
successfulAccountSigninCount | lt |
|
successfulAccountSigninCount | ne |
|
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 |
|---|---|
AppId | project |
EndTime | project |
IPAddress | project |
StartTime | project |
applicationSet | project |
disabledAccountLoginAttempts | project |
disabledAccountSet | project |
disabledAccountsTargeted | project |
successSigninEnd | project |
successSigninStart | project |
successfulAccountSigninCount | project |
successfulAccountSigninSet | project |
JoinedOnIp | extend |
AccountName | extend |
AccountUPNSuffix | extend |