Detection rules › Kusto

AD FS Remote HTTP Network Connection

Status
available
Severity
medium
Time window
1d
Group by
Computer
Source
github.com/Azure/Azure-Sentinel

This detection uses Sysmon events (NetworkConnect events) to detect incoming network traffic on port 80 on AD FS servers. This could be a sign of a threat actor trying to use replication services on the AD FS server to get its configuration settings and extract sensitive information such as AD FS certificates. In order to use this query you need to enable Sysmon telemetry on the AD FS Server. Reference: https://twitter.com/OTR_Community/status/1387038995016732672

MITRE ATT&CK coverage

TacticTechniques
Collection

Telemetry coverage

Rule body

id: d57c33a9-76b9-40e0-9dfa-ff0404546410
name: AD FS Remote HTTP Network Connection
description: |
  'This detection uses Sysmon events (NetworkConnect events) to detect incoming network traffic on port 80 on AD FS servers. This could be a sign of a threat actor trying to use replication services on the AD FS server to get its configuration settings and extract sensitive information such as AD FS certificates.
  In order to use this query you need to enable Sysmon telemetry on the AD FS Server.
  Reference: https://twitter.com/OTR_Community/status/1387038995016732672
  '
severity: Medium
requiredDataConnectors:
  - connectorId: SecurityEvents
    dataTypes:
      - SecurityEvent
  - connectorId: WindowsSecurityEvents
    dataTypes:
      - SecurityEvent
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
status: Available
tactics:
  - Collection
relevantTechniques:
  - T1005
tags:
  - SimuLand
query: |
  // Adjust this to use a longer timeframe to identify ADFS servers
  //let lookback = 0d;
  // Adjust this to adjust detection timeframe
  //let timeframe = 1d;
  // Filter out other servers in the AD FS farm
  let ADFSServersList = dynamic(["ADFS02.domain.com","ADFS03.domain.com"]);
  // Start by identifying ADFS servers to reduce FP chance
  let ADFS_Servers = (
  Event
  //| where TimeGenerated > ago(timeframe+lookback)
  | where Source == "Microsoft-Windows-Sysmon"
  | where EventID == 18
  | where Computer !in (ADFSServersList)
  | extend EventData = parse_xml(EventData).DataItem.EventData.Data
  | mv-expand bagexpansion=array EventData
  | evaluate bag_unpack(EventData)
  | extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
  | evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, MG, ManagementGroupName, _ResourceId)
  | extend Image = column_ifexists("Image", "")
  | extend process = split(Image, '\\', -1)[-1]
  | where process =~ "Microsoft.IdentityServer.ServiceHost.exe"
  | summarize by Computer
  );
  // Look for ADFS servers receiving connections over port 80
  Event
  //| where TimeGenerated > ago(timeframe)
  | where Source == "Microsoft-Windows-Sysmon"
  | where Computer in~ (ADFS_Servers)
  | extend RenderedDescription = tostring(split(RenderedDescription, ":")[0])
  | extend EventData = parse_xml(EventData).DataItem.EventData.Data
  | mv-expand bagexpansion=array EventData
  | evaluate bag_unpack(EventData)
  | extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
  | evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, RenderedDescription, MG, ManagementGroupName, _ResourceId)
  | extend RuleName = column_ifexists("RuleName", ""), TechniqueId = column_ifexists("TechniqueId", ""),  TechniqueName = column_ifexists("TechniqueName", "")
  | parse RuleName with * 'technique_id=' TechniqueId ',' * 'technique_name=' TechniqueName
  | where EventID == 3
  // Look for endpoints connecting to the AD FS server over port 80
  | extend DestinationPort = column_ifexists("DestinationPort", ""), Image = column_ifexists("Image", ""), Initiated = column_ifexists("Initiated", ""), SourceIp = column_ifexists("DestinationIp", ""), DestinationIp = column_ifexists("DestinationIp", "")
  | where DestinationPort == 80
  | extend process = split(Image, '\\', -1)[-1]
  // Look for the System process receiving connections
  | where process == 'System' and Initiated == 'false'
  | where DestinationIp !in ('::1','0:0:0:0:0:0:0:1')
  | extend Operation = RenderedDescription
  | project-reorder TimeGenerated, Operation, Image, Computer, UserName
  | 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, @'\')[1]), AccountNTDomain = tostring(split(UserName, @'\')[0])
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: DnsDomain
        columnName: HostNameDomain
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIp
version: 1.0.2
kind: Scheduled

Stages and Predicates

Parameters

let ADFSServersList = dynamic(["ADFS02.domain.com","ADFS03.domain.com"]);

Let binding: ADFS_Servers used in Stages 1, 4

let ADFS_Servers = (
Event
| where Source == "Microsoft-Windows-Sysmon"
| where EventID == 18
| where Computer !in (ADFSServersList)
| extend EventData = parse_xml(EventData).DataItem.EventData.Data
| mv-expand bagexpansion=array EventData
| evaluate bag_unpack(EventData)
| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")
| evaluate pivot(Key, any(Value), TimeGenerated, Source, EventLog, Computer, EventLevel, EventLevelName, EventID, UserName, MG, ManagementGroupName, _ResourceId)
| extend Image = column_ifexists("Image", "")
| extend process = split(Image, '\\', -1)[-1]
| where process =~ "Microsoft.IdentityServer.ServiceHost.exe"
| summarize by Computer
);

Stage 1: source

let ADFS_Servers

Stage 2: source

Event

Stage 3: where

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

Stage 4: where

| where Computer in~ (ADFS_Servers)

Stage 5: extend

| extend RenderedDescription = tostring(split(RenderedDescription, ":")[0])

Stage 6: extend

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

Stage 7: mv-expand

| mv-expand bagexpansion=array EventData

Stage 8: evaluate

| evaluate bag_unpack(EventData)

Stage 9: extend

| extend Key = tostring(column_ifexists('@Name', "")), Value = column_ifexists('#text', "")

Stage 10: evaluate

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

Stage 11: extend

| extend RuleName = column_ifexists("RuleName", ""), TechniqueId = column_ifexists("TechniqueId", ""),  TechniqueName = column_ifexists("TechniqueName", "")

Stage 12: parse

| parse RuleName with * 'technique_id=' TechniqueId ',' * 'technique_name=' TechniqueName

Stage 13: where

| where EventID == 3

Stage 14: extend

| extend DestinationPort = column_ifexists("DestinationPort", ""), Image = column_ifexists("Image", ""), Initiated = column_ifexists("Initiated", ""), SourceIp = column_ifexists("DestinationIp", ""), DestinationIp = column_ifexists("DestinationIp", "")

Stage 15: where

| where DestinationPort == 80

Stage 16: extend

| extend process = split(Image, '\\', -1)[-1]

Stage 17: where

| where process == 'System' and Initiated == 'false'

Stage 18: where

| where DestinationIp !in ('::1','0:0:0:0:0:0:0:1')

Stage 19: extend

| extend Operation = RenderedDescription

Stage 20: project-reorder

| project-reorder TimeGenerated, Operation, Image, Computer, UserName

Stage 21: extend (3 consecutive steps)

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

Exclusions

The rule actively suppresses these predicates.

FieldKindExcluded valuesSearch
DestinationIpin0:0:0:0:0:0:0:1, ::1excludes:DestinationIp field:"DestinationIp" value:"0:0:0:0:0:0:0:1" field:"DestinationIp" value:"::1"

Indicators

These rows show field, operator, and value matches.

FieldKindValuesSearch
Computerin
  • ADFS_Servers corpus 5 (kusto 5)
field:"Computer" kind:in value:"ADFS_Servers"
DestinationPorteq
  • 80 corpus 11 (sigma 6, elastic 3, kusto 2)
field:"DestinationPort" kind:eq value:"80"
EventIDeq
  • 3 corpus 24 (splunk 14, kusto 8, chronicle 2)
field:"EventID" kind:eq value:"3"
Initiatedeq
  • false corpus 2 (sigma 1, kusto 1)
field:"Initiated" kind:eq value:"false"
processeq
  • System
field:"CommandLine" kind:eq value:"System"

Output fields

These fields are emitted when the rule matches.

FieldSource
RenderedDescriptionextend
EventDataextend
Keyextend
Valueextend
RuleNameextend
TechniqueIdextend
TechniqueNameextend
DestinationIpextend
DestinationPortextend
Imageextend
Initiatedextend
SourceIpextend
processextend
Operationextend
DomainIndexextend
HostNameextend
HostNameDomainextend
AccountNTDomainextend
AccountNameextend