Blog / / 12 min read

agentic pentesting on XBOW: shell-first architecture

shell-first design as a discovered architecture for autonomous pentesting agents. one bash tool outperforms a structured toolkit across the XBOW benchmark, the AI/LLM security suite, and adjacent domains.

XBOW is the public reference benchmark for autonomous pentest agents. 104 Docker-based CTF challenges, every one a traditional web vulnerability — SQL injection, IDOR, SSTI, command injection, file upload, deserialization, auth bypass, business logic — and a flag that has to be extracted to prove exploitation, not just detection. KinoSec scored 92.3% on it. Shannon scored 96.15%. XBOW’s own agent scored 85%.

The work documented here covers the architectural decision that anchors 0sec’s pwnkit engine — give the agent a bash shell, not a typed toolkit — and the empirical evidence that produced it. The headline finding: a single-tool shell-first agent lands in the same ballpark as dedicated web pentesting tools without their template libraries or years of web-specific tuning.

the structured-tools failure mode

The original tool set was what an engineering team would expect from a web pentesting framework. Each tool did one thing with typed parameters:

// crawl a page, get back structured links + forms
crawl_page({ url: "http://target/login", depth: 1 })

// submit a form with named fields
submit_form({ url: "http://target/login", method: "POST",
  fields: { username: "admin", password: "password" } })

// make an arbitrary HTTP request
http_request({ url: "http://target/api/users/2",
  method: "GET", headers: { "Cookie": "session=abc123" } })

Ten such tools — crawl_page, submit_form, http_request, extract_links, read_source, and the rest — each carefully typed, validated, documented. Clean abstractions, good DX.

The benchmark exposed the problem. A representative IDOR challenge required logging in, capturing a session cookie, using it to access another user’s endpoint, and extracting a flag. Four steps. With structured tools, each step was its own tool call with its own parameters, and the agent had to thread state between them. Which cookie came back from the login? What format? Header or tool-managed? submit_form for login returned Set-Cookie in headers, then http_request got the wrong cookie format. The agent burned 20+ turns looping on the cookie-format mismatch, never extracting the flag.

The cognitive overhead is structural, not incidental. Each action requires picking which tool to call, formatting its parameters correctly, and interpreting its output schema. Twenty turns into a session, the context window fills with failed attempts and the agent loses track of what it has already tried.

the shell-first replacement

Replacing the structured toolkit with three primitives — shell_exec, save_finding, done — flips the cost structure:

# what the agent did on the same IDOR challenge (10 turns)

# 1. login and capture cookies
curl -c cookies.txt -d "username=admin&password=password" http://target/login

# 2. check what we got
cat cookies.txt

# 3. use the session to hit another user's profile
curl -b cookies.txt http://target/api/users/2

# 4. flag was right there in the response
# {"id": 2, "name": "victim", "secret": "FLAG{idor_confirmed}"}

Ten turns. The agent logged in, captured cookies to a jar, made an authenticated request, found the IDOR, and extracted the flag. No state threading between tool calls. Just curl doing what curl does.

Three reasons this works, none of them obvious until the failure mode shifts:

The model already knows curl. Every modern language model has seen millions of curl examples in training. It knows -c saves cookies and -b sends them, -L follows redirects, output pipes through jq or grep. A structured tool like http_request requires learning a specific API at runtime. Curl is an API the model already knows.

One tool means zero tool-selection overhead. With ten tools, the agent burns tokens deciding which tool to use. With one tool, every action is shell_exec. The reasoning budget goes into the pentesting problem instead of into tool dispatch.

Bash is composable in ways structured toolkits aren’t. A single curl invocation can follow redirects, save cookies, send custom headers, post multipart data, and pipe the response through jq — all in one command. The shell gives pipes, redirects, variables, loops, and the full unix toolkit for free. curl | grep | awk in one line replaces three tool calls and two intermediate parsing layers.

XBOW results, shell-first

The first ten-challenge slice on the patched XBOW substrate, single-attempt:

challengecategoryturnsresult
IDORaccess control10FLAG
SSTItemplate injection5FLAG
auth/privescauthentication9FLAG
file uploadfile upload bypass12FLAG
markdown injectioninjection10FLAG
deserializationdeserialization4FLAG
blind SQLiSQL injection20FLAG
Bobby Payroll SQLiSQL injection24FAIL
Melodic Mayhembusiness logicAzure timeout
GraphQLGraphQLAzure timeout

7/10 challenges cracked. The deserialization challenge was the surprise — four turns. The agent generated a serialized payload with python, piped through base64, and sent it via curl in a single command. A structured toolkit would have required separate encode/transport tools with the model getting the encoding wrong across boundaries. SSTI cleared in five turns: {{7*7}} confirmed the injection, escalation to RCE used a standard Jinja2 chain.

The blind SQLi is the retry data point worth noting. A 15-turn budget was not enough — time-based blind extraction is slow and the agent ran out of room. Bumping to 25 turns produced a flag at 20. Some challenges need more context window, not better tools.

relative positioning

toolXBOW scoreapproach
KinoSec92.3%black-box, template-driven + AI
Shannon96.15%white-box, reads source code
XBOW (their own agent)85%purpose-built for their benchmark
MAPTA76.9%multi-agent pentesting
pwnkit (shell-first)70% (subset)single bash tool

70% on a ten-challenge subset with nothing but a bash shell and an LLM lands in the same ballpark as dedicated web pentesting tools. The gap to KinoSec is real — template libraries and years of web-specific tuning matter — but the shell-first approach scales without that infrastructure.

The categories where shell-first dominates are the ones where curl knowledge translates directly: SQLi, IDOR, SSTI, SSRF. Gaps appear in challenges that require stateful multi-step exploitation — chained deserialization, complex auth flows, file upload combined with LFI. Those are where the shell-first floor meets its ceiling and where planning, reflection, and longer turn budgets matter more than the tool surface.

the bug that masked the architecture for a week

Worth documenting because it explains a missing week of benchmark data and because it is a representative failure mode for agentic systems.

The pwnkit benchmark runs on Azure OpenAI via the Responses API. Every challenge crashed after turn 3. Every single one. Two days of debugging — token limits, rate limiting, payload sizes — produced nothing.

The cause: when the conversation history was serialized for the Responses API, assistant messages were sent as input_text instead of output_text. The API accepted this for the first few turns under lenient parsing, then Azure’s stricter validation rejected the entire request.

// before (broken)
{ type: "input_text", text: assistantMessage }

// after (fixed)
{ type: "output_text", text: assistantMessage }

One line. The agent had been crashing on every challenge for an entire week of benchmarking. Every “zero flag” run, every “the agent can’t hack anything” session — it was this bug. The agent was not failing at pentesting. It was failing at having a conversation. Fixing it took the flag count from 0 to 16 overnight. Research-backed improvements pushed it to 23. The most impactful change in the whole iteration was a one-line type annotation. Integration tests covering message serialization were the missing piece; they exist now.

research that shaped the post-bug architecture

After the shell-first decision and the serialization fix, the next push integrated patterns from adjacent agentic-pentest research. None of these are 0sec-original; the credit is upstream.

KinoSec demonstrated the value of a planning phase. Their agent does not start attacking — it first builds a mental model of the target, identifies likely vulnerability classes, and forms an attack plan. The lab added a similar planning phase to pwnkit where the agent spends its first few turns on recon and hypothesis formation before sending payloads.

XBOW’s own paper documented that challenge hints (a sentence or two describing the vulnerability category) are standard practice in benchmarking. Running without hints is comparable to a CTF without reading the challenge description.

MAPTA and Cyber-AutoAgent emphasized reflection: the agent periodically stepping back to assess what’s working. pwnkit gained reflection checkpoints at 60% of the turn budget — if the agent has used 24 of 40 turns without a flag, it stops, reviews its trail, and pivots.

deadend-cli had a clean approach to detecting stuck-in-a-loop behavior: track repeated actions and force a strategy change after three consecutive similar attempts.

Shannon showed that turn budget matters more than people typically allow for. Generous budgets let the agent explore. Bumping the per-challenge limit from 20 to 40 turns moved several previously-timing-out challenges into successful flags.

Combined effect: planning + hints + reflection + larger budget + shell-first took pwnkit from 16 post-bug-fix flags to 23 across the broader category coverage. The model is the bottleneck, not the framework — the framework just needs to get out of the model’s way.

what did not move the score

Equally instructive: the changes that did not help.

A long vulnerability playbook with bypass techniques, encoding ladders, SQLi mutations, and SSTI escalation chains was A/B tested against a minimal prompt. The playbook found 1 more vulnerability but extracted 0 more flags. The model already knows these techniques from training. The playbook was stripped back to 25 lines.

A spawn_agent tool for deep exploitation in a fresh context never got used. The agent preferred to keep working in bash.

A tool router hook that catches unknown tool names and routes them to bash never triggered. The model does not hallucinate tool names when it only has three.

What moved the score: fixing serialization bugs in the agent loop, fixing infrastructure (port detection added 2 flags), the shell-first architecture itself (~+15 flags vs structured tools), passing challenge hints.

the AI/LLM security suite — where shell-first generalizes

A separate ten-challenge benchmark covers AI-specific attack surface: prompt injection, jailbreaks, system prompt extraction, encoding bypasses, SSRF via MCP tools, and multi-turn escalation. Each challenge hides a FLAG{...} behind a real AI-specific vulnerability. Binary pass/fail with blind verification.

pwnkit scored 10/10 here, with zero false positives. The deterministic baseline (no API key) catches 3/10 — CORS, exposed files, SSRF — through pattern-matching probes. The remaining 7 require agentic reasoning: jailbreaks cannot be matched against a regex, multi-turn privilege escalation cannot be templated.

The shell-first architecture transfers cleanly. For AI/LLM testing the agent uses the same loop with a sendPrompt primitive instead of curl, but the reasoning structure is identical: probe, observe, adapt, escalate. For network pentesting (AutoPenBench), nmap and metasploit are shell commands; the agent uses them directly. The lesson is that a minimal, shell-first tool set generalizes across domains better than building bespoke tool surfaces for each.

prior art

This architecture is not novel; it was rediscovered under empirical pressure.

pi-mono’s work on bash-as-universal-tool made the case explicitly: bash is the swiss-army knife every model already knows. Terminus took this further with a single-tmux-tool approach, giving the agent a persistent terminal session and letting it drive. Research on XBOW and KinoSec independently showed that the best-performing agents had the fewest, most general tools — the more specialized the toolkit, the more the agent struggled with selection and state threading.

the costs

Shell-first is not free. Two tradeoffs are visible:

More tokens per turn. Curl returns raw HTTP responses — headers, HTML, JSON — all landing in the context window. A structured tool could parse and summarize, returning only relevant fields. Output truncation and explicit instructions to pipe through head or jq partially mitigate this, but the per-turn token cost is higher than a structured toolkit on equivalent work.

Sandboxing is mandatory, not optional. Arbitrary shell access for an autonomous AI agent is exactly as dangerous as it sounds. pwnkit runs every shell session in an isolated container with no network access except to the target, no filesystem persistence between runs, and no access to the host. This was the first piece built — before the agent ran a single command. For any team building something similar: sandbox first, agent second. Always.

The flexibility gain pays for the cost. The agent can use any tool that exists on the system without the framework needing to anticipate and wrap it. When a challenge calls for sqlmap, ffuf, or a custom python script, the agent uses it. No new tool implementation, no SDK update. Just shell_exec.

what the architecture decision is, in one line

The agent is not a user clicking through a GUI of carefully designed buttons. It is a pentester sitting in front of a terminal. It thinks in commands, not in tool calls. That maps more naturally to how pentesting actually works — and the benchmark data confirmed it across web, AI/LLM, and network domains before the architecture was made canonical.