CTF / Boot-to-Root Box
Own a whole machine: recon -> service triage -> foothold -> user.txt -> privesc -> root.txt.
Run everything from the engagement tooling host (e.g. Kali in tmux), capture into targets/<eng>/.
Track progress in targets/<eng>/Approach.md -- work the current phase's open items, mark [x] as each
lands; honor GATE 1 (no hand-rolled exploit before its wiki item is [x]), GATE 2 (no exploit step [x]
without a poc/ image), GATE 3 (exhausted vector -> [!] + one Deadends.md line, never re-run it).
Run python3 scripts/campaign.py next before every exploit step, and done/pass-done after each lands. The driver reprints the required posture (msf-shell rule, searchsploit+msf-first, the RTL stop-tells) and is the forcing function now that the drift-guard block is advisory-only - do not free-hand a box off the board.
Standing mandate: Wiki-first, tools-before-scripts (GATE 1, all phases)
This mandate stands over every phase below. Before writing ANY custom script or recalling an exploit from memory:
qmd_query "<tech/version> exploit"andqmd_query "<service> privilege escalation"via wiki-search MCP. Read matches. If the MCP is down (it drops mid-session), runbash scripts/wiki-query.sh "<tech> exploit"(same qmd index;-kfor an exact CVE) - do NOT skip wiki-first or fall back to grep.- Pull payloads from
wiki/payloads/and chains fromwiki/cheatsheets/attack-chains.md+wiki/cheatsheets/cve-arsenal.md, orSkill(arsenal)to resolve the exact file. - Privesc reference:
wiki/techniques/linux/linux-privesc.md/wiki/cheatsheets/linux-privesc.md(or windows-privesc). Only after the wiki has nothing do you write a custom PoC. Do not reinvent what the wiki already documents.
Tooling home: check /opt/arsenal first for pspy/linpeas/shot/capture. pspy64/linpeas/winPEAS live in /opt/arsenal, seeded by vm-provision.sh from their GitHub releases; our own helpers (shot.py, capture.sh) are pushed on demand by bash scripts/vm-sync.sh <name> from the vault.
Version-pinned tooling -> a THROWAWAY docker container, never mutate the VM's runtime. A tool that needs a specific/older interpreter or an abandoned dependency (e.g. h2csmuggler pins the deprecated hyper lib and only runs on Python <=3.11; a legacy exploit needs node:16) should run inside a disposable container matched to that version, NOT via a host downgrade or a fragile venv that leaves the Kali VM in a broken state for the next box. docker run -it --rm -v $(pwd):/app python:3.11 bash (swap the tag: python:3.9, node:16, ...); --rm deletes it the moment you exit, so nothing persists and nothing on the VM breaks. Keep the VM's system python/tooling pristine.
Anti-pattern: a raw one-shot bash /root/vm.sh '<exploit>' (or an inline node -e/python3 -c payload through it) for a listener, shell, or chained exploit is the smell this mandate exists to catch -- it skips wiki-first and leaves no session to capture. Run persistent/interactive steps in their own named tmux tab instead: scripts/vm-scan.sh <eng> <target> '<cmd>'.
Phase 1 Recon: basic tools only (in this order)
Use the standard toolkit. Do NOT hand-roll recon scripts.
Preflight (every engagement, before scanning): prune the tooling VM's /etc/hosts and /etc/krb5.conf of PRIOR-box entries. The VM persists across boxes, so a stale <ip> <domain>/<realm> line (or a stale default_realm) silently mis-resolves this box while nxc/certipy (explicit IP) look fine; impacket Kerberos hangs on a dead KDC instead. bash /root/vm.sh 'grep -vE "^#|^127\.|^::1|^$" /etc/hosts', delete any line not for this box, add only this target; bash /root/vm.sh 'grep -i default_realm /etc/krb5.conf', blank/reset it.
Tooling-first: rustscan/nmap/feroxbuster/ffuf/nuclei/nxc, never a hand-rolled /dev/tcp or curl fuzz loop (skips the fingerprint router). feroxbuster is the DEFAULT web content-discovery tool (recursive, faster than ffuf/big.txt); launch it the moment nmap shows a web port; ffuf is for param-mining + vhosts.
Card EVERY scan tab AS it finishes, not at the end: scripts/capture.sh recon <eng> <slug> <tab> renders the tmux tab into recon/, rustscan, nmap, feroxbuster, ffuf, nuclei, whatweb (its own tab). Even an empty result gets a card. status.py surfaces the recon-card count.
ONE tmux session per engagement, one WINDOW per parallel scan. bash scripts/vm-scan.sh <eng> <target> '<scan>' for the first, then --win nuclei/--win ferox/--win whatweb for the rest on the same host; never bump the session name to dodge a collision. Screenshot a tab with Skill(screenshot) --tmux <eng>:<tab>.
vm.sh drops long foreground commands (exit 255, no output) past ~2 min. Run scans/cracks/sprays DETACHED (a tmux tab via vm-scan.sh, or nohup <cmd> >/tmp/out 2>&1 & + poll for a DONE marker), never blocking one vm.sh call. Stdin is not forwarded through vm.sh; push files via base64 (echo <b64> | base64 -d > ~/file), write+run in one call.
Never pkill -f <pattern> with a pattern that also matches your OWN driver/ssh command - it kills your own tooling (a pattern on the target IP can match the ssh session carrying it).
T=<ip>
rustscan -a $T --ulimit 5000 -g # fast full-port sweep -> open-port CSV
nmap -p<found> -sCV -Pn $T -oN nmap-svc.txt # version/script scan on rustscan's hits
nmap -p- --min-rate 2000 -T4 -Pn $T -oN nmap-all.txt # full-TCP confirm (own tmux tab)
nc -nv $T <port> # manual banner / custom-proto services
dig any @$T <domain>; dig axfr @$T <domain> # DNS if 53 open / vhost hints
nxc smb $T -u '' -p '' --shares # SMB: netexec first (fingerprint router), not smbclient
nxc smb $T -u 'guest' -p '' --shares --rid-brute # guest fallback + RID-cycle user enum
smbclient -N //$T/<share> -c 'recurse;ls' # smbclient only to pull a specific file/share
# --- web ports found: feroxbuster (primary) + nuclei + whatweb IN PARALLEL, own tmux tab each ---
feroxbuster -u http://$T -w /tmp/harness-paths.txt -x php,txt,log,sql,bak,zip,env,old,conf -d 2 --no-state -o ferox.txt # our high-signal list first
feroxbuster -u http://$T -w /usr/share/seclists/Discovery/Web-Content/raft-large-words.txt -x php,txt,log,bak -d 2 --no-state # then the big list
bash scripts/backup-sweep.sh http://$T ferox.txt # full-filename backup suffixes (.bak/~/.old/...) ferox -x misses; BURP_PROXY=127.0.0.1:8080 to route via Burp
ffuf -c -u "http://$T/?FUZZ=x" -w scripts/wordlists/harness-params.txt -fs <baseline> # param mining
ffuf -c -u http://$T/ -H "Host: FUZZ.$T" -w <vhost-wordlist> -mc all -o vh.json # vhosts: diff redirect Location, don't filter by code (a decoy vhost 302s the same place; the real one differs)
ffuf -c -u "http://$T/~FUZZ/" -w /usr/share/seclists/Usernames/top-usernames-shortlist.txt -mc 200,301,403 # Apache userdir (~name) - no dir wordlist carries these, fuzz explicitly
nuclei -u http://$T -o nuclei.txt
whatweb -a3 http://$T ; curl -s -I http://$T # own tmux tab so it gets a recon card
wpscan -u http://$T # if WordPress
Missing -w wordlist = ffuf prints help text and silently finds NOTHING (a false "no hidden endpoints"). Preflight every fuzz (ls "$W" || W=/usr/share/wordlists/dirb/big.txt); seclists may be absent on the VM, push harness lists over via base64 first.
Slow vs worker-starved, don't conflate them. Fast HTTP 000 under concurrency (ffuf/sqlmap/parallel scans) = the box is WORKER-STARVED: back off to serial, one request per call, drop fuzzers, let it drain. A request that just never returns = the box is SLOW, not dead: raise the timeout to 120s+ and be patient (a heavy CMS/backend page can take 60-90s). Tell: nmap shows "filtered" but a single nc/curl connection succeeds = it drops fast-scan probes, switch to gentle serial mode rather than declaring it dead.
On any TLS host, dump the cert SANs early; a hidden vhost that decoy services and ffuf/feroxbuster never surface can be listed only in the Subject Alternative Name, see [[cdn-waf-bypass]].
On any web surface, before moving to exploitation, all of this, in parallel where possible:
- Capture it as-is first:
capture.sh web <eng> <slug> http://T:PORT/(browser shot) +curl -s http://T:PORT/ > poc/<slug>-source.html(raw source), for EVERY distinct surface (login, dashboards, OSINT/social apps) as you open it. - Read full, not grep. Fetch a file/response (source, config, HLS manifest, JS bundle) and read it END-TO-END; the vuln hides in the line a narrow
grepskips. Never pipe an exploit/lead response throughhead/grepbefore reading it whole. - Exploit requests -> Burp. curl is fine for quick loops; push every load-bearing request (SSRF/LFI/SQLi/BFLA/deser/flag-returning) into Repeater via
Skill(hunt-burp), card withcapture.sh burp. - Source-read primitive first. The moment you can read files (LFI/.git/backup), read ALL the app source before brute-forcing a login/DB; the vuln is usually in source you can already read.
- Read the client JS and snippet what it reveals. Grep bundles for
fetch(/axios//api/token/secret/admin; a JS/SPA's POST-only JSON API is invisible to feroxbuster/ffuf (they GET, a bare/api404s so recursion never descends). The moment source reveals something load-bearing, capture it:scripts/capture.sh snippet <eng> <slug> <url-or-file> '<pattern>' '<what it reveals>'-> paste the fenced block into walkthrough.md Recon. - Launch scanners in parallel the moment a web port appears, feroxbuster + nuclei + whatweb, one tmux tab each, don't wait serially; card each on finish. Never conclude "no web vuln" until both feroxbuster and nuclei have run AND been read.
Fingerprint the exact app + version. Skill(arsenal) FIRST on any fingerprinted surface (maps it to wiki/tools/ automation), then INVOKE Skill(hunt-<class>) for each fingerprinted vuln class; actually load and follow it, not just the router's advisory nudge (a routed-but-never-invoked hunt skill is drift in eval.md). Record HOW each vuln was discovered, not just the exploit; screenshot findings as they land. OT/ICS ports (502/102/44818/1880, an HMI web app) -> Skill(hunt-ics). A web surface -> scripts/recon-web.sh <eng> <url> (fans out feroxbuster+nuclei+whatweb+backup-sweep); re-run per new vhost/path, nothing auto-launches it.
Phase 2 Weaponize: pick the exploit
- Version-known -> [[wiki/tools/searchsploit]] AND [[metasploit]] FIRST (the quick-win reflex). The instant a service is fingerprinted to a version, run BOTH before hand-rolling or deep-diving a CVE:
searchsploit <app> <ver>(local Exploit-DB;-m <id>to copy a PoC,-xto read it) andmsfconsole -qx "search <app>; exit"(a readyuse-able module = often an instant shell). A matching msf module or a copy-pasteable searchsploit PoC beats writing your own. Cross-check with the wiki CVE lookup ([[cve-arsenal]] · [[metasploit]]); prefer the documented/ready PoC over a fresh one. (GATE 1 still holds: the wiki item for the tech is[x]before a hand-rolled PoC - but a canned searchsploit/msf exploit for a known version IS the wiki-blessed tool, use it.) For the full search->use->check->runsequence and DB-backed recon, drive it throughSkill(metasploit)rather than free-handingmsfconsole. - Treat a documented exploit tool as a BLACK BOX: use its interface, do not read its source. For a known/public tool or PoC (e.g.
[[wp2shell]], a searchsploit/msf module, a vendor advisory PoC), drive it through its documented CLI as a trusted reference. Opening a tool's full source to study its mechanics wastes tokens and can stall the engagement on the safety classifier; keep your understanding at the WHAT-it-does / HOW-to-invoke level (its wiki page), not the source level. When a step needs a PoC pulled from GitHub (or any external exploit fetched/run), pause for the OPERATOR's nudge - the operator supplies the authorizing context at that point rather than the agent deriving the mechanics itself. - Pick the payload set from
wiki/payloads/for the fingerprinted class (GATE 1: the wiki item is[x]before you hand-roll a PoC). - Stage the chosen exploit/PoC into
targets/<eng>/poc/scripts/before firing, so the code and the run are captured together. - A fingerprinted service can expose SEVERAL vuln classes - do not tunnel on the first. One plugin/app often has an SQLi AND a file-read AND an authed RCE; the intended door is usually the CHEAPEST (a source/config read), not the loudest (a blind SQLi you grind for hours). Two mechanical tells that the vector you picked is the WRONG one - switch it, do not tune the tooling (see [[hunt-core]] Stop conditions): (1) the box starves under your own exploit loop (repeated
000/timeout/empty-reply) - a vector that DoSes a lab box is almost never intended; (2) two verified hashes in a row fail the wordlist - the passwords are delivered out-of-band (email/note/KeePass/config), so stop cracking and read the app's OTHER surfaces (LFI/source, a second vhost, mail) for where creds are handed out. Re-fingerprint the asset for untested classes (coverage/campaign.py board) before extracting a third hash. When the next door is not obvious, callSkill(redteamlead)for wiki-grounded ranked directions with a STOP - one RTL call the moment a vector fights back beats an hours-long sunk-cost grind (do not wait to be told; the box's/redteamleadreminder is a real instruction).
Phase 3 Deliver: land a shell
- Diagnose target->attacker EGRESS before committing to a shell channel (recurring time-sink). A connect-back that silently fails is usually a FILTERED egress port, not a broken payload - common C2 ports (4444/1337) are often blocked while 443/80/53/8000 pass. Test it FIRST with one clean single-command probe (a
curl/wgetto your listener port, orbash -c 'echo x >/dev/tcp/<you>/<port>') across a couple of ports; if connect-back is filtered, DO NOT grind ports or a reverse shell - pivot to an HTTP-pull + one-shot-webshell/SSH channel (the box's "beacons OUT on a schedule" theme is a literal hint the network is egress-shaped). Deliver the shell payload base64-wrapped or as a hosted script run via a download cradle, NEVER a nested-quotebash -i >& /dev/tcp/...one-liner through thevm.sh -> ssh -> --cmdbridge (the quotes mangle and you cannot tell a dead port from a mangled payload - same delivery discipline asvm-rsh.shfor commands). - Deliver the staged payload/exploit against the target; prefer the documented PoC over a fresh one.
- Stabilize EVERY Linux shell the moment it lands (a raw
ncshell has no job control, no arrow keys, no tab-complete, and Ctrl-C kills it). Full TTY-upgrade dance, in order:python3 -c 'import pty;pty.spawn("/bin/bash")'(fall back topython/python2ifpython3is absent;script -qc /bin/bash /dev/nullif no python) ->Ctrl+Zto background ->stty raw -echo; fg(hands your terminal's raw mode to the shell; press Enter twice) ->export TERM=xterm(fixes clear/less/vim). Then setstty rows <R> cols <C>to your localstty sizeif editors wrap. Or better, drop your SSH key into a writable user's~/.ssh/authorized_keysfor a resilient, already-interactive session. (Windows shells: no PTY dance; grab a proper shell via a C2/ConPtyShellor just RDP/WinRM once you have creds.) - Catch every reverse-shell pop through
msfconsolemulti/handler- never a rawnclistener by default. A rawncPTY has the footgun thatCtrl-Ckills the LISTENER (dropping the shell back to your own prompt - the false-RCE trap) and needs quoting gymnastics to drive. Default the payload to meterpreter (linux/x64/meterpreter/reverse_tcpetc.); when meterpreter is blocked or unstable - routine on Windows/EDR, where the stager dies but a plain shell survives - fall back to a plainshell_reverse_tcppayload still caught inmulti/handler(a barenc/vm-scan.sh --win shelllistener is the last resort). Meterpreter also givesautoroute/portfwd/socksto reach internal-only ports - use that before hand-rolling SSH-Lforwards.msfconsole/msfvenomare on the VM. (An already-interactive SSH/evil-winrm/cred session needs none of this - drive it directly.) Drive the whole handler setup + catch throughSkill(metasploit). - Drive commands into the landed shell (reverse shell or tmux tab) with
scripts/vm-rsh.sh <eng> '<cmd>'- it base64-wraps the command so any metachars survive the vm.sh -> ssh -> tmux bridge and returns clean output. NEVER hand-quotetmux send-keys+capture-pane, or/dev/tcpfile-transfers, to run commands; that quoting hell is a known repeat drift. - On FIRST foothold, stand up ONE persistent multiplexed channel and reuse it. SSH
ControlMaster/ControlPersist(plus-Lforwards for internal-only ports), or a reverse shell that survives home-perm resets. Opening a fresh SSH per command through an unstable VPN tunnel was the dominant wall-clock sink on a recent box. See [[pivoting]]. - Cred-reuse FIRST. Capture creds to
targets/<eng>/loot.mdimmediately; try reuse (su / ssh / other services) BEFORE hunting new ones. DB/web creds are very often reused for SSH.- Spray EVERY secret you hold against EVERY user x surface, not just the newest one. The drift: a cred gets written to loot.md, one obvious target is tried, and the rest of the matrix is never run while a fresh privesc vector is ground for half an hour. Re-run the FULL matrix each time a new secret lands.
suand SSH are NOT equivalent -PermitRootLogin nomakesssh root@fail whilesu rootwith the same password succeeds, so a "root: []" SSH result is NOT a negative for root (needs a PTY:pty.spawn/script -qc). - The leaked config is not the live secret. An app that does
load_dotenv()/os.environ.get(...)keeps its REAL password in a.envbeside the source, not in the seeded.sql. Read every config the app actually loads, and diff it against what the leak gave you. - Crack the hashes you ALREADY hold before hunting a new vector. A DB account with
ALL PRIVILEGESlets you read the DBMS's own account table; a second, otherwise-invisible account there is a deliberate lead. MySQL 8caching_sha2_password($A$005$...) is hashcat-m 7401(see [[password-cracking]], [[mysql]]) - convert$A$<iter>$<20-byte salt><43-byte digest>to$mysql$A$005*<salt-hex>*<digest-hex>. A DBMS password is a prime candidate for OS-account reuse.
- Spray EVERY secret you hold against EVERY user x surface, not just the newest one. The drift: a cred gets written to loot.md, one obvious target is tried, and the rest of the matrix is never run while a fresh privesc vector is ground for half an hour. Re-run the FULL matrix each time a new secret lands.
Phase 4 Exploit: finish the foothold, then privesc
RCE-first (impact-first). When more than one vector is open, take the one that yields code-exec first - an RCE foothold is the attack vector that carries impact. Grind XSS/IDOR/enum only after code-exec is ruled out or the objective specifically needs them (the driver now ranks code-exec classes ahead of access/enum rows).
Web foothold = STAY in Burp (anti-drift). If the foothold is an HTTP primitive (RCE/LFI/SSRF/authed
API), the recurring failure is abandoning Burp the instant it lands and scripting the ENTIRE
post-exploitation over raw curl/vm.sh/urllib - so the operator, who is watching Burp, loses all
visual on the exploitation. Keep the load-bearing follow-ups (the injection that reads user.txt, the
authed call that leaks config, each privesc-relevant fetch) in Burp Repeater (native mcp__burp__*
first, CLI bridge fallback; Skill(hunt-burp)). Quick throwaway enumeration loops over the bridge are
fine; the requests you'd screenshot are not throwaway. See CLAUDE.md "Burp-first does NOT stop at foothold".
STOP-and-think the moment ANY shell lands (before reaching for a shortcut). The recurring failure is following the FASTEST path instead of the INTENDED one, then going off the rails. Pause and:
- Read the box's THEME/name + any notes/creds/files you already have - CTF boxes telegraph the
intended vector (a module lesson, a service left on, a labelled secret).
qmd_querythe wiki for that theme + the exact fingerprinted tech before acting. - Enumerate the INTENDED escalation surface FIRST:
sudo -l, writable configs/cron/timers/scripts you own, group memberships, service creds - the deliberate path is almost always one of these andpspy/linpeassurface it in seconds. When a user is insudo/wheelbut the password is nowhere findable AND cron/writable/SUID/group vectors dead-end, READ/etc/pam.d/{sudo,su}and/etc/sudoers.d/*(not a grep) - apam_ssh_agent_auth(hijack a forwarded agent socket in/tmp/ssh-*/agent.*-> passwordless sudo),pam_exec, or bareNOPASSWDline IS the vector; a user conspicuously inlxd/dockerthat's unreachable is often a DECOY (see [[linux-privesc]]). Read linpeas output WHOLE - the pam/sudoers tell is there but a keyword grep skips it. - Kernel-LPE arsenal (dirtyfrag/copyfail/PwnKit/...) is a LABELLED FALLBACK, not the opening move.
We DO carry instant-escalation payloads for old kernels ([[privesc-exploit-arsenal]], [[dirty-frag]]) -
note the
uname -rband as a fallback, but VERIFY the intended path is genuinely absent AND verify the CVE precondition (userns/module/patch-band) before firing. A shortcut that works is fine; taking it BEFORE checking the taught vector is the drift. Once you have full root, look back at what was INTENDED and record it (walkthrough + Deadends), so the fast win still teaches the lesson. The confirmed compile-and-run itself (cross-compile the PoC, deliver it, execute, verify) is aSkill(delegate)hand-off - fully specified once the CVE/precondition is verified, cheap-model-shaped. On an active meterpreter session, runpost/multi/recon/local_exploit_suggesterfirst (viaSkill(metasploit)) to surface the candidate before delegating the run.
Finish the foothold first (leftover Rule 2 techniques, if the shell landed mid-app):
- Consistent escalation primitive: if a box exposes the SAME pivot at each stage (a Docker socket/TLS pivot per level, an SSRF each hop), verify/exhaust that intended vector end-to-end BEFORE an opportunistic shortcut (raw device mount, a memory CVE). A shortcut that relies on
--privileged/a loose cap can mask that a hardened config would block it, and it is less reproducible/auditable. Getting the flag via a shortcut is fine; SKIPPING the taught vector without testing it is the miss. - Web SQLi: in-band (UNION/error) before blind; test EVERY quote context (
'"numeric) and second-order (a stored value used unsafely on another page). If the app hashes the password inside the query, read plaintext frominformation_schema.PROCESSLIST. LoadSkill(hunt-sqli)/ see [[sql-injection]]. - Recovered unsalted MD5/SHA1: online lookup first (CrackStation/hashes.com) before hashcat/john. A hash that resists everything on a hard box may be a deliberate decoy; pivot, don't grind.
- A custom binary that signs its own protocol (HMAC/token): decompile the KEY SCHEDULE before brute-guessing key/message combos. When an implant/agent/updater HMACs its IPC/C2 and the key is derived from readable material (a
.rodatablob, a PRNG pad with a hardcoded seed,/etc/machine-id), reproduce the derivation offline (the derivation functions are usually tiny) and VERIFY against one captured live signature, then forge - do not spend rounds guessingblob/machine-id/HMAC(blob,mid)permutations. Recognize the glibcrand()LCG (*0x41c64e6d + 0x3039, byte(state>>16)&0xff). See [[cryptography-attacks]] "binary signed-protocol forge".
Then privesc = pspy ALWAYS + linpeas/winpeas, then manual. Run a long tool reliably with
bash scripts/vm-bg.sh <eng> <win> '<tool>' - it stages the tool to /dev/shm, runs it inside the
stabilized tmux shell (line-buffered to a /dev/shm/*.log), and you --read / --wait 120 the
logfile. Never hand-roll a backgrounded watcher over the one-shot SSH bridge (it gets reaped / tee
buffers), and never retry a broken tool-execution pattern >2x - switch method (vm-bg) or
Skill(redteamlead). See [[pspy]] / [[linpeas]].
pspy shows exec/process events, NOT unix-socket/IPC beacons. If the privesc lead is an internal listener/socket (a service "talking to itself" on loopback), RE and interact with THAT directly - do not grind pspy or stage linpeas for a socket-protocol vector they cannot see. Run pspy when the suspected vector is cron/timer/exec.
Always run pspy first to catch root-run background jobs/cron/timers that static checks miss:
# upload pspy64 (host it on the tooling box, curl/wget it down) and watch >=60s
./pspy64 -pf -i 1000 # see every exec + file event as root; reveals per-minute cron/systemd timers
Then the automated sweep:
./linpeas.sh # linux (winpeas.exe / winpeas.bat on windows)
# AVOID `-a` (deep/thorough) on small boxes (<=1-2 GB RAM, most THM VMs): the full scan can
# OOM-thrash the target until every service (ssh/http/db) times out - looks exactly like the box
# crashed. Use the default scan, or throttle: `nice -n19 ionice -c3 ./linpeas.sh`. If it does hang
# the box, `pkill -9 -f linpeas` (kill it as the user that launched it) and load drains in seconds.
Capture linpeas/pspy TWO ways: a --term screenshot of the highlighted findings (Skill(screenshot),
the colored hits survive) AND the FULL text log - redirect the tool to a file, then
scripts/capture.sh log <eng> <slug> /tmp/linpeas.txt pulls the whole scan (ANSI stripped) into
poc/NN-<slug>.md. The screenshot is unreadable past one screen; the .md keeps every line so you
(and the operator) can grep it later. Do this for full nmap output too when it is long.
Read-whole is not just for recon: grep is NOT the read for PAYLOAD RESULTS or pspy/linpeas output either. A UNION/echo dump can reflect in an unexpected phrasing (singular "1 word" vs plural "N words"), and a root cron hides in the full pspy stream; read the whole result block, not a keyword grep.
Then walk the manual checklist (do not skip any; the box's intended path is usually ONE of these):
| Check | Command | Win = |
|---|---|---|
| sudo rights | sudo -l |
NOPASSWD/GTFObin -> root |
| SUID/SGID | find / -perm -4000 -o -perm -2000 2>/dev/null |
GTFObins |
| Capabilities | getcap -r / 2>/dev/null |
cap_setuid etc. |
| Cron | cat /etc/crontab /etc/cron.d/*; ls -la /etc/cron.* + pspy |
writable/relative-path script |
| systemd timers | systemctl list-timers --all + systemctl cat <svc> |
writable ExecStart script run as root (e.g. THM Ollie feedme.timer) |
| Writable root files | writable scripts/units root executes; /etc/passwd writable |
inject payload, wait for trigger |
| Groups | id (docker/lxd/disk/adm/shadow) |
group->root GTFO |
| Creds | configs, history, DB, .ssh, backups |
reuse / su |
| Kernel/pkg CVE | uname -r; pkg versions (pkexec/polkit, sudo, dbus) |
LAST resort - check the patch level first ([[linux-privesc]]) |
Box-specific chains now live in wiki (see the technique pages linked per class in approach-notes.json REFS).
Don't assume the flag path (e.g. /root/root.txt); once you have exec, find it - the root flag can live at a non-standard path.
Capture (engagement discipline)
After each phase, write to targets/<eng>/: hosts/access -> state.md, creds -> loot.md, chain -> state.md's ## Chain section, vulns -> Vuln-index.md, dead-ends -> Deadends.md, narrative -> walkthrough.md (at close-out). Flags go in the writeup, never in session/* or wiki/.
Board hygiene, live (GATE 3). state.md's ## Chain section and Deadends.md rot under momentum while other state.md prose absorbs the narrative instead; the instant a vector (a potato variant, a kernel CVE, a cred spray) is exhausted, append ONE Deadends.md line + set its status in state.md's ## Chain BEFORE trying the next. status.py shows board phase + deadend count to spot the drift.
Live-capture machinery; evidence is never backfilled at close-out:
- Auto-card is a backstop, not primary. The Stop hook (
scripts/autocard.sh, cappedAUTOCARD_MAX=2/run) renders any finished scan tmux tab intorecon/; hand-carding as you go (Phase 1:capture.sh recon <eng> <slug> <tab>) stays PRIMARY. 0 cards while tabs finished = VM down /timeoutmissing; hand-card now. - Hand-card exploit-state shots (the flag, the RCE firing, an authed panel) the moment they land; persist to
state.md/loot.mdimmediately, never deferred. - A PoC card is ONE human-authored command, concrete values, full paths, NO
export/$VAR, NO;/&&-chains, NO echo banners, NO base64/pty wrappers. Re-run the clean single command for the capture even if a messy pipeline was needed to work the box (that stays inpoc/scripts/).
walkthrough.md (assembled at close-out) is the narrative and the clean human version (concrete one-liners, no $VARs); the messy automation (base64 wrappers, pty helpers) stays in poc/scripts/.
Read UI/source hints literally before fuzzing. A placeholder, button label, or leaked source usually states the intended input format directly; do not default to injection-fuzzing a field that already told you its format.
Locally-substituted payloads = false-positive RCE. $(...)/backticks/$VAR sent through the VM bridge or any local shell get substituted LOCALLY before reaching the target, and the tooling VM runs as root; a reflected uid=0 may be YOUR box. Always single-quote or base64 injection payloads; confirm execution with a target-only marker (hostname, a file only it has) before claiming RCE.
Preserve exploit scripts and source reads. Copy any exploit script or read target source into targets/<eng>/poc/scripts/, saved as <name>.md with a fenced code block (sh/js/py/html), NOT a bare .sh/.js/.py, since Obsidian only previews .md/images. capture.sh snippet <eng> <slug> <url-or-file> '<pattern>' '<reveals>' for a targeted excerpt.
Video/media -> mp4 into poc/ early. ffmpeg -i <in> -c copy poc/<slug>.mp4, then hand it to the operator; a visual puzzle (shoulder-surf, on-screen code) is far cheaper read by a human than brute-analyzed frame-by-frame; frame extraction/OCR is the fallback, not the opening move.
Screenshot every successful step live, not at the end. Capture EACH privilege milestone (foothold / user / operator / root) the MOMENT it lands - a fast box is not an excuse to defer capture to walkthrough time (a transient exploited state may not be re-shootable, and backfilling evidence is the documented drift). No auto-capture net exists; scripts/capture.sh (ev/req/tmux/burp) captures straight into poc/ the moment a step lands. capture.sh ev <eng> <slug> "<url>" "<cmd-label>" for a one-call output+request card; capture.sh req <eng> <slug> -- <curl-args> for a full request/response card; capture.sh tmux for a session card; capture.sh burp for a Repeater PoC. Skill(screenshot) covers authed/exploited GUI states. Never hand-write a --term card (fabricated evidence); tee real output into the pane if it was redirected to a file. python3 scripts/build-walkthrough.py <eng> auto-populates the ## Evidence gallery from every poc/ card.
Grow the harness wordlist. A non-obvious route/param that cracked the box -> python3 scripts/wordlist-suggest.py (leak-safe) then scripts/wl-add.sh paths <token> / wl-add.sh params <name>, generic methodology names only, never client branding.
Leave-no-trace at close-out (pentest/real engagements only; optional on CTF/lab). Before marking a real engagement done, remove what you dropped on the target: planted SSH keys in authorized_keys, uploaded tools (pspy/linpeas/webshells) in /tmp /dev/shm /var/www, staged exploit scripts, and any throwaway account you created. Track droppings as you plant them so cleanup is a checklist, not a memory test. (On a CTF this is optional - note it, don't grind.)
Context tools
- [[wiki/tools/nmap]]
- [[wiki/tools/rustscan]]
- [[naabu]]
- [[wiki/tools/ffuf]]
- [[wiki/tools/feroxbuster]]
- [[gobuster]]
- [[wiki/tools/nuclei]]
- [[nikto]]
- [[wiki/tools/whatweb]]
- [[arjun]]
- [[dalfox]]
- [[swaks]]
- [[wiki/cheatsheets/recon]]
- [[nuclei-arsenal]]
- [[wordlists]]
- [[network-services]]
- [[linux-enumeration]]
- [[windows-enumeration]]
- [[windows-privesc]]
- [[password-attacks]]
- [[sqlmap]]
- [[pivoting]]
- [[web-client-attacks]]
- [[metasploit]]