Detection rules › Kusto

Gain Code Execution on ADFS Server via Remote WMI Execution

Severity
medium
Time window
7d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This query detects instances where an attacker has gained the ability to execute code on an ADFS Server through remote WMI Execution. In order to use this query you need to be collecting Sysmon EventIDs 19, 20, and 21. If you do not have Sysmon data in your workspace this query will raise an error stating: Failed to resolve scalar expression named "[@Name]" For more on how WMI was used in Solorigate see https://www.microsoft.com/security/blog/2021/01/20/deep-dive-into-the-solorigate-second-stage-activation-from-sunburst-to-teardrop-and-raindrop/. The query contains some features from the following detections to look for potentially malicious ADFS activity. See them for more details. - ADFS DKM Master Key Export: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/MultipleDataSources/ADFS-DKM-MasterKey-Export.yaml

MITRE ATT&CK coverage

TacticTechniques
Lateral MovementT1210 Exploitation of Remote Services

Event coverage

Rule body kusto

id: 0bd65651-1404-438b-8f63-eecddcec87b4
name: Gain Code Execution on ADFS Server via Remote WMI Execution
description: |
   'This query detects instances where an attacker has gained the ability to execute code on an ADFS Server through remote WMI Execution.
   In order to use this query you need to be collecting Sysmon EventIDs 19, 20, and 21.
   If you do not have Sysmon data in your workspace this query will raise an error stating:
        Failed to resolve scalar expression named "[@Name]"
   For more on how WMI was used in Solorigate see https://www.microsoft.com/security/blog/2021/01/20/deep-dive-into-the-solorigate-second-stage-activation-from-sunburst-to-teardrop-and-raindrop/.
   The query contains some features from the following detections to look for potentially malicious ADFS activity. See them for more details.
   - ADFS DKM Master Key Export: https://github.com/Azure/Azure-Sentinel/blob/master/Detections/MultipleDataSources/ADFS-DKM-MasterKey-Export.yaml'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes: 
      - SecurityEvents 
  - connectorId: WindowsForwardedEvents
    dataTypes: 
      - WindowsEvent 
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - LateralMovement
relevantTechniques:
  - T1210
tags:
  - Solorigate
  - NOBELIUM
query: |
  let timeframe = 1d;
  // Adjust for a longer timeframe for identifying ADFS Servers
  let lookback = 6d;
  // Identify ADFS Servers
  let ADFS_Servers = ( union isfuzzy=true
  ( Event
  | where TimeGenerated > ago(timeframe+lookback)
  | where Source == "Microsoft-Windows-Sysmon"
  | where EventID == 1
  | extend EventData = parse_xml(EventData).DataItem.EventData.Data
  | mv-expand bagexpansion=array EventData
  | evaluate bag_unpack(EventData)
  | extend Key=tostring(['@Name']), Value=['#text']
  | evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
  | extend process = split(Image, '\\', -1)[-1]
  | where process =~ "Microsoft.IdentityServer.ServiceHost.exe"
  | distinct Computer
  ),
  ( SecurityEvent
  | where TimeGenerated > ago(timeframe+lookback)
  | where EventID == 4688 and SubjectLogonId != "0x3e4"
  | where ProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
  | distinct Computer
  ),
  (WindowsEvent
  | where TimeGenerated > ago(timeframe+lookback)
  | where EventID == 4688 and EventData has "0x3e4" and EventData has "Microsoft.IdentityServer.ServiceHost.exe"
  | extend SubjectLogonId  = tostring(EventData.SubjectLogonId)
  | where SubjectLogonId != "0x3e4"
  | extend ProcessName  = tostring(EventData.ProcessName)
  | where ProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
  | distinct Computer
  )
  | distinct Computer);
  (union isfuzzy=true
  (
  SecurityEvent
  | where EventID == 4688
  | where TimeGenerated > ago(timeframe)
  | where Computer in~ (ADFS_Servers)
  | where ParentProcessName has 'wmiprvse.exe'
  // Looking for rundll32.exe is based on intel from the blog linked in the description
  // This can be commented out or altered to filter out known internal uses
  | where CommandLine has_any ('rundll32') 
  | project TimeGenerated, TargetAccount, CommandLine, Computer, Account, TargetLogonId
  | extend timestamp = TimeGenerated
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])
  // Search for recent logons to identify lateral movement
  | join kind= inner
  (SecurityEvent
  | where TimeGenerated > ago(timeframe)
  | where EventID == 4624 and LogonType == 3
  | where Account !endswith "$"
  | project TargetLogonId
  ) on TargetLogonId
  ),
  (
  WindowsEvent
  | where EventID == 4688
  | where TimeGenerated > ago(timeframe)
  | where Computer in~ (ADFS_Servers)
  | where EventData has 'wmiprvse.exe' and EventData has_any ('rundll32') 
  | extend ParentProcessName = tostring(EventData.ParentProcessName)
  | where ParentProcessName has 'wmiprvse.exe'
  // Looking for rundll32.exe is based on intel from the blog linked in the description
  // This can be commented out or altered to filter out known internal uses
  | extend CommandLine = tostring(EventData.CommandLine)
  | where CommandLine has_any ('rundll32') 
  | extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
  | extend Account = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
  | extend TargetLogonId = tostring(EventData.TargetLogonId)
  | project TimeGenerated, TargetAccount, CommandLine, Computer, Account, TargetLogonId
  | extend timestamp = TimeGenerated
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])
  // Search for recent logons to identify lateral movement
  | join kind= inner
  (WindowsEvent
  | where TimeGenerated > ago(timeframe)
  | where EventID == 4624 
  | extend  LogonType = tostring(EventData.LogonType)
  | where LogonType == 3
  | extend Account = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
  | where Account !endswith "$"
  | extend TargetLogonId = tostring(EventData.TargetLogonId)
  | project TargetLogonId
  ) on TargetLogonId
  ),
  (
  Event
  | where TimeGenerated > ago(timeframe)
  | where Source == "Microsoft-Windows-Sysmon"
  // Check for WMI Events
  | where Computer in~ (ADFS_Servers) and EventID in (19, 20, 21)
  | extend EventData = parse_xml(EventData).DataItem.EventData.Data
  | mv-expand bagexpansion=array EventData
  | evaluate bag_unpack(EventData)
  | extend Key=tostring(['@Name']), Value=['#text']
  | evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
  | project TimeGenerated, EventType, Image, Computer, UserName
  | extend timestamp = TimeGenerated
  | extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
  | extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
  | extend AccountName = tostring(split(UserName, "\\")[0]), AccountNTDomain = tostring(split(UserName, "\\")[1])
  )
  )
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: UserName
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: NTDomain
        columnName: HostNameDomain
version: 1.1.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Stage 0: let

let timeframe = 1d;
let lookback = 6d;
let ADFS_Servers = ( union isfuzzy=true
( Event
| where TimeGenerated > ago(timeframe+lookback)
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 1
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key=tostring(['@Name']), Value=['#text']
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)
| extend process = split(Image, '\\', -1)[-1]
| where process =~ "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
),
( SecurityEvent
| where TimeGenerated > ago(timeframe+lookback)
| where EventID == 4688 and SubjectLogonId != "0x3e4"
| where ProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
),
(WindowsEvent
| where TimeGenerated > ago(timeframe+lookback)
| where EventID == 4688 and EventData has "0x3e4" and EventData has "Microsoft.IdentityServer.ServiceHost.exe"
| extend SubjectLogonId  = tostring(EventData.SubjectLogonId)
| where SubjectLogonId != "0x3e4"
| extend ProcessName  = tostring(EventData.ProcessName)
| where ProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
)
| distinct Computer);

Stage 1: source

let ADFS_Servers

Stage 2: union

union isfuzzy=true

Stage 3: source

SecurityEvent

Stage 4: where

| where EventID == 4688

Stage 5: where time_window=86400s

| where TimeGenerated > ago(timeframe)

Stage 6: where

| where Computer in~ (ADFS_Servers)

Stage 7: where

| where ParentProcessName has 'wmiprvse.exe'

Stage 8: where

| where CommandLine has_any ('rundll32')

Stage 9: project

| project TimeGenerated, TargetAccount, CommandLine, Computer, Account, TargetLogonId

Stage 10: extend

| extend timestamp = TimeGenerated

Stage 11: extend

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))

Stage 12: extend

| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)

Stage 13: extend

| extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])

Stage 14: join

| join kind= inner
(SecurityEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4624 and LogonType == 3
| where Account !endswith "$"
| project TargetLogonId
) on TargetLogonId

Stage 15: source

WindowsEvent

Stage 16: where

| where EventID == 4688

Stage 17: where time_window=86400s

| where TimeGenerated > ago(timeframe)

Stage 18: where

| where Computer in~ (ADFS_Servers)

Stage 19: where

| where EventData has 'wmiprvse.exe' and EventData has_any ('rundll32')

Stage 20: extend

| extend ParentProcessName = tostring(EventData.ParentProcessName)

Stage 21: where

| where ParentProcessName has 'wmiprvse.exe'

Stage 22: extend

| extend CommandLine = tostring(EventData.CommandLine)

Stage 23: where

| where CommandLine has_any ('rundll32')

Stage 24: extend

| extend TargetAccount = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)

Stage 25: extend

| extend Account = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)

Stage 26: extend

| extend TargetLogonId = tostring(EventData.TargetLogonId)

Stage 27: project

| project TimeGenerated, TargetAccount, CommandLine, Computer, Account, TargetLogonId

Stage 28: extend

| extend timestamp = TimeGenerated

Stage 29: extend

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))

Stage 30: extend

| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)

Stage 31: extend

| extend AccountName = tostring(split(Account, "\\")[0]), AccountNTDomain = tostring(split(Account, "\\")[1])

Stage 32: join

| join kind= inner
(WindowsEvent
| where TimeGenerated > ago(timeframe)
| where EventID == 4624 
| extend  LogonType = tostring(EventData.LogonType)
| where LogonType == 3
| extend Account = strcat(EventData.TargetDomainName,"\\", EventData.TargetUserName)
| where Account !endswith "$"
| extend TargetLogonId = tostring(EventData.TargetLogonId)
| project TargetLogonId
) on TargetLogonId

Stage 33: source

Event

Stage 34: where time_window=86400s

| where TimeGenerated > ago(timeframe)

Stage 35: where

| where Source == "Microsoft-Windows-Sysmon"

Stage 36: where

| where Computer in~ (ADFS_Servers) and EventID in (19, 20, 21)

Stage 37: extend

| extend EventData = parse_xml(EventData).DataItem.EventData.Data

Stage 38: mv-expand

| mv-expand bagexpansion=array EventData

Stage 39: evaluate

| evaluate bag_unpack(EventData)

Stage 40: extend

| extend Key=tostring(['@Name']), Value=['#text']

Stage 41: evaluate

| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, UserName, RenderedDescription, MG, ManagementGroupName, Type, _ResourceId)

Stage 42: project

| project TimeGenerated, EventType, Image, Computer, UserName

Stage 43: extend

| extend timestamp = TimeGenerated

Stage 44: extend

| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))

Stage 45: extend

| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)

Stage 46: extend

| extend AccountName = tostring(split(UserName, "\\")[0]), AccountNTDomain = tostring(split(UserName, "\\")[1])

Exclusions

Top-level NOT(...) conjuncts: predicates this rule actively suppresses.

StageFieldKindExcluded values
14Accountends_with$
32Accountends_with$

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
CommandLinematch
  • rundll32 corpus 26 (sigma 23, chronicle 2, kusto 1)
Computerin
  • ADFS_Servers corpus 5 (kusto 5)
EventDatamatch
  • rundll32
  • wmiprvse.exe
EventIDeq
  • 4624 transforms: cased corpus 26 (splunk 13, kusto 9, chronicle 4)
  • 4688 transforms: cased corpus 312 (splunk 283, kusto 29)
EventIDin
  • 19
  • 20
  • 21
LogonTypeeq
  • 3 transforms: cased corpus 39 (splunk 13, sigma 12, elastic 9, kusto 5)
ParentProcessNamematch
  • wmiprvse.exe

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
Computerproject
EventTypeproject
Imageproject
TimeGeneratedproject
UserNameproject
timestampextend
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend