Detection rules › Kusto

Gain Code Execution on ADFS Server via SMB + Remote Service or Scheduled Task

Status
available
Severity
medium
Time window
7d
Group by
SubjectLogonId, TargetLogonId
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 SMB and Remote Service or Scheduled Task.

MITRE ATT&CK coverage

TacticTechniques
Lateral MovementT1210 Exploitation of Remote Services

Event coverage

Rule body kusto

id: 12dcea64-bec2-41c9-9df2-9f28461b1295
name: Gain Code Execution on ADFS Server via SMB + Remote Service or Scheduled Task
description: |
   'This query detects instances where an attacker has gained the ability to execute code on an ADFS Server through SMB and Remote Service or Scheduled Task.'
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1d
queryPeriod: 7d
triggerOperator: gt
triggerThreshold: 0
status: Available
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 = (
  SecurityEvent
  | where TimeGenerated > ago(timeframe+lookback)
  | where EventID == 4688 and SubjectLogonId != "0x3e4"
  | where NewProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
  | distinct Computer
  );
  SecurityEvent
  | where TimeGenerated > ago(timeframe)
  | where Computer in~ (ADFS_Servers)
  | where Account !endswith "$"
  // Check for scheduled task events
  | where EventID in (4697, 4698, 4699, 4700, 4701, 4702)
  | extend EventDataParsed = parse_xml(EventData)
  | extend SubjectLogonId = tostring(EventDataParsed.EventData.Data[3]["#text"])
  // Check specifically for access to IPC$ share and PIPE\svcctl and PIPE\atsvc for Service Control Services and Schedule Control Services
  | union (
      SecurityEvent
      | where TimeGenerated > ago(timeframe)
      | where Computer in~ (ADFS_Servers)
      | where Account !endswith "$"
      | where EventID == 5145
      | where RelativeTargetName =~ "svcctl" or RelativeTargetName  =~ "atsvc"
  )
  // Check for lateral movement
  | join kind=inner
  (SecurityEvent
  | where TimeGenerated > ago(timeframe)
  | where Account !endswith "$"
  | where EventID == 4624 and LogonType == 3
  ) on $left.SubjectLogonId == $right.TargetLogonId
  | project TimeGenerated, Account, Computer, EventID, RelativeTargetName
  | 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, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: FullName
        columnName: Account
      - identifier: Name
        columnName: AccountName
      - identifier: NTDomain
        columnName: AccountNTDomain
  - entityType: Host
    fieldMappings:
      - identifier: FullName
        columnName: Computer
      - identifier: HostName
        columnName: HostName
      - identifier: DnsDomain
        columnName: HostNameDomain
version: 1.2.1
kind: Scheduled

Stages and Predicates

Parameters

let timeframe = 1d;
let lookback = 6d;

Let binding: ADFS_Servers

let ADFS_Servers = (
SecurityEvent
| where TimeGenerated > ago(timeframe+lookback)
| where EventID == 4688 and SubjectLogonId != "0x3e4"
| where NewProcessName has "Microsoft.IdentityServer.ServiceHost.exe"
| distinct Computer
);

Derived from timeframe, lookback.

Stage 1: source

let ADFS_Servers

Stage 2: source

SecurityEvent

Stage 3: where

| where TimeGenerated > ago(timeframe)

Stage 4: where

| where Computer in~ (ADFS_Servers)

References ADFS_Servers (defined above).

Stage 5: where

| where Account !endswith "$"

Stage 6: where

| where EventID in (4697, 4698, 4699, 4700, 4701, 4702)

Stage 7: extend

| extend EventDataParsed = parse_xml(EventData)

Stage 8: extend

| extend SubjectLogonId = tostring(EventDataParsed.EventData.Data[3]["#text"])

Stage 9: union

| union

Stage 10: source

SecurityEvent

Stage 11: where

| where TimeGenerated > ago(timeframe)

Stage 12: where

| where Computer in~ (ADFS_Servers)

References ADFS_Servers (defined above).

Stage 13: where

| where Account !endswith "$"

Stage 14: where

| where EventID == 5145

Stage 15: where

| where RelativeTargetName =~ "svcctl" or RelativeTargetName  =~ "atsvc"

Stage 16: join

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

Stage 17: project

| project TimeGenerated, Account, Computer, EventID, RelativeTargetName

Stage 18: extend (4 consecutive steps)

| 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, @'\')[1]), AccountNTDomain = tostring(split(Account, @'\')[0])

Exclusions

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

FieldKindExcluded values
Accountends_with$
Accountends_with$
Accountends_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
Computerin
  • ADFS_Servers corpus 5 (kusto 5)
EventIDeq
  • 4624 transforms: cased corpus 25 (splunk 13, kusto 8, chronicle 4)
  • 5145 transforms: cased corpus 18 (splunk 16, kusto 2)
EventIDin
  • 4697 transforms: cased corpus 2 (splunk 2)
  • 4698 transforms: cased corpus 14 (splunk 14)
  • 4699 transforms: cased
  • 4700 transforms: cased
  • 4701 transforms: cased
  • 4702 transforms: cased
LogonTypeeq
  • 3 transforms: cased corpus 40 (splunk 13, sigma 12, elastic 9, kusto 6)
RelativeTargetNameeq
  • atsvc corpus 4 (sigma 3, kusto 1)
  • svcctl corpus 6 (sigma 5, kusto 1)

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
Accountproject
Computerproject
EventIDproject
RelativeTargetNameproject
TimeGeneratedproject
timestampextend
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend