Detection rules › Kusto
Detect Multiple Hello for Business PRT tokens being used simultaneously for one device.
This detection rule tries to find multiple PRT tokens being used simultaneously for one device. This might indicate that an attacker was able to request a new PRT on a second device using exxported Windows Hello for Business keys. More information about the attack scenario can be found in the references.
MITRE ATT&CK coverage
| Tactic | Techniques |
|---|---|
| Credential Access | T1606 Forge Web Credentials |
References
Rule body yaml
// Get the Sign-in logs we want to query
let base = materialize(
SigninLogs
| where Timestamp > ago(1d)
);
// Get all the WHfB signins by looking at the authentication method and incomming token
let whfb = (
base
// Get WHfB signins
| mv-expand todynamic(AuthenticationDetails)
| where AuthenticationDetails.authenticationMethod == "Windows Hello for Business"
| where IncomingTokenType == "primaryRefreshToken"
| extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime)
// Remove empty Session and Device IDs
| where SessionId != "" and DeviceID != ""
);
// Save the time frame for each WHfB PRT token
// We use the SessionID to identify a specific PRT token since the SessionID changes when a new refresh token is being used
let prt_timeframes = (
whfb
// Summarize the first and last PRT usage per device, by using the Session ID
| summarize TimeMin = arg_min(AuthenticationDateTime,*), TimeMax=arg_max(AuthenticationDateTime,*) by DeviceID, SessionId
| project DeviceID, SessionId, TimeMin, TimeMax
);
// Save all the Session IDs for the logins that came from a WHfB authentication method
let whfb_sessions = toscalar(
whfb
| summarize make_set(SessionId)
);
base
| mv-expand todynamic(AuthenticationDetails)
| extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime)
// Get all signins related to a WHfB Session
| where SessionId in (whfb_sessions)
// Join the access token requests comming from a WHfB session with all the PRT tokens used in the past for each device
| join kind=inner prt_timeframes on DeviceID
| extend CurrentSessionID = SessionId, OtherSessionID = SessionId1, OtherSessionTimeMin = TimeMin, OtherSessionTimeMax = TimeMax, DeviceName = tostring(DeviceDetail.displayName)
// Get logins where the current SessionID is not the same as another one
| where CurrentSessionID != OtherSessionID
// Check if the new Session ID is seen while other Session IDs are still active (only check first login of the current Session ID)
| summarize arg_min(AuthenticationDateTime, *) by DeviceID, CurrentSessionID
| where AuthenticationDateTime between (OtherSessionTimeMin .. OtherSessionTimeMax)
// Exclude Windows Sign In as application login since attackers will use the PRT to request access tokens for other applications (they do not need to signin into Windows anymore)
| where AppDisplayName != "Windows Sign In"
| project AuthenticationDateTime, UserPrincipalName, DeviceID, DeviceName, CurrentSessionID, OtherSessionID, OtherSessionTimeMin, OtherSessionTimeMax, AppDisplayName, ResourceDisplayName
// Get the Sign-in logs we want to query
let base = materialize(
SigninLogs
| where TimeGenerated > ago(1d)
);
// Get all the WHfB signins by looking at the authentication method and incomming token
let whfb = (
base
// Get WHfB signins
| mv-expand todynamic(AuthenticationDetails)
| where AuthenticationDetails.authenticationMethod == "Windows Hello for Business"
| where IncomingTokenType == "primaryRefreshToken"
| extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime)
// Remove empty Session and Device IDs
| where SessionId != "" and DeviceID != ""
);
// Save the time frame for each WHfB PRT token
// We use the SessionID to identify a specific PRT token since the SessionID changes when a new refresh token is being used
let prt_timeframes = (
whfb
// Summarize the first and last PRT usage per device, by using the Session ID
| summarize TimeMin = arg_min(AuthenticationDateTime,*), TimeMax=arg_max(AuthenticationDateTime,*) by DeviceID, SessionId
| project DeviceID, SessionId, TimeMin, TimeMax
);
// Save all the Session IDs for the logins that came from a WHfB authentication method
let whfb_sessions = toscalar(
whfb
| summarize make_set(SessionId)
);
base
| mv-expand todynamic(AuthenticationDetails)
| extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime)
// Get all signins related to a WHfB Session
| where SessionId in (whfb_sessions)
// Join the access token requests comming from a WHfB session with all the PRT tokens used in the past for each device
| join kind=inner prt_timeframes on DeviceID
| extend CurrentSessionID = SessionId, OtherSessionID = SessionId1, OtherSessionTimeMin = TimeMin, OtherSessionTimeMax = TimeMax, DeviceName = tostring(DeviceDetail.displayName)
// Get logins where the current SessionID is not the same as another one
| where CurrentSessionID != OtherSessionID
// Check if the new Session ID is seen while other Session IDs are still active (only check first login of the current Session ID)
| summarize arg_min(AuthenticationDateTime, *) by DeviceID, CurrentSessionID
| where AuthenticationDateTime between (OtherSessionTimeMin .. OtherSessionTimeMax)
// Exclude Windows Sign In as application login since attackers will use the PRT to request access tokens for other applications (they do not need to signin into Windows anymore)
| where AppDisplayName != "Windows Sign In"
| project AuthenticationDateTime, UserPrincipalName, DeviceID, DeviceName, CurrentSessionID, OtherSessionID, OtherSessionTimeMin, OtherSessionTimeMax, AppDisplayName, ResourceDisplayName
Stages and Predicates
Let binding: whfb
let whfb = (
base
| mv-expand todynamic(AuthenticationDetails)
| where AuthenticationDetails.authenticationMethod == "Windows Hello for Business"
| where IncomingTokenType == "primaryRefreshToken"
| extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime)
| where SessionId != "" and DeviceID != ""
);
Derived from base.
Let binding: prt_timeframes
let prt_timeframes = (
whfb
| summarize TimeMin = arg_min(AuthenticationDateTime,*), TimeMax=arg_max(AuthenticationDateTime,*) by DeviceID, SessionId
| project DeviceID, SessionId, TimeMin, TimeMax
);
Derived from whfb.
Let binding: whfb_sessions
let whfb_sessions = toscalar(
whfb
| summarize make_set(SessionId)
);
Derived from whfb.
The stages below define let base (the rule's main pipeline source).
Stage 1: source
SigninLogs
Stage 2: where
| where Timestamp > ago(1d)
The stages below run on base (the outer pipeline).
Stage 3: mv-expand
base
| mv-expand todynamic(AuthenticationDetails)
Stage 4: extend
| extend DeviceID = tostring(DeviceDetail.deviceId), AuthenticationDateTime = todatetime(AuthenticationDetails.authenticationStepDateTime)
Stage 5: where
| where SessionId in (whfb_sessions)
References whfb_sessions (defined above).
Stage 6: join
| join kind=inner prt_timeframes on DeviceID
Stage 7: extend
| extend CurrentSessionID = SessionId, OtherSessionID = SessionId1, OtherSessionTimeMin = TimeMin, OtherSessionTimeMax = TimeMax, DeviceName = tostring(DeviceDetail.displayName)
Stage 8: where
| where CurrentSessionID != OtherSessionID
Stage 9: summarize
| summarize arg_min(AuthenticationDateTime, *) by DeviceID, CurrentSessionID
Stage 10: where
| where AuthenticationDateTime between (OtherSessionTimeMin .. OtherSessionTimeMax)
Stage 11: where
| where AppDisplayName != "Windows Sign In"
Stage 12: project
| project AuthenticationDateTime, UserPrincipalName, DeviceID, DeviceName, CurrentSessionID, OtherSessionID, OtherSessionTimeMin, OtherSessionTimeMax, AppDisplayName, ResourceDisplayName
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 |
|---|---|---|
AppDisplayName | ne |
|
AuthenticationDateTime | ge |
|
AuthenticationDateTime | le |
|
CurrentSessionID | ne |
|
IncomingTokenType | eq |
|
SessionId | in |
|
authenticationMethod | 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 |
|---|---|
AppDisplayName | project |
AuthenticationDateTime | project |
CurrentSessionID | project |
DeviceID | project |
DeviceName | project |
OtherSessionID | project |
OtherSessionTimeMax | project |
OtherSessionTimeMin | project |
ResourceDisplayName | project |
UserPrincipalName | project |