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 Access

References

Telemetry coverage

Rule body

// 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 base is inlined into the numbered stages below.

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 != ""
);

Let binding: prt_timeframes used in Stage 6

let prt_timeframes = (
    whfb
    | summarize TimeMin = arg_min(AuthenticationDateTime,*), TimeMax=arg_max(AuthenticationDateTime,*) by DeviceID, SessionId
    | project DeviceID, SessionId, TimeMin, TimeMax
);

Let binding: whfb_sessions used in Stage 5

let whfb_sessions = toscalar(
    whfb
    | summarize make_set(SessionId)
);

Stages 1 to 2 define let base (the rule's main pipeline source); stages 3 to 12 run on it.

Stage 1: source

SigninLogs

Stage 2: where

| where Timestamp > ago(1d)

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)

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

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
AppDisplayNameproject
AuthenticationDateTimeproject
CurrentSessionIDproject
DeviceIDproject
DeviceNameproject
OtherSessionIDproject
OtherSessionTimeMaxproject
OtherSessionTimeMinproject
ResourceDisplayNameproject
UserPrincipalNameproject