Query Details
# *Detect Multiple Hello for Business PRT tokens being used simultaneously for one device.*
## Query Information
#### MITRE ATT&CK Technique(s)
| Technique ID | Title | Link |
| --- | --- | --- |
| T1606 | Forge Web Credentials | https://attack.mitre.org/techniques/T1606/ |
#### Description
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.
#### Risk
By using this detections, we can try to detect an attacker requesting access tokens with a forged PRT token on a new device.
#### Author <Optional>
- **Name:** Robbe Van den Daele
- **Github:** https://github.com/RobbeVandenDaele
- **Twitter:** https://x.com/RobbeVdDaele
- **LinkedIn:** https://www.linkedin.com/in/robbe-van-den-daele-677986190/
- **Website:** https://hybridbrothers.com/
#### References
- https://hybridbrothers.com/detecting-non-privileged-windows-hello-abuse/
## Defender XDR
```KQL
// 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
```
## Sentinel
```KQL
// 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
```This query is designed to detect potential security threats by identifying instances where multiple Primary Refresh Tokens (PRTs) are being used simultaneously on a single device. This could indicate that an attacker has managed to generate a new PRT on another device using exported Windows Hello for Business keys.
Here's a simplified breakdown of what the query does:
Data Collection: It starts by gathering sign-in logs from the past day.
Filter Windows Hello for Business (WHfB) Sign-ins: It filters these logs to focus on sign-ins that use the Windows Hello for Business authentication method and involve a primary refresh token.
Session Tracking: It tracks the time frames during which each PRT token is used on a device. This is done by identifying sessions through their Session IDs, which change when a new refresh token is used.
Session Comparison: The query then compares current session IDs with other session IDs to find instances where a new session ID appears while another session is still active on the same device.
Suspicious Activity Detection: It flags these instances as suspicious, especially if the new session is not related to a Windows Sign In, since attackers typically use PRTs to access other applications.
Output: Finally, it outputs details about these suspicious activities, including the time of authentication, user details, device information, and the applications accessed.
Overall, this query helps in identifying potential misuse of authentication tokens, which could be indicative of an attack involving forged credentials.

Robbe Van den Daele
Released: April 26, 2025
Tables
Keywords
Operators