Detection rules › Kusto

IP with multiple failed Microsoft Entra ID logins successfully logs in to Palo Alto VPN

Severity
medium
Time window
1d
Author
Microsoft Security Research
Source
github.com/Azure/Azure-Sentinel

This query creates a list of IP addresses with the number of failed login attempts to Entra ID above a set threshold ( default of 5 ). It then looks for any successful Palo Alto VPN logins from any of these IPs within the same timeframe.

MITRE ATT&CK coverage

TacticTechniques
Initial AccessT1078 Valid Accounts
Credential AccessT1110 Brute Force

Rule body kusto

id: ba144bf8-75b8-406f-9420-ed74397f9479
name: IP with multiple failed Microsoft Entra ID logins successfully logs in to Palo Alto VPN
description: |
    This query creates a list of IP addresses with the number of failed login attempts to Entra ID 
    above a set threshold ( default of 5 ).  It then looks for any successful Palo Alto VPN logins from any of these IPs within the same timeframe.
severity: Medium
requiredDataConnectors:
  - connectorId: AzureActiveDirectory
    dataTypes:
     - SigninLogs
  - connectorId: AzureActiveDirectory
    dataTypes:
      - AADNonInteractiveUserSignInLogs
  - connectorId: PaloAltoNetworks
    dataTypes:
     - CommonSecurityLog
queryFrequency: 1d
queryPeriod: 1d
triggerOperator: gt
triggerThreshold: 0
tactics:
  - InitialAccess
  - CredentialAccess
relevantTechniques:
  - T1078
  - T1110
query: |
  //Set a threshold of failed AAD signins from an IP address within 1 day above which we want to deem those logins suspicious.
  let signin_threshold = 5; 
  //Make a list of IPs with AAD signin failures above our threshold.
  let aadFunc = (tableName:string){
  let suspicious_signins = 
      table(tableName)
      //Looking for logon failure results
      | where ResultType !in ("0", "50125", "50140")
      //Exclude localhost addresses to reduce the chance of FPs
      | where IPAddress !in ("127.0.0.1", "::1")
      | summarize count() by IPAddress
      | where count_ >  signin_threshold
      | summarize make_set(IPAddress);
      suspicious_signins
  };
  let aadSignin = aadFunc("SigninLogs");
  let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");
  let suspicious_signins = 
  union isfuzzy=true aadSignin, aadNonInt
  | summarize make_set(set_IPAddress);
  //See if any of those IPs have sucessfully logged into PA VPNs during the same timeperiod
  CommonSecurityLog
      //Select only PA VPN sucessful logons
      | where DeviceVendor == "Palo Alto Networks" and DeviceEventClassID == "globalprotect"
      | where Message has "GlobalProtect gateway user authentication succeeded"
      //Parse out the logon source IP from the Message field to match on
      | extend SourceIP = extract("Login from: ([^,]+)", 1, Message) 
      | where SourceIP in (suspicious_signins)
      | extend Reason = "Multiple failed AAD logins from SourceIP"
      //Parse out other useful information from Message field
      | extend User = extract('User name: ([^,]+)', 1, Message) 
      | extend ClientOS = extract('Client OS version: ([^,\"]+)', 1, Message)
      | extend Location = extract('Source region: ([^,]{2})',1, Message)
      | project TimeGenerated, Reason, SourceIP, User, ClientOS, Location, Message, DeviceName, ReceiptTime, DeviceVendor, DeviceEventClassID, Computer, FileName
      | extend timestamp = TimeGenerated
entityMappings:
  - entityType: Account
    fieldMappings:
      - identifier: Name
        columnName: User
  - entityType: Host
    fieldMappings:
      - identifier: HostName
        columnName: DeviceName
  - entityType: IP
    fieldMappings:
      - identifier: Address
        columnName: SourceIP
version: 1.0.4
kind: Scheduled
metadata:
    source:
        kind: Community
    author:
        name: Microsoft Security Research
    support:
        tier: Community
    categories:
        domains: [ "Security - Others", "Identity" ]

Stages and Predicates

Parameters

let signin_threshold = 5;
let aadSignin = aadFunc("SigninLogs");
let aadNonInt = aadFunc("AADNonInteractiveUserSignInLogs");

Let binding: aadFunc

let aadFunc = (tableName:string){
let suspicious_signins = 
    table(tableName)
    | where ResultType !in ("0", "50125", "50140")
    | where IPAddress !in ("127.0.0.1", "::1")
    | summarize count() by IPAddress
    | where count_ >  signin_threshold
    | summarize make_set(IPAddress);

Derived from signin_threshold, suspicious_signins.

union isfuzzy=true (2 sources)

Each leg below queries one source; the rule matches if any leg does. Sources: aadSignin, aadNonInt

Leg 1: aadSignin

Leg 2: aadNonInt

Applied to the combined result

| summarize make_set(set_IPAddress) | where DeviceVendor == "Palo Alto Networks" and DeviceEventClassID == "globalprotect" | where Message has "GlobalProtect gateway user authentication succeeded" | extend SourceIP = extract("Login from: ([^,]+)", 1, Message) | where SourceIP in (suspicious_signins) | extend Reason = "Multiple failed AAD logins from SourceIP" | extend User = extract('User name: ([^,]+)', 1, Message) | extend ClientOS = extract('Client OS version: ([^,\"]+)', 1, Message) | extend Location = extract('Source region: ([^,]{2})',1, Message) | project TimeGenerated, Reason, SourceIP, User, ClientOS, Location, Message, DeviceName, ReceiptTime, DeviceVendor, DeviceEventClassID, Computer, FileName | extend timestamp = TimeGenerated

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
DeviceEventClassIDeq
  • globalprotect transforms: cased
DeviceVendoreq
  • Palo Alto Networks transforms: cased
Messagematch
  • GlobalProtect gateway user authentication succeeded transforms: term
SourceIPin
  • suspicious_signins 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
ClientOSproject
Computerproject
DeviceEventClassIDproject
DeviceNameproject
DeviceVendorproject
FileNameproject
Locationproject
Messageproject
Reasonproject
ReceiptTimeproject
SourceIPproject
TimeGeneratedproject
Userproject
timestampextend