Detection rules › Kusto

Detect Multiple Hello for Business PRT tokens being used simultaneously for one device.

Group by
CurrentSessionID, DeviceID
Author
Robbe Van den Daele
Source
github.com/HybridBrothers/Hunting-Queries-Detection-Rules

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

TacticTechniques
Credential AccessT1606 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.

FieldKindValues
AppDisplayNamene
  • Windows Sign In transforms: cased
AuthenticationDateTimege
  • OtherSessionTimeMin
AuthenticationDateTimele
  • OtherSessionTimeMax
CurrentSessionIDne
  • OtherSessionID transforms: cased
IncomingTokenTypeeq
  • primaryRefreshToken transforms: cased
SessionIdin
  • whfb_sessions transforms: cased
authenticationMethodeq
  • Windows Hello for Business transforms: cased

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.

FieldSource
AppDisplayNameproject
AuthenticationDateTimeproject
CurrentSessionIDproject
DeviceIDproject
DeviceNameproject
OtherSessionIDproject
OtherSessionTimeMaxproject
OtherSessionTimeMinproject
ResourceDisplayNameproject
UserPrincipalNameproject