Query Details

HUNT 16 AD ADIDNS Poisoning Record Audit 30d

Query

// =========================================================
// HUNT-16 | AD-ADIDNS-Poisoning-Record-Audit-30d
// Description : ADs integrated DNS stores zone records as
//               AD objects (dnsNode class). This query
//               audits new dnsNode and dnsZone records
//               (EventID 5137/5136) created by non-admin
//               or service accounts, focusing on wildcard
//               (*) and look-alike hostnames that could
//               serve NTLM relay or WPAD poisoning attacks.
// Period      : 30 days
// Use Case    : ADIDNS poisoning detection, NTLM relay
//               setup, WPAD attack surface assessment
// Tables      : SecurityEvent
// =========================================================

let Period = 30d;

// Known DNS admin accounts
let DNSAdmins = SecurityEvent
    | where TimeGenerated > ago(90d)
    | where EventID in (4728, 4732, 4756)
    | where TargetUserName has_any ("DnsAdmins", "Domain Admins", "Enterprise Admins",
                                    "Administrators")
    | summarize by DNSAdmin = tolower(tostring(column_ifexists("MemberName", "")));

// dnsNode and dnsZone object creations and modifications
let DNSObjectChanges = SecurityEvent
    | where TimeGenerated > ago(Period)
    | where EventID in (5136, 5137, 5141)   // modify, create, delete
    | where tostring(column_ifexists("ObjectClass", "")) in~ ("dnsNode", "dnsZone")
    | extend
        RecordName   = extract(@"DC=([^,]+)", 1, ObjectName),
        ZoneName     = extract(@"DC=([^,]+),CN=MicrosoftDNS", 1, ObjectName),
        Actor        = strcat(SubjectDomainName, "\\", SubjectUserName),
        ActorNorm    = tolower(SubjectUserName),
        EventType    = case(
            EventID == 5137, "Created",
            EventID == 5141, "Deleted",
            "Modified"
        ),
        IsWildcard   = ObjectName startswith "DC=*,",
        IsWPAD       = tolower(ObjectName) has "wpad"
    | extend        IsLookAlike  = RecordName matches regex @"^[a-z]{3,8}$"  // short names common in poisoning
                        and RecordName !in~ ("www", "mail", "smtp", "pop", "imap", "ftp", "vpn",
                                             "dns", "ldap", "kerberos", "gc", "pdc");

DNSObjectChanges
| join kind=leftouter (DNSAdmins) on $left.ActorNorm == $right.DNSAdmin
| extend IsAuthorizedActor = isnotempty(DNSAdmin)
| summarize
    TotalChanges        = count(),
    UnauthorizedChanges = countif(not(IsAuthorizedActor)),
    WildcardRecords     = countif(IsWildcard),
    WPADRecords         = countif(IsWPAD),
    LookAlikeRecords    = countif(IsLookAlike),
    CreatedRecords      = countif(EventType == "Created"),
    DeletedRecords      = countif(EventType == "Deleted"),
    Actors              = make_set(Actor, 10),
    UnauthorizedActors  = make_set_if(Actor, not(IsAuthorizedActor), 10),
    RecordNames         = make_set(RecordName, 30),
    Zones               = make_set(ZoneName, 10),
    LastChange          = max(TimeGenerated),
    FirstChange         = min(TimeGenerated)
  by ActorNorm
| extend
    RiskScore = (WildcardRecords * 50)
              + (WPADRecords * 50)
              + (UnauthorizedChanges * 25)
              + (LookAlikeRecords * 15)
              + (CreatedRecords * 5),
    RiskLevel = case(
        WildcardRecords >= 1, "Critical - Wildcard_DNS_Record_Injected",
        WPADRecords >= 1,     "Critical - WPAD_Record_Injected",
        UnauthorizedChanges >= 3, "High - Unauthorized_DNS_Modifications",
        LookAlikeRecords >= 2,    "High - LookAlike_Records_Created",
        UnauthorizedChanges >= 1, "Medium - Unauthorized_DNS_Change",
        "Low"
    ),
    PossibleAttack = case(
        WildcardRecords >= 1, "ADIDNS_Wildcard_NTLM_Relay",
        WPADRecords >= 1,     "WPAD_Poisoning",
        LookAlikeRecords >= 2,"DNS_Spoofing_Look-Alike",
        "Unauthorized_DNS_Modification"
    )
| project
    ActorNorm,
    RiskLevel,
    RiskScore,
    PossibleAttack,
    TotalChanges,
    WildcardRecords,
    WPADRecords,
    LookAlikeRecords,
    CreatedRecords,
    UnauthorizedActors,
    RecordNames,
    Zones,
    FirstChange,
    LastChange
| order by RiskScore desc

Explanation

This query is designed to detect potential security threats related to DNS records in an Active Directory environment over the past 30 days. Here's a simplified breakdown of what it does:

  1. Identify DNS Admins: It first identifies known DNS admin accounts by looking at specific security events over the last 90 days.

  2. Track DNS Changes: It then tracks changes to DNS records (creation, modification, deletion) within the last 30 days, focusing on records that could be used for malicious purposes, such as wildcard entries or those resembling common phishing targets (e.g., "wpad").

  3. Analyze Changes: The query checks if these changes were made by authorized DNS admins or by other accounts. It categorizes the changes based on their nature (e.g., wildcard, WPAD, look-alike) and who made them.

  4. Assess Risk: It calculates a risk score for each actor (user) based on the types of changes they made. Higher scores indicate more suspicious activity.

  5. Determine Risk Level and Possible Attack: Based on the risk score and the nature of the changes, it assigns a risk level (e.g., Critical, High, Medium, Low) and suggests possible attack scenarios (e.g., NTLM relay, WPAD poisoning).

  6. Output: The query outputs a list of users (actors) along with their risk level, risk score, potential attack type, and details of the DNS changes they made. It orders the results by risk score, highlighting the most suspicious activities.

Overall, this query helps in identifying unauthorized or potentially malicious changes to DNS records, which could indicate attempts at DNS poisoning or other network attacks.

Details

David Alonso profile picture

David Alonso

Released: March 24, 2026

Tables

SecurityEvent

Keywords

SecurityEventDNSDNSNodeDNSZoneActorEventTypeRecordNameZoneNameActorNormRiskScoreRiskLevelPossibleAttackTotalChangesWildcardRecordsWPADRecordsLookAlikeRecordsCreatedRecordsUnauthorizedActorsRecordNamesZonesFirstChangeLastChange

Operators

letinwherehas_anysummarizetolowertostringcolumn_ifexistsagoextendextractstrcatcasestartswithmatchesregex!in~joinkindleftouteron$left$rightisnotemptycountcountifmake_setmake_set_ifmaxminbyprojectorder bydesc

Actions