Detection rules › Kusto
AD FS Remote HTTP Network Connection
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
| Tactic | Techniques |
|---|---|
| Collection | T1005 Data from Local System |
Event coverage
| Provider | Event | Title |
|---|---|---|
| Sysmon | Event ID 3 | Network connection |
| Sysmon | Event ID 18 | PipeEvent (Pipe Connected) |
Rule body kusto
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
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
);
Derived from ADFSServersList.
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)
References ADFS_Servers (defined above).
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
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
DestinationIp | in | 0:0:0:0:0:0:0:1, ::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 |
|---|---|---|
Computer | in |
|
DestinationPort | eq |
|
EventID | eq |
|
Initiated | eq |
|
process | eq |
|
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 |
|---|---|
RenderedDescription | extend |
EventData | extend |
Key | extend |
Value | extend |
RuleName | extend |
TechniqueId | extend |
TechniqueName | extend |
DestinationIp | extend |
DestinationPort | extend |
Image | extend |
Initiated | extend |
SourceIp | extend |
process | extend |
Operation | extend |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
AccountNTDomain | extend |
AccountName | extend |