Skip to content

Back to the catalog

The LDAP Whitespace Problem | Huntress
huntress.comwriteup

The LDAP Whitespace Problem | Huntress

Part two of a Huntress detection-engineering series showing that Windows Event 1644 logs LDAP filters from Active Directory's LDAP Service Layer with inconsistent, undocumented whitespace around bitwise operators, causing naive Sigma rules for Impacket tools like findDelegation.py to fire only ~30% of the time. The author walks through iterating from exact-string matches to whitespace-tolerant, case-insensitive regex (e.g. `.*userAccountControl\s*&\s*524288.*`), a pre-filter-then-regex pattern for SIEM performance, and a final production Sigma rule combining userAccountControl bitwise checks, requested-attribute lists, and source-IP filtering to reliably catch RBCD/delegation abuse.

TL;DR Windows Event 1644 doesn't normalize whitespace in logged LDAP filters, so naive Sigma rules for Impacket tools like findDelegation.py miss most hits — this post shows the regex fixes that make them reliable.
Document

What it is

Part 2 of Huntress's LDAP/Active Directory detection series, focused on making Sigma rules for Impacket-style bitwise LDAP queries (userAccountControl&524288, e.g. findDelegation.py) fire reliably. The concrete takeaway the summary omits is the actual working regex and the failure modes of the intermediate attempts.

How it works

LDAP tools send OID syntax; Active Directory's LDAP Service Layer transforms it to a bitwise operation before Event 1644 logs it. That layer does not normalize whitespace, so the same query appears in logs with unpredictable spacing around both the & operator and the parentheses. The author observed six real variants of one identical query:

(userAccountControl&524288)
(userAccountControl & 524288)
( userAccountControl&524288 )
( userAccountControl & 524288 )
(userAccountControl& 524288)
( userAccountControl &524288)

Exact-string matching gave a ~30% production hit rate. The spacing is driven by client formatting plus undocumented AD string-processing, serialization, and additional processing steps — so enumerating variants is futile.

What to do

The rule progression, ending at the pattern that gave consistent hits. The lesson is that \s* must cover the parentheses too, not just the operator — the regex in Stage 3 still missed hits because it ignored parenthesis spacing.

Insufficient (operator-only, still missed hits):

selection:
  winlog.event_data.LDAPFilter|re: '.*userAccountControl\s*&\s*524288.*'

Working pattern (accounts for parenthesis spacing):

selection:
  winlog.event_data.LDAPFilter|re: '.*\(\s*userAccountControl\s*&\s*524288\s*\).*'

Match against winlog.event_data.LDAPFilter from Windows Event 1644. Apply the same \s*-everywhere approach to any other bitwise userAccountControl mask you detect on.

Caveats

  • Event 1644 must be enabled for any of this to log at all (the page assumes it is, does not cover enablement).
  • The whitespace behavior is undocumented by Microsoft, so treat the six observed variants as illustrative, not exhaustive — the value of the regex is that it stops depending on the exact set.
  • The fetched text cuts off mid-example in the final "patterns that actually work" section, so additional field-specific patterns beyond the userAccountControl case may exist on the live page.

Organisations named here

  • Event 1644

Technologies named here

  • LDAP
  • Active Directory
  • Impacket
  • findDelegation.py
  • Sigma
  • regex101.com
  • Windows

Techniques named here

  • LDAP query OID-to-bitwise transformation