Auditing Every Active Directory LDAP Extended Control: Additional Findings and Detection Blind Spots
This is a companion blog to, Powerful LDAP Extended Controls: Anti-Remediation and Invisible Recon in AD, listing addtional interesting but not critical findings.
Research writeup, 2026-06-29. Lab work against a two-DC cloud.lab (Windows Server 2022, forest functional level 2016). Companion to the Powerful LDAP Extended Controls post, which covers the rwo qualified-novel findings; this post covers everything else worth knowing.
TL;DR
I built a scanner and fuzzer and exercised every MS-ADTS LDAP extended control and operation (~40 advertised controls, plus extended operations) across four privilege tiers and two domain controllers. Most controls behave exactly as documented. The additional findings that matter fall into three buckets:
- Event 1644 detection doesnt work as defenders assume. What Active Directory’s LDAP query log (Event 1644) actually captures is far less than “every LDAP query,” and several abuse- relevant controls are invisible to it. These were all validated by firing the techniques.
- DirSync reads confidential attributes but not secrets — a known bypass, bounded precisely.
- A few sharp edges in individual controls.
Nothing here is new technique; the value is the precise, validated behavior — especially the logging blind spots, which I have not seen written up in one place.
What I did
I enumerated the rootDSE supportedControl set, then drove each control through real LDAP operations (search, and gated behind an explicit destructive flag, add/modify/delete) at four identities: anonymous, plain domain user, partial-replication, and full-replication. A BER-aware fuzzer threw ~10,400 malformed and structurally-valid-but-extreme inputs at the control parsers with a crash monitor watching DC health. A second DC let us test replication-dependent behavior.
The single most useful output for defenders turned out not to be an offensive finding at all — it was discovering what the DC does and does not log when these controls are used.
The detection blind spots
Everyone “enables Event 1644” (Expensive LDAP Searches) and assumes they now see LDAP abuse. Here is what I found.
1. thresholds = 0 logs nothing. The common way to enable Event 1644 sets three registry thresholds. I found (and the lab’s own setup script had this bug) that a threshold of 0 means disabled. Only inefficient/expensive searches get logged by a separate check, so targeted single-object control-searches (the interesting ones) produce no event. You must set the thresholds to 1 to log every search. A DC “with 1644 enabled” at 0 has a false sense of coverage.
2. Event 1644 is search-only. It logs LDAP search operations. A control attached to a MODIFY/ADD/DELETE generates no 1644 at all. I confirmed FORCE_UPDATE (a modify control) produces zero 1644 events. The same applies to SET_OWNER on an add and to BATCH_REQUEST (an extended operation). Any rule keyed on those control OIDs in 1644 will never fire.
3. 1644 records only some search controls. Even for searches, only a subset of controls appears in the event’s Server controls field. We fired each and read the result:
| Control | In Server controls? | Logged as |
|---|---|---|
| SD_FLAGS (.801) | ✅ | SDflags:0x7; |
| ASQ (.1504) | ✅ | ASQ:member; |
| SHOW_DELETED (.417) | ✅ | return_deleted; |
| SEARCH_OPTIONS / phantom-root (.1340) | ❌ | (field empty) |
| EXTENDED_DN (.529) | ❌ | (field empty) |
| EXPECTED_ENTRY_COUNT (.2211) | ❌ | (field empty) |
| NOTIFICATION (.528) | ❌ | (no entry) |
So a cross-NC phantom-root search or an existence/count oracle (EXPECTED_ENTRY_COUNT) is invisible to 1644 even though the search itself logs. (Also: a search that matches 0 entries doesn’t log at all — zero-result probing is partially free.)
4. DirSync is detected via Event 4662, not 1644. DirSync uses replication (Get-Changes) semantics, not the search path. A DirSync read produced 0× Event 1644 and 1× Event 4662 with the DS-Replication-Get-Changes property GUID {1131f6aa-…}. The trap: many “DCSync” rules watch only Get-Changes-All ({1131f6ad-…}); DirSync needs only Get-Changes, so those rules miss it.
5. OBJECT_SECURITY DirSync is invisible to both logs. DirSync with the OBJECT_SECURITY flag (0x1) is the unprivileged path — no replication right, no permission change, just Domain Users. As a plain user it returned 1000 objects (and, with SD_FLAGS, all 1000 nTSecurityDescriptors) with zero Event 4662 and zero Event 1644 — no Get-Changes right (no 4662), replication path (no 1644). A stealthy bulk-collection / ACL-harvest primitive; the only reliable signal is a SACL canary or network/ETW. Covered in depth including the offensive framing and the honest “stealth, not new access” bound in the companion post “Powerful LDAP Extended Controls: Anti-Remediation and Invisible Recon in AD.”
6. ADWS logs as loopback and the builtin noise filter makes it worse. Active Directory Web Services (TCP 9389, the default transport for the PowerShell AD cmdlets) proxies LDAP. The same query run via Get-ADUser logged Client: 127.0.0.1 (the DC itself); a direct LDAP search logged the real client IP. Every 1644 rule must filter 127.0.0.1 to drop DC self-noise and that exact filter also hides all ADWS-proxied activity. The ADWS request log (events 1138/1139, which carry the real IP) is not enabled by default, so out-of-the-box ADWS abuse is unattributed.
7. Filter obfuscation - partial. AD normalizes attribute OIDs to friendly names in the 1644 filter ((1.2.840.113556.1.4.221=x) was logged as (sAMAccountName=x)), so OID obfuscation does not evade an attribute-name match. But structure and whitespace are preserved verbatim ((!(!(…))) logged as-is), so exact-filter-string rules are brittle. Match on names, not strings.
Defensive backstop: a SACL audit ACE on a high-value object/attribute fires Event 4662 regardless of control, transport, or ADWS — the one control-agnostic tripwire. Use it on the important objects/attributes (e.g. the RBCD attribute, privileged-group member, AdminSDHolder).
DirSync reads confidential attributes but not secrets
This one is not new. Simon Décosse (simondotsh) wrote up DirSync abuse in 2022. My contribution is the precise boundary.
Marking a custom attribute confidential (searchFlags bit 0x80) is common hardening advice: normal LDAP reads then require the CONTROL_ACCESS right, so plain users get nothing. DirSync does not apply that gate because it is a replication read. Any principal with replication rights reads the confidential value via a single DirSync search. The matrix I measured:
msTestConfidential searchFlags | Get-Changes only | + Filtered-Set | Get-Changes-All | domain user |
|---|---|---|---|---|
0x80 (confidential) | leaked | leaked | leaked | blocked |
0x80 + 0x200 (confidential + RODC-FAS) | blocked | leaked | leaked | blocked |
So “mark it confidential” is insufficient; set both 0x80 and 0x200 (as LAPS does for ms-Mcs-AdmPwd) and tightly control all three replication rights.
The important bound: DirSync does not expose secrets. I requested the replicated secret attributes (unicodePwd, dBCSPwd, supplementalCredentials, pekList, …) via DirSync at the full-replication tier and got empty for every one. DirSync (an LDAP-layer control) does not serialize password material. That comes only via DRSUAPI IDL_DRSGetNCChanges (classic DCSync). DirSync ≠ DCSync. It bypasses the confidential-attribute gate (custom sensitive data, LAPS-style blobs) but is not an LDAP path to password hashes.
BATCH_REQUEST: the first public client, and a clean negative
LDAP_SERVER_BATCH_REQUEST (1.2.840.113556.1.4.2212) is absent from supportedControl but present in supportedExtension. It is an extended operation, not a control (a registry correction worth noting; AD’s atomic mechanism is BATCH_REQUEST, not RFC 5805 transactions). The obvious offensive question: can you smuggle a denied operation past an allowed one in a single atomic batch, or get inconsistent per-operation access checks?
I built what appears to be the first public working BATCH_REQUEST client, a BER constructor and response parser, to test it. The answer is a clean negative:
- An
ADD(allowed) + modify→Domain Admins(denied)batch left no side effects — the allowed ADD did not persist (atomic rollback) and svc-research did not enter Domain Admins. - A
denied-first + ADD(allowed)batch did not execute the second op (sequential short-circuit). - The extended-op envelope returns
successeven when an inner op is denied (per MS-ADTS), and no response value is returned on a rolled-back batch.
Per-operation ACLs are enforced under the bound identity, with atomic rollback. No cross-op bypass. The byproduct of the research, a reusable BATCH client, is the lasting contribution.
Sharp edges
EXPECTED_ENTRY_COUNT is a base-DN oracle, and broken under subtree. MS-ADTS says the control (.2211) fails a search with constraintViolation when the match count is outside [min,max]. Under BASE scope it does exactly that. A [1,1] assertion against a known DN succeeds, [0,0] and [2,5] violate which leaks the exact existence/count of an object without rights to read its attributes (a clean existence oracle). Under SUBTREE scope, this DC evaluates the count as 0 regardless of actual matches. [0,0] succeeds over a filter matching 1000+ objects. So it is not a subtree count oracle here; a spec-vs-reality discrepancy worth knowing.
SET_OWNER: advertised, but rejects critical and lies on downgrade. LDAP_SERVER_SET_OWNER (.2255) is advertised, but sent critical. It returns unavailableCriticalExtension even for an Enterprise Admin; only the non-critical form is honored. An unprivileged caller’s out-of-scope owner request is silently downgraded to the creator — no error, so tooling can believe it stamped an owner it did not. It is properly right-gated (DS-Set-Owner): no escalation, but two behaviors that will mislead an operator or a script.
Tools!
I built two proof-of-concept tools for Red Team and Blue Team to explore and defend LDAP extended controls: https://github.com/dfirdeferred/LDAP-Extended-Control-Tools
| Folder | Tool | Language | Use it to… |
|---|---|---|---|
red/ | offensive CLI (ldapctl) | Python 3 / ldap3 | collect the directory invisibly, make a change survive remediation, and probe existence without logging |
blue/ | defensive module (AdLdapDefense) | PowerShell | audit what your DC actually logs, hunt replication-metadata tampering, deploy & self-test SACL canaries, and catch replication (DirSync/DCSync) abuse |
Takeaways
For defenders:
- Don’t trust “Event 1644 is enabled.” Verify thresholds are 1, and understand it sees only searches, and only some controls within those. Modify/add controls, DirSync, BATCH, and ADWS are blind spots.
- Detect DirSync/DCSync on Event 4662 (the Get-Changes GUIDs), watching Get-Changes, not just Get-Changes-All.
- Put SACL canaries on the crown jewels; it’s the only transport and control-agnostic signal.
- Mark sensitive custom attributes
0x80 | 0x200, not just0x80, and control replication rights.
For researchers: most MS-ADTS controls do exactly what the spec says — the interesting surface is the gap between what they do and what your telemetry shows. The 2 offensive primitives that came out of this — FORCE_UPDATE replication-conflict win and Non Logging of AD Enumeratoin with ObjectSecurity DirSync — are covered separately.