Detection rules › Kusto
AD FS Remote Auth Sync Connection
This detection uses Security events from the "AD FS Auditing" provider to detect suspicious authentication events on an AD FS server. The results then get correlated with events from the Windows Filtering Platform (WFP) to detect suspicious incoming network traffic on port 80 on the AD FS server. 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 AD FS auditing on the AD FS Server. References: https://docs.microsoft.com/windows-server/identity/ad-fs/troubleshooting/ad-fs-tshoot-logging https://twitter.com/OTR_Community/status/1387038995016732672
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Collection | T1005 Data from Local System |
Event coverage
| Provider | Event | Title |
|---|---|---|
| Security-Auditing | Event ID 412 | AD FS authentication failure. |
| Security-Auditing | Event ID 501 | AD FS proxy authentication request. |
| Security-Auditing | Event ID 5156 | The Windows Filtering Platform has permitted a connection. |
Rule body kusto
id: 2f4165a6-c4fb-4e94-861e-37f1b4d6c0e6
name: AD FS Remote Auth Sync Connection
description: |
'This detection uses Security events from the "AD FS Auditing" provider to detect suspicious authentication events on an AD FS server. The results then get correlated with events from the Windows Filtering Platform (WFP) to detect suspicious incoming network traffic on port 80 on the AD FS server.
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 AD FS auditing on the AD FS Server.
References:
https://docs.microsoft.com/windows-server/identity/ad-fs/troubleshooting/ad-fs-tshoot-logging
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;
// SamAccountName of AD FS Service Account. Filter on the use of a specific AD FS user account
//let adfsuser = 'adfsadmin';
// Identify ADFS Servers
let ADFS_Servers = (
SecurityEvent
//| where TimeGenerated > ago(timeframe+lookback)
| where EventSourceName == 'AD FS Auditing'
| distinct Computer
);
SecurityEvent
//| where TimeGenerated > ago(timeframe)
| where Computer in~ (ADFS_Servers)
// A token of type 'http://schemas.microsoft.com/ws/2006/05/servicemodel/tokens/SecureConversation'
// for relying party '-' was successfully authenticated.
| where EventID == 412
| extend EventData = parse_xml(EventData).EventData.Data
| extend InstanceId = tostring(EventData[0])
| join kind=inner
(
SecurityEvent
//| where TimeGenerated > ago(timeframe)
| where Computer in~ (ADFS_Servers)
// Events to identify caller identity from event 412
| where EventID == 501
| extend EventData = parse_xml(EventData).EventData.Data
| where tostring(EventData[1]) contains 'identity/claims/name'
| extend InstanceId = tostring(EventData[0])
| extend ClaimsName = tostring(EventData[2])
// Filter on the use of a specific AD FS user account
//| where ClaimsName contains adfsuser
)
on $left.InstanceId == $right.InstanceId
| join kind=inner
(
SecurityEvent
| where EventID == 5156
| where Computer in~ (ADFS_Servers)
| extend EventData = parse_xml(EventData).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, Computer, EventID)
| extend DestPort = column_ifexists("DestPort", ""),
Direction = column_ifexists("Direction", ""),
Application = column_ifexists("Application", ""),
DestAddress = column_ifexists("DestAddress", ""),
SourceAddress = column_ifexists("SourceAddress", ""),
SourcePort = column_ifexists("SourcePort", "")
// Look for inbound connections from endpoints on port 80
| where DestPort == 80 and Direction == '%%14592' and Application == 'System'
| where DestAddress !in ('::1','0:0:0:0:0:0:0:1')
)
on $left.Computer == $right.Computer
| project TimeGenerated, Computer, ClaimsName, SourceAddress, SourcePort
| extend HostName = tostring(split(Computer, ".")[0]), DomainIndex = toint(indexof(Computer, '.'))
| extend HostNameDomain = iff(DomainIndex != -1, substring(Computer, DomainIndex + 1), Computer)
| extend AccountName = tostring(split(ClaimsName, @'\')[1]), AccountNTDomain = tostring(split(ClaimsName, @'\')[0])
entityMappings:
- entityType: Account
fieldMappings:
- identifier: FullName
columnName: ClaimsName
- 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: SourceAddress
version: 1.0.4
kind: Scheduled
Stages and Predicates
Let binding: ADFS_Servers
let ADFS_Servers = (
SecurityEvent
| where EventSourceName == 'AD FS Auditing'
| distinct Computer
);
Stage 1: source
SecurityEvent
Stage 2: where
| where Computer in~ (ADFS_Servers)
References ADFS_Servers (defined above).
Stage 3: where
| where EventID == 412
Stage 4: extend
| extend EventData = parse_xml(EventData).EventData.Data
Stage 5: extend
| extend InstanceId = tostring(EventData[0])
Stage 6: join
| join kind=inner
(
SecurityEvent
| where Computer in~ (ADFS_Servers)
| where EventID == 501
| extend EventData = parse_xml(EventData).EventData.Data
| where tostring(EventData[1]) contains 'identity/claims/name'
| extend InstanceId = tostring(EventData[0])
| extend ClaimsName = tostring(EventData[2])
)
on $left.InstanceId == $right.InstanceId
Stage 7: join
| join kind=inner
(
SecurityEvent
| where EventID == 5156
| where Computer in~ (ADFS_Servers)
| extend EventData = parse_xml(EventData).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, Computer, EventID)
| extend DestPort = column_ifexists("DestPort", ""),
Direction = column_ifexists("Direction", ""),
Application = column_ifexists("Application", ""),
DestAddress = column_ifexists("DestAddress", ""),
SourceAddress = column_ifexists("SourceAddress", ""),
SourcePort = column_ifexists("SourcePort", "")
| where DestPort == 80 and Direction == '%%14592' and Application == 'System'
| where DestAddress !in ('::1','0:0:0:0:0:0:0:1')
)
on $left.Computer == $right.Computer
Stage 8: project
| project TimeGenerated, Computer, ClaimsName, SourceAddress, SourcePort
Stage 9: 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(ClaimsName, @'\')[1]), AccountNTDomain = tostring(split(ClaimsName, @'\')[0])
Exclusions
Top-level NOT(...) conjuncts: predicates this rule actively suppresses.
| Field | Kind | Excluded values |
|---|---|---|
DestAddress | 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 |
|---|---|---|
Application | eq |
|
Computer | in |
|
DestPort | eq |
|
Direction | eq |
|
EventData[1] | contains |
|
EventID | 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 |
|---|---|
ClaimsName | project |
Computer | project |
SourceAddress | project |
SourcePort | project |
TimeGenerated | project |
DomainIndex | extend |
HostName | extend |
HostNameDomain | extend |
AccountNTDomain | extend |
AccountName | extend |