| name | screenshot |
| description | Capture web-page / PoC screenshots into the engagement evidence (targets/<eng>/poc/) and embed them in walkthrough.md. Live pages + authenticated/exploited states (post-login dashboard, the flag page, an SSTI/cmdi render). Runs chromium on the Kali tooling host (VPN path to targets); hands off to the evidence skill for redaction before a real report. Use when building PoC evidence or asked to "take a screenshot / capture the PoC". |
Screenshot: visual PoC capture
Turn the text walkthrough into evidence. Capture runs on the Kali tooling host (it has the VPN
route to in-scope targets); the PNG is then pulled into the vault under targets/<eng>/poc/. Driver:
scripts/shot.py (chromium headless). For redaction-before-report use Skill(evidence).
One evidence dir, captured by hand. poc/ = the curated story you choose to show (login, the
exploited state, the flag). ALL PoC evidence is now captured MANUALLY and LIVE via capture.sh straight
into poc/ the moment a step lands - there is no auto-capture net.
Scope check
Only screenshot in-scope hosts (read targets/<eng>/scope.md). On passive_only RoE a screenshot
is fine (it's a GET you already make). Never screenshot a host you would not curl.
What to capture: EVERY breakthrough, as it happens (with retries)
Screenshot the moment each step LANDS, not at the end - the box may expire. A breakthrough = anything
you'd put in the report: the entry page, a leaked secret / useful source (deobfuscated JS, a
config, an app.log), the cipher / decrypt response, the vuln firing, the authed/exploited
state, the flag. Tell the arc (Entry -> Trigger -> Impact) AND capture the intermediate proofs
(the leak, the crypto response) - those are exactly what a reviewer disbelieves without a picture.
- Always show the URL. Every web/source capture must carry its address bar so the image is
self-identifying - which page/path it is (
https://T/folder/folder/log.md), not an anonymous blob.
Live <url> mode adds it automatically; for --html/--term of a fetched resource pass
--url-bar "<full URL>".
- Retry (box is up). Renders fail transiently (nav timeout, cold chromium). Retry 2-3x and confirm
the PNG is non-empty (
[ -s out.png ]) before moving on; while the box is up, go back and re-capture
anything you missed rather than shipping evidence with gaps.
Name NN-slug.png (NN = step order) so the evidence reads top-to-bottom.
Fastest path: capture.sh ev (one call, live - USE THIS by default)
Batching captures at the end of a box is the failure mode: you lose transient state AND the live
situational awareness that helps pick the next move when stuck. The ev mode of scripts/capture.sh
collapses the whole capture (render a card showing command + request URL, pull the PNG into poc/,
print the md ref) into ONE call, so live capture has no friction. cmd and url are REQUIRED args - no
anonymous card.
bash /root/vm.sh 'python3 /tmp/exploit.py 2>&1 | tee /tmp/poc/flag3.log'
scripts/capture.sh ev <eng> flag3-contract "POST http://T:8545 eth_sendTransaction" "reset()+transferDeposit()"
The card shows $ <cmd-label> in the title bar AND <request-url> in a browser address bar (both
required) so every image says what was run and where. Use the manual shot.py calls below only for the
modes capture.sh does not wrap (a GUI --window/--screen, an authed --html render).
Rendered web page: capture.sh web
The most operator-legible shot is the target as a browser renders it - the actual page, not a curl
card. capture.sh web <eng> <slug> <url> renders the live URL through chromium on the VM (browser-chrome
frame + address bar) and pulls the PNG into poc/. This is the FIRST evidence to grab on any web target
(capture-as-is before poking), and again at each rendered exploit state (an SSTI/cmdi output page, the
flag page):
scripts/capture.sh web <eng> home http://T:PORT/
scripts/capture.sh web <eng> flag http://T:PORT/admin
A dead target makes it fail loud (chromium net::ERR -> non-zero exit, no error-page PNG pulled). For an
authed page (needs a session cookie), curl-save the HTML and render it with the manual shot.py --html --url-bar call below; web is for GET-able live pages.
Request/response leads: capture.sh req
The highest-value CTF/pentest evidence is the real curl request and response for a lead (creds, a
flag, a leaked source) - presentable as full-file PoC. Capture it deliberately, the moment the lead lands:
Full fidelity (capture.sh req): for a request you want as a clean PoC (request line +
headers + body, response status + headers + body), run it through the req mode - it runs curl -sS -iv,
colors the request(>)/response(<) Burp-style, and pulls the PNG into poc/:
scripts/capture.sh req <eng> <slug> -- -sk -X POST https://T:5000/login --data @/tmp/body.txt
For a crypto-forged request (envelope/signature), give the exploit script a --curl mode: it writes
the forged body to a file and prints the capture.sh req args, so the PoC is a reproducible curl, not
"run my python". Never settle for a script-summary card (POST https://T/login + python output) when the
real curl request/response is the artifact a client / writeup needs.
Real tmux-session cards: capture.sh tmux
When the evidence should look like an ACTUAL Kali terminal session (real commands + real output, the way
recon scans are captured), run the proof in a real tmux pane and screenshot the pane:
scripts/capture.sh tmux <eng> <slug> <script.sh> runs the script in a wide tmux pane, waits for its
echo POC-DONE, --tmux-captures the pane (FULL scrollback), colors it, and pulls the PNG into poc/.
Write the script to echo "# comment" and echo "$ <command>" then run the real curl -i/exploit
command. The card colors comment (green) / command (cyan) / response (default) so the three are
visually distinct (shot.py --reqresp, works on --term and --tmux).
NEVER truncate or abbreviate evidence. Show the FULL command, FULL request, FULL response - no ...,
no <result=...> placeholder, no eliding a base64 blob or a long body. A reviewer must verify/reproduce
from the image ALONE. The tooling enforces this (capture.sh tmux captures full pane scrollback via
--history and never caps rendered lines); keep the same discipline in the script content - paste the
real value, never summarize it.
Capture (run on Kali via vm.sh)
Push the script once, then shoot. T = target host, PORT = web port, <eng> = engagement dir.
B64=$(base64 -w0 scripts/shot.py); bash /root/vm.sh "mkdir -p /tmp/poc; echo $B64 | base64 -d > /tmp/shot.py"
bash /root/vm.sh 'python3 /tmp/shot.py http://T:PORT/internal --step 1 --slug login --dir /tmp/poc --caption "NOC login"'
bash /root/vm.sh 'curl -s -b "session=VAL" http://T:PORT/internal/dashboard > /tmp/d.html;
python3 /tmp/shot.py --html /tmp/d.html --base http://T:PORT --url-bar "http://T:PORT/internal/dashboard" --step 3 --slug dashboard --dir /tmp/poc --caption "Dashboard via SQLi"'
shot.py prints saved <path> + a ready md:  line - keep that line.
Terminal-tool + source/log capture (--term)
Render console output OR a fetched source/log/config file as a clean colored card (ANSI converted,
never shown raw). The fancy 2-line shell prompt is auto-stripped so a tmux grab is not a fused
rootnmap -p- mess (--raw keeps prompts if you need them). For a fetched web resource, add
--url-bar so the card shows its URL - a leaked source file is evidence only if you can see where it lives.
bash /root/vm.sh 'nmap -sV T | tee /tmp/o.txt;
python3 /tmp/shot.py --term /tmp/o.txt --cmd "nmap -sV T" --step 1 --slug nmap --dir /tmp/poc'
bash /root/vm.sh 'curl -s http://T/logs/app.log > /tmp/l.txt;
python3 /tmp/shot.py --term /tmp/l.txt --url-bar http://T/logs/app.log --cmd "GET /logs/app.log" -o /tmp/poc/app-log.png'
--maxlines (default 120) caps long output (linpeas); the card auto-sizes to wrapped rows so a long
obfuscated-JS line does not crop the reveal. Then pull the PNG into poc/ like the web shots above.
No auto catch-all: scan output is NOT auto-captured - screenshot the scans worth showing yourself,
and they land in poc/ like every other deliberate story shot (the box's arc). Scans you run detached in
a tmux tab (vm-scan.sh) don't return output to the response, so capture those live with
--tmux <eng>:<tab> once the pane has output.
Run scans in tmux + capture them (nmap/ffuf/nuclei live)
Run each scan in a named tmux tab on the VM (root, persistent - survives the vm.sh call),
one tab per target; multi-web targets get one tab per endpoint:
bash scripts/vm-scan.sh <eng> T 'nmap -sV -Pn T'
bash scripts/vm-scan.sh <eng> T-web-192.0.2.10 'ffuf -u http://192.0.2.10/FUZZ -w wl'
Tab name correlates to the target; dots/colons/spaces become - (tmux target syntax).
Screenshot a live/finished tab as a clean colored card (no display needed):
bash /root/vm.sh 'python3 /tmp/shot.py --tmux <eng>:T --step 2 --slug nmap --dir /tmp/poc'
Target the tab by the @NN window id or the sanitized name (dots/colons became -) that vm-scan.sh printed, not the raw dotted target.
then pull the PNG into poc/ as usual.
GUI / desktop capture (scrot)
For real GUI apps with no text stream (Burp, Wireshark, a browser rendering an exploit)
or the whole desktop:
bash /root/vm.sh 'python3 /tmp/shot.py --window "Burp Suite" --step 3 --slug burp --dir /tmp/poc'
bash /root/vm.sh 'python3 /tmp/shot.py --screen --step 4 --slug desktop --dir /tmp/poc'
shot.py wakes + unlocks the seat session and grabs as the desktop user (root-over-SSH has
no X authority, so a bare scrot fails with "Can't open X display :0"). --window grabs
the named app (even behind a lock); it falls back to full screen if the name is not found.
Use --term for tool STDOUT text, --tmux for a live tmux scan tab, --screen/--window
for GUI windows.
Pull into the vault (Kali -> WSL, base64 over the channel)
The PNG is born on Kali; the vault is on WSL. Transfer each:
mkdir -p targets/<eng>/poc
bash /root/vm.sh 'base64 -w0 /tmp/poc/03-dashboard-after-sqli.png' | base64 -d > targets/<eng>/poc/03-dashboard-after-sqli.png
Embed in the walkthrough
- Drop the printed
 ref inline at the matching step in walkthrough.md.
- Maintain a
## Evidence appendix (one row per shot) so the gallery is browsable:
## Evidence
| shot | caption |
|------|---------|
|  | NOC login (/internal) |
|  | Dashboard reached via SQLi bypass |
Images live only under targets/<eng>/ (gitignored) - never embed images in wiki/ (image-free rule).
Lesson: capture live, not at the end
The whole box was solved (3 flags: supply-chain cred intercept -> OTP crypto crack -> smart-contract
reset()), and only THEN screenshotted by re-running the commands. Two real costs: (1) no mid-engagement
evidence to reason from while stuck - the long dead-end hunting the login username would have been easier
to unstick with the intercepted-creds card and the cipher-response card already in hand; (2) re-running is
unsafe for stateful steps (a spent padding-oracle or a contract transferDeposit() that zeroes a balance
cannot be re-fired for a clean card). Fix: capture.sh ev after EACH landing step, not at the end. Also
banked into shot.py: --term cards budget generous bottom headroom so the last line (usually the
flag/cred) never clips, and every card carries BOTH the command and the request URL.
Preserve the exploit code too, not just the screenshot. When you write an exploit script (a
payload HTML, an escape/forge script, a webshell) or read a target's source, copy it into
targets/<eng>/poc/scripts/ alongside its card, so the reviewer has the code and the state together,
not just a screenshot. Save it as <name>.md with the code in a ```py/```sh/```js/```html fence, NOT a bare
.py/.sh/.js/.html - Obsidian only previews .md/images in the GUI, so a raw-extension file
is invisible to the operator (same reason capture.sh log writes .md and saved source is -source.md).
Redaction (real engagements only)
Before a client report, run Skill(evidence): black-bar session cookies / PII, strip
metadata. The authed-via-curl-to-HTML flow already keeps the session token out of the image (it's in
curl, not the URL bar). CTF/lab: skip redaction.
Output
Report: shots saved (paths under poc/), the ![]() refs added to walkthrough.md, any blocker
(nav timeout -> raise --wait; missing styles -> set --base).