Query Details

Enriched Entra Sign In Logs Requested Token By Suspicious RT

Query

// Requested access and refresh tokens outside of Global Secure Access
// Hunting query which helps to get an overview of issued access or refresh tokens outside of Global Secure Access
// by using a bounded refresh token without IncomingTokenType

// External lookup to get list of FOCI applications
let FociClientApplications = (externaldata(client_id: string)
    [@"https://raw.githubusercontent.com/secureworks/family-of-client-ids-research/refs/heads/main/known-foci-clients.csv"] with (format="csv", ignoreFirstRecord=true)
    | project-rename FociClientId = client_id
    );
// External lookup for ErrorCode description
let ErrorCodes = (externaldata(ResultType: string, Message: string)
    [@"https://raw.githubusercontent.com/f-bader/EntraID-ErrorCodes/refs/heads/main/EntraIDErrorCodes.csv"] with (format="csv", ignoreFirstRecord=true)
    | project-rename ResultErrorDescription = Message
    | project ResultType, ResultErrorDescription
    );
union SigninLogs, AADNonInteractiveUserSignInLogs
// Optional: Filter for specific time window, user and only successful sign-ins
// | where UserPrincipalName == "<UserPrincipalName>"
//         and CreatedDateTime between ( todatetime('<StartTime>') .. todatetime('<EndTime>') )
//         and ResultType == "0"
// Filter for sign-ins to home tenant only
| where HomeTenantId == ResourceTenantId
// Enrichment of device details        
| extend DeviceDetail = iff(isempty(DeviceDetail_dynamic), todynamic(DeviceDetail_string), DeviceDetail_dynamic)
| extend DeviceId = tostring(tolower(DeviceDetail.deviceId))
| extend DeviceName = tostring(toupper(DeviceDetail.displayName))
// Enrichment of token protection details
| extend TokenProtectionStatus = iff(isempty(TokenProtectionStatusDetails_dynamic), todynamic(TokenProtectionStatusDetails_string), TokenProtectionStatusDetails_dynamic)
| extend SignInSessionStatus = TokenProtectionStatus.signInSessionStatus
// Lookup for FOCI client
| join kind=inner (FociClientApplications) on $left.AppId == $right.FociClientId
| extend IsFoci = iff((AppId == FociClientId), "true", "false")
| join kind = leftouter (NetworkAccessTraffic
// Correlation with GSA network traffic
    | project
        TimeGenerated,
        TransactionId,
        ConnectionId,
        IPAddress = SourceIp,
        AgentVersion,
        UserId,
        DeviceId,
        UniqueTokenIdentifier = UniqueTokenId,
        InitiatingProcessName
    )
    on UserId, DeviceId, UniqueTokenIdentifier, IPAddress
| extend TokenAcquiredThroughGsa = iff((isnotempty(ConnectionId)), "true", "false")
// Lookup for Error Code
| join kind=leftouter (ErrorCodes) on ResultType
// Filtering for specific token properties
| where IncomingTokenType == "none" and SignInSessionStatus == "bound"
| project
    StolenTokenType = IncomingTokenType,
    StolenTokenApp = AppDisplayName,
    StolenTokenFromGsa = TokenAcquiredThroughGsa,
    StolenTokenStatus = SignInSessionStatus,
    StolenTokenUniqueTokenId = UniqueTokenIdentifier,
    StolenTokenProcessName = InitiatingProcessName,
    StolenTokenIsFoci = IsFoci,
    SessionId,
    CorrelationId,
    ResultType,
    OriginalRequestId,
    IPAddress,
    StolenTimestamp = CreatedDateTime
// Lookup to get error codes
| join kind=leftouter (ErrorCodes) on ResultType
// Get details of UniqueTokenIdentifier and TokenProtection for Diagnostic logs
| join kind = inner (
    union AADNonInteractiveUserSignInLogs, SigninLogs
    // Enrichment of device details        
    | extend DeviceDetail = iff(isempty(DeviceDetail_dynamic), todynamic(DeviceDetail_string), DeviceDetail_dynamic)
    | extend DeviceId = tostring(tolower(DeviceDetail.deviceId))
    | extend DeviceName = tostring(toupper(DeviceDetail.displayName))            
    | extend TokenProtectionStatus = iff(isempty(TokenProtectionStatusDetails_dynamic), todynamic(TokenProtectionStatusDetails_string), TokenProtectionStatusDetails_dynamic)
    | extend SignInSessionStatus = TokenProtectionStatus.signInSessionStatus
    | join kind=leftouter (FociClientApplications) on $left.AppId == $right.FociClientId
    | extend IsFoci = iff((AppId == FociClientId), "true", "false")
    | project 
        SessionId,
        CorrelationId,
        ErrorCode = ResultType,
        ResultType,
        OriginalRequestId,
        IPAddress,
        UniqueTokenIdentifier,
        SignInSessionStatus,
        IsFoci,
        DeviceId,
        CreatedDateTime,
        AppDisplayName,
        ResourceDisplayName,
        UserId
) on SessionId, CorrelationId, ResultType, OriginalRequestId, IPAddress
// Remove tokens which has been issued from compliant network
| join kind = anti (NetworkAccessTraffic
    | where TimeGenerated >ago(1d)
    | project
        TimeGenerated,
        TransactionId,
        ConnectionId,
        IPAddress = SourceIp,
        AgentVersion,
        UserId,
        DeviceId,
        UniqueTokenIdentifier = UniqueTokenId,
        InitiatingProcessName
) on UserId, DeviceId, UniqueTokenIdentifier, IPAddress
| extend IsOutsideOfGsa = "true"
| project UserPrincipalName, StolenTimestamp, StolenTokenType, StolenTokenApp, StolenTokenIsFoci, StolenTokenStatus, StolenTokenProcessName, SessionId, SignInTime = CreatedDateTime, AppDisplayName, ResourceDisplayName, SignInSessionStatus, IsFoci, IsOutsideOfGsa, UniqueTokenIdentifier, ErrorCode, ResultErrorDescription
| sort by SignInTime asc

Explanation

This query is designed to identify and analyze access or refresh tokens that were issued outside of a secure network environment, specifically outside of Global Secure Access (GSA). Here's a simplified breakdown of what the query does:

  1. External Data Sources:

    • It pulls in a list of known Family of Client IDs (FOCI) applications and error code descriptions from external CSV files hosted on GitHub.
  2. Data Union:

    • It combines data from two sources: SigninLogs and AADNonInteractiveUserSignInLogs, which contain sign-in information.
  3. Filtering:

    • The query filters for sign-ins that occurred within the user's home tenant.
    • It focuses on tokens that do not have an IncomingTokenType and have a SignInSessionStatus of "bound".
  4. Data Enrichment:

    • It enriches the data with device details and token protection details.
    • It checks if the application is a FOCI client by joining with the FOCI applications list.
    • It correlates the sign-in data with network traffic data to determine if the token was acquired through GSA.
  5. Error Code Lookup:

    • It joins with the error codes data to provide descriptions for any error codes found.
  6. Token Identification:

    • It identifies tokens that were issued outside of the GSA by excluding those that have been issued from a compliant network.
  7. Output:

    • The query projects a set of fields that include user details, token details, application details, and error descriptions.
    • It sorts the results by the sign-in time in ascending order.

Overall, this query helps security analysts identify potentially unauthorized or risky token issuances by highlighting tokens that were issued outside of a secure network environment.

Details

Thomas Naunheim profile picture

Thomas Naunheim

Released: August 19, 2025

Tables

SigninLogsAADNonInteractiveUserSignInLogsNetworkAccessTraffic

Keywords

TokensDevicesApplicationsNetworkUsersLogs

Operators

letexternaldataproject-renameprojectunionwhereextendiffisemptytodynamictostringtolowertoupperjoinonkindsortasc

Actions

GitHub