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 Access
Credential Access

Telemetry coverage

Rule body

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);

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

These rows show field, operator, and value matches.

Output fields

These fields are emitted when the rule matches.

FieldSource
ClientOSproject
Computerproject
DeviceEventClassIDproject
DeviceNameproject
DeviceVendorproject
FileNameproject
Locationproject
Messageproject
Reasonproject
ReceiptTimeproject
SourceIPproject
TimeGeneratedproject
Userproject
timestampextend