Query Details

Unusual LDAP Query Burst From New Or Known Device

Query

# *Unusual LDAP Query Burst from New or Known Device*

## Query Information

#### MITRE ATT&CK Technique(s)

| Technique ID | Title    | Link    |
| ---  | --- | --- |
| T1087.002 | Domain Account | https://attack.mitre.org/techniques/T1087/002/ |
| T1069.002 | Domain Groups | https://attack.mitre.org/techniques/T1069/002/ |

#### Description

This rule detects an unusual burst of LDAP queries originating from a device. It identifies both new devices with a high absolute burst count and known devices exhibiting a significant deviation (2 standard deviations above average) from their established baseline of LDAP query activity. The detection focuses on LDAP queries to Active Directory with a BaseObject starting with 'DC=' and excludes known scheduled sources and IP addresses.

#### Author <Optional>
- **Name: Benjamin Zulliger**
- **Github: https://github.com/benscha/KQLAdvancedHunting**
- **LinkedIn: https://www.linkedin.com/in/benjamin-zulliger/**


## Defender XDR
```KQL
let LookbackWindow = 24h;
let BurstWindow = 10min;
let KnownScheduledSources = dynamic([
    "DEVICE001",
    "DEVICE002"
]);
let BurstBase =
    IdentityQueryEvents
    | where TimeGenerated >= ago(LookbackWindow)
    | where ActionType == "LDAP query"
    | where Application == "Active Directory"
    | extend AF = parse_json(AdditionalFields)
    | extend
        BaseObject = tostring(AF.BaseObject),
        SearchFilter = tostring(AF.SearchFilter),
        FromDevice = tostring(AF["FROM.DEVICE"]),
        SourceOS = tostring(AF.SourceComputerOperatingSystem)
    | where BaseObject matches regex @"^DC="
    | where isnull(parse_ipv4(FromDevice))
    | where FromDevice !in~ (KnownScheduledSources)
    | summarize
        BurstCount = count(),
        TargetDCCount = dcount(DestinationDeviceName),
        TargetDCs = make_set(DestinationDeviceName, 5),
        BaseObjects = make_set(BaseObject, 5),
        SearchFilters = make_set(SearchFilter, 5),
        FirstSeen = min(TimeGenerated),
        LastSeen = max(TimeGenerated)
        by FromDevice, IPAddress, SourceOS, bin(TimeGenerated, BurstWindow);
// Flat-pattern autodetect across the entire lookback window
let FlatPatternDevices =
    BurstBase
    | summarize
        UniqueCounts = dcount(BurstCount),
        BinCount = count()
        by FromDevice
    | where BinCount >= 3 and UniqueCounts == 1
    | project FromDevice;
// Baseline per device: typical BurstCount of the last 7 days
let Baseline =
    IdentityQueryEvents
    | where TimeGenerated between (ago(7d) .. ago(LookbackWindow))
    | where ActionType == "LDAP query"
    | where Application == "Active Directory"
    | extend AF = parse_json(AdditionalFields)
    | extend
        BaseObject = tostring(AF.BaseObject),
        FromDevice = tostring(AF["FROM.DEVICE"])
    | where BaseObject matches regex @"^DC="
    | where isnull(parse_ipv4(FromDevice))
    | summarize
        BinCount = count()
        by FromDevice, bin(TimeGenerated, BurstWindow)
    | summarize
        AvgBurst = avg(BinCount),
        StdBurst = stdev(BinCount)
        by FromDevice;
BurstBase
| where FromDevice !in (FlatPatternDevices)
// Baseline join: only significant deviation from normal behavior
| join kind=leftouter Baseline on FromDevice
| extend
    Threshold = AvgBurst + (2 * StdBurst),   // 2-sigma above baseline
    IsNewDevice = isnull(AvgBurst)             // No baseline = unknown device
| where
    // New device without baseline AND high absolute burst
    (IsNewDevice and BurstCount >= 50 and TargetDCCount >= 2)
    or
    // Known device that lies significantly above its own baseline
    (not(IsNewDevice) and BurstCount > Threshold and TargetDCCount >= 2)
| extend Severity = case(
    TargetDCCount >= 3 and BurstCount >= 200, "High",
    TargetDCCount >= 2 and BurstCount >= 50,  "Medium",
    "Low"
)
| project
    bin_TimeGenerated = bin(TimeGenerated, BurstWindow),
    FromDevice,
    IPAddress,
    SourceOS,
    BurstCount,
    TargetDCCount,
    TargetDCs,
    BaseObjects,
    SearchFilters,
    AvgBurst = round(AvgBurst, 1),
    Threshold = round(Threshold, 1),
    IsNewDevice,
    Severity
| order by BurstCount desc
```

Explanation

This query is designed to detect unusual bursts of LDAP queries from devices to Active Directory. Here's a simplified breakdown of what it does:

  1. Time Frames: It looks at LDAP query activity over the past 24 hours and breaks it down into 10-minute intervals.

  2. Exclusions: It excludes queries from known scheduled devices (like "DEVICE001" and "DEVICE002") and ignores queries from IP addresses.

  3. Data Collection: It collects data on LDAP queries that target Active Directory objects starting with 'DC='.

  4. Burst Detection:

    • For new devices (those without a historical baseline), it flags them if they have a high number of queries (at least 50) and target at least two different domain controllers.
    • For known devices, it checks if their query activity is significantly higher than their usual pattern (more than two standard deviations above their average) and also targets at least two domain controllers.
  5. Severity Levels: It assigns a severity level based on the number of queries and the number of domain controllers targeted:

    • "High" if there are 200 or more queries targeting 3 or more domain controllers.
    • "Medium" if there are 50 or more queries targeting 2 or more domain controllers.
    • "Low" otherwise.
  6. Output: The query outputs details such as the device name, IP address, operating system, number of queries, number of domain controllers targeted, and severity level, sorted by the number of queries in descending order.

In essence, this query helps identify potential security threats by flagging devices that show unusual LDAP query behavior, which could indicate unauthorized access or reconnaissance activities.

Details

Benjamin Zulliger profile picture

Benjamin Zulliger

Released: June 22, 2026

Tables

IdentityQueryEvents

Keywords

DevicesIdentityQueryEventsActiveDirectoryLDAPBaseObjectIPAddressSourceOSTimeGeneratedBurstCountTargetDCCountDestinationDeviceNameAdditionalFields

Operators

letdynamicagowhereextendparse_jsontostringmatches regexisnullparse_ipv4!in~summarizecountdcountmake_setminmaxbybinprojectbetweenjoin kind=leftoutercaseroundorder by

Actions