Detection rules › Kusto

Azure VM Run Command operation executed during suspicious login window

Severity
high
Time window
2d
Group by
Authorization, Caller, CorrelationId
Author
Microsoft Security Rearch
Source
github.com/Azure/Azure-Sentinel

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

TacticTechniques
Credential AccessT1212 Exploitation for Credential Access
Lateral MovementT1570 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
Threshold
ge UEBAWindowStart

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.

FieldKindValues
ActionUncommonlyPerformedByUsereq
  • True transforms: cased
Authorizationmatch
  • virtualMachines transforms: term
EventSourceeq
  • Azure AD transforms: cased
OperationNameValueeq
  • MICROSOFT.COMPUTE/VIRTUALMACHINES/RUNCOMMAND/ACTION
StartTimege
  • UEBAWindowStart
StartTimele
  • UEBAWindowEnd
UEBASourceIPLocationis_not_null
  • (no value, null check)
UserPrincipalNameis_not_null
  • (no value, null check)
list_ActivityStatusValuematch
  • Succeeded
  • Success

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
Callerproject
CallerIpAddressproject
EndTimeproject
StartTimeproject
Subscriptionproject
UEBAActionTypeproject
UEBAActivityInsightsproject
UEBAEventTimeproject
UEBASourceIPLocationproject
UEBAUsersInsightsproject
VirtualMachineNameproject
AccountNameextend
AccountUPNSuffixextend