Detection rules › Kusto
Azure VM Run Command operation executed during suspicious login window
Identifies when the Azure Run Command operation is executed by a UserPrincipalName and IP Address that has resulted in a recent user entity behaviour alert.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | T1212 Exploitation for Credential Access |
| Lateral Movement | T1570 Lateral Tool Transfer |
Event coverage
Rule body kusto
id: 11bda520-a965-4654-9a45-d09f372f71aa
name: Azure VM Run Command operation executed during suspicious login window
description: |
'Identifies when the Azure Run Command operation is executed by a UserPrincipalName and IP Address that has resulted in a recent user entity behaviour alert.'
severity: High
requiredDataConnectors:
- connectorId: AzureActivity
dataTypes:
- AzureActivity
- connectorId: BehaviorAnalytics
dataTypes:
- BehaviorAnalytics
queryFrequency: 1d
queryPeriod: 2d
triggerOperator: gt
triggerThreshold: 0
tactics:
- LateralMovement
- CredentialAccess
relevantTechniques:
- T1570
- T1212
query: |
AzureActivity
// Isolate run command actions
| where OperationNameValue =~ "MICROSOFT.COMPUTE/VIRTUALMACHINES/RUNCOMMAND/ACTION"
// Confirm that the operation impacted a virtual machine
| where Authorization has "virtualMachines"
// Each runcommand operation consists of three events when successful, Started, Accepted (or Rejected), Successful (or Failed).
| summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), max(CallerIpAddress), make_list(ActivityStatusValue) by CorrelationId, Authorization, Caller
// Limit to Run Command executions that Succeeded
| where list_ActivityStatusValue has_any ("Success", "Succeeded")
// Extract data from the Authorization field
| extend Authorization_d = parse_json(Authorization)
| extend Scope = Authorization_d.scope
| extend Scope_s = split(Scope, "/")
| extend Subscription = tostring(Scope_s[2])
| extend VirtualMachineName = tostring(Scope_s[-1])
| project StartTime, EndTime, Subscription, VirtualMachineName, CorrelationId, Caller, CallerIpAddress=max_CallerIpAddress
// Create a join key using the Caller (UPN)
| extend joinkey = tolower(Caller)
// Join the Run Command actions to UEBA data
| join kind = inner (
BehaviorAnalytics
// We are specifically interested in unusual logins
| where EventSource == "Azure AD" and ActivityInsights.ActionUncommonlyPerformedByUser == "True"
| project UEBAEventTime=TimeGenerated, UEBAActionType=ActionType, UserPrincipalName, UEBASourceIPLocation=SourceIPLocation, UEBAActivityInsights=ActivityInsights, UEBAUsersInsights=UsersInsights
| where isnotempty(UserPrincipalName) and isnotempty(UEBASourceIPLocation)
| extend joinkey = tolower(UserPrincipalName)
) on joinkey
// Create a window around the UEBA event times, check to see if the Run Command action was performed within them
| extend UEBAWindowStart = UEBAEventTime - 1h, UEBAWindowEnd = UEBAEventTime + 6h
| where StartTime between (UEBAWindowStart .. UEBAWindowEnd)
| project StartTime, EndTime, Subscription, VirtualMachineName, Caller, CallerIpAddress, UEBAEventTime, UEBAActionType, UEBASourceIPLocation, UEBAActivityInsights, UEBAUsersInsights
| extend AccountName = tostring(split(Caller, "@")[0]), AccountUPNSuffix = tostring(split(Caller, "@")[1])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: Caller
- identifier: Name
columnName: AccountName
- identifier: UPNSuffix
columnName: AccountUPNSuffix
- entityType: IP
fieldMappings:
- identifier: Address
columnName: CallerIpAddress
version: 1.0.10
kind: Scheduled
metadata:
source:
kind: Community
author:
name: Microsoft Security Rearch
support:
tier: Community
categories:
domains: [ "Security - Others", "Platform" ]
Stages and Predicates
Stage 1: source
AzureActivity
Stage 2: where
| where OperationNameValue =~ "MICROSOFT.COMPUTE/VIRTUALMACHINES/RUNCOMMAND/ACTION"
Stage 3: where
| where Authorization has "virtualMachines"
Stage 4: summarize
| summarize StartTime=min(TimeGenerated), EndTime=max(TimeGenerated), max(CallerIpAddress), make_list(ActivityStatusValue) by CorrelationId, Authorization, Caller
Stage 5: where
| where list_ActivityStatusValue has_any ("Success", "Succeeded")
Stage 6: extend (5 consecutive steps)
| extend Authorization_d = parse_json(Authorization)
| extend Scope = Authorization_d.scope
| extend Scope_s = split(Scope, "/")
| extend Subscription = tostring(Scope_s[2])
| extend VirtualMachineName = tostring(Scope_s[-1])
Stage 7: project
| project StartTime, EndTime, Subscription, VirtualMachineName, CorrelationId, Caller, CallerIpAddress=max_CallerIpAddress
Stage 8: extend
| extend joinkey = tolower(Caller)
Stage 9: join
| join kind = inner (
BehaviorAnalytics
| where EventSource == "Azure AD" and ActivityInsights.ActionUncommonlyPerformedByUser == "True"
| project UEBAEventTime=TimeGenerated, UEBAActionType=ActionType, UserPrincipalName, UEBASourceIPLocation=SourceIPLocation, UEBAActivityInsights=ActivityInsights, UEBAUsersInsights=UsersInsights
| where isnotempty(UserPrincipalName) and isnotempty(UEBASourceIPLocation)
| extend joinkey = tolower(UserPrincipalName)
) on joinkey
Stage 10: extend
| extend UEBAWindowStart = UEBAEventTime - 1h, UEBAWindowEnd = UEBAEventTime + 6h
Stage 11: where
| where StartTime between (UEBAWindowStart .. UEBAWindowEnd)
Stage 12: project
| project StartTime, EndTime, Subscription, VirtualMachineName, Caller, CallerIpAddress, UEBAEventTime, UEBAActionType, UEBASourceIPLocation, UEBAActivityInsights, UEBAUsersInsights
Stage 13: extend
| extend AccountName = tostring(split(Caller, "@")[0]), AccountUPNSuffix = tostring(split(Caller, "@")[1])
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 |
|---|---|---|
ActionUncommonlyPerformedByUser | eq |
|
Authorization | match |
|
EventSource | eq |
|
OperationNameValue | eq |
|
StartTime | ge |
|
StartTime | le |
|
UEBASourceIPLocation | is_not_null | |
UserPrincipalName | is_not_null | |
list_ActivityStatusValue | match |
|
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 |
|---|---|
Caller | project |
CallerIpAddress | project |
EndTime | project |
StartTime | project |
Subscription | project |
UEBAActionType | project |
UEBAActivityInsights | project |
UEBAEventTime | project |
UEBASourceIPLocation | project |
UEBAUsersInsights | project |
VirtualMachineName | project |
AccountName | extend |
AccountUPNSuffix | extend |