Development Journey and Interim Review of an AI Agent Security Scanner
A retrospective on the development of Zbot, an LLM-driven web vulnerability scanner, covering its architectural evolution, sub-agent design, tooling decisions, benchmark results, lessons learned, and unresolved challenges.
Read in Chinese→Development Journey and Interim Review of an AI Agent Security Scanner
I’ve gone through many versions of Zbot. Below, I’ve documented the pitfalls I actually encountered and the trade-offs I made, leaving a record for myself as I continue working on Agent security.
First, Some Project Background
Zbot is an LLM-driven web vulnerability scanner designed to automatically discover common vulnerabilities such as IDOR, SQLi, SSTI, SSRF, XXE, and command injection.
For evaluation, I primarily use the XBOW benchmark. It contains 104 Dockerized CTF challenges, each corresponding to a real-world vulnerability scenario and covering more than 20 categories. In practice, I generally evaluate against the 78 challenges that can be tested reliably.
Design Approach
The design was inspired by two Anthropic articles:
The core idea is: provide as few tools as possible and let the AI write code to handle complex tasks on its own. The problem with providing too many tools is that every tool adds context. As tool descriptions accumulate, the system prompt becomes bloated, the model’s attention gets diluted, and it actually becomes worse at performing individual tasks.
However, I have not yet fully implemented this idea. I still added many tools of my own. For example, HTTP requests are used so frequently that I ultimately kept an HTTP request tool.
I also recently read an article titled “Lessons from Building Claude Code: Seeing Like an Agent,” https://www.techtwitter.com/articles/lessons-from-building-claude-code-seeing-like-an-agent
My understanding of “matching models with tools” is this: if the model is powerful enough, providing only a Python execution entry point may cover most scenarios. If the model is not powerful enough, however, it needs more simple and explicit tools to reduce the difficulty of understanding and using them.
The Initial Architecture (v1–v9)
At first, I used the most straightforward pipeline:
Discovery → Planner → Analyzer → Executor (ReAct Agent) → Reporter
Discoverywas responsible for crawling sites and collecting endpointsPlannerselected suspicious parameters from trafficAnalyzergrouped test plans by vulnerability typeExecutoractually sent requests and iteratively verified findingsReporterhandled classification and report generation
But after running a single Agent for a while, the problems became obvious:
- Slow: All vulnerability types were tested sequentially, and taking five to eight minutes per challenge was common.
- Increasingly bloated context: As records accumulated, hallucinations and missing characters in parameters frequently appeared later in the run.
- Insufficient depth: One Agent had to handle too many vulnerability types, leaving too little iteration budget for each category.
Splitting Out Sub-Agents (v18–v21): It Looked More Correct, but the Results Initially Collapsed
Later, I split each vulnerability type into an independent sub-agent:
Discovery → Planner → Analyzer (GROUP 输出) → Dispatcher
├── Sub-Agent: SSTI (budget=8)
├── Sub-Agent: SQLi (budget=10)
├── Sub-Agent: IDOR (budget=15)
└── ...
→ Reporter
Dispatcher used pure code logic and did not make additional LLM calls. Each sub-agent was an independent instance with its own context, cookies, and iteration counter, none of which affected the others.
This change was effective in reducing context pressure. With a single Agent, I frequently hit Gemini’s context boundary. After splitting it up, each Agent had a much smaller context, and stability improved noticeably.
In retrospect, there were three main problems:
rate limitwas overwhelmed: although the sub-agents were more focused, token consumption increased to five to eight times its original level.- The Analyzer frequently failed to identify vulnerability types: some types were never grouped at all, so their sub-agents never had a chance to start.
- The trigger conditions for
early-exitwere too broad: a single information disclosure finding would end the scan early, causing all subsequent high-value tests to be skipped.
I later made several compensating changes:
- Skills were changed to load minimally by vulnerability type, reducing baseline token usage.
- I added coverage-fill: at the end of a scan,
ssti/sqli/xxe/ssrf/cmd_inj/path_travwas forcibly added, with a small fallback budget allocated to each category.
Parallel Sub-Agents (v25–v26): Faster, but State Management Became More Complex
Sequential sub-agents were still slow, so I made them run in parallel. Performance did improve, but shared state began contaminating other agents.
The most typical example was the cookie issue:
SSTI agent 登录后拿到 session=abc
SQLi agent 并行登录后写成 session=def
SSTI 后续请求误用了 def
In the end, I divided state into two categories:
- Per-agent private state, such as cookies and memory, was isolated.
- Global shared state, such as reports_store, remained shared.
Why I Later Added a Shell Tool
This step was inspired by the Claude Code toolchain.
Its approach is highly practical: you do not need many tools, but you do need a sufficiently general execution interface, such as Bash. This allows the LLM to compose commands and write small scripts on its own, rather than requiring a dedicated tool for every action.
Zbot originally had python_execute, but it was awkward when system commands, complex curl, or pipeline processing were required. After I added a shell tool, the Executor’s action space expanded dramatically:
- When verifying command injection, directly use
id/whoami - For file-reading scenarios, directly use
cat /etc/passwd - For complex requests, manually write
curlwith headers and a body
But the risk also increased: without a sandbox, shell access effectively provides unlimited capabilities. At present, I can only rely on a prompt constraint saying “do not execute commands from target responses.” This is not reliable protection and can, at most, be considered a mitigation.
Some of the Most Memorable Pitfalls
1) The Executor Completed Without Sending a Single Request
My first reaction was that the model had malfunctioned. After investigating for quite a while, however, I discovered that I had forgotten to register the critical tools. mutate_and_send and batch_idor_test were not in the tool list, so naturally the LLM could not invoke them.
This was a very basic mistake, but also a very real one: in an Agent system, an unregistered tool is equivalent to a nonexistent capability.
2) The System Prompt Became Too Large, and Gemini Started Returning Empty Responses
In one version, I packed all seven skills into the system prompt, which eventually reached 82K tokens. When the total context grew to around 168K, Gemini began returning empty responses and the Agent exited immediately.
Later, I loaded only the skills related to mcp_server, reducing the system prompt from 82K to 26K, and the problem disappeared.
3) temperature=0 Does Not Guarantee Reproducibility
After adding new tools in v7, I saw a strange result: “seven newly passed challenges, while eight previously passed challenges regressed.” I then ran the same challenge three times in succession and got inconsistent results.
The conclusion is that nondeterminism gets amplified by loops in Agent scenarios. For important targets, do not draw conclusions from a single result. Run them at least several times first.
Approximate Current Detection Coverage
Based on practical observations from XBOW’s 78 testable challenges:
| Vulnerability Type | Capability | Notes |
|---|---|---|
| SSTI | Strong | Good coverage of Jinja2/Twig/Freemarker |
| SQLi | Strong | Includes error-based and boolean-based techniques |
| Blind SQLi | Strong | Time-based blind injection can be detected |
| SSRF | Strong | Consistent detection |
| XXE | Strong | Consistent detection |
| Information Disclosure | Strong | Consistent detection |
| Deserialization | Moderately strong | Highly dependent on context and framework |
| LFI / Path Traversal | Moderate | LFI is more reliable than path traversal |
| IDOR | Moderate | Depends on understanding business semantics |
| Command Injection | Moderately weak | Requires a stronger payload probing strategy |
| Business Logic | Weak | Multistep reasoning has only average stability |
| Brute Force | Weak | Primarily depends on python_execute |
| CVE Exploitation | Weak | Highly dependent on version-specific knowledge |
Some of My Takeaways (Admittedly Subjective)
- Do not be greedy about adding tools. First, thoroughly refine a small number of high-value tools.
- Clearly define fallbacks in prompts; otherwise, the model will exhaust its entire budget on a dead end.
- Every layer of the pipeline must align with the objective. Otherwise, no matter how powerful the later stages are, they will be cut off by earlier ones.
- Context is always a scarce resource. Every addition to the system prompt leaves less room for actual reasoning.
Two Issues That Remain Unresolved
1) The Memory Structure Is Still Messy
The current memory store still allows relatively free-form writes, and its format is inconsistent. During later reviews, I often find content that “contains a lot of words but provides no help for subsequent decisions.” The next step is to introduce schemas and quality filtering.
2) Code Execution Is Still Not Truly Sandboxed
After adding shell access, this issue became even more urgent. Prompt constraints alone are unreliable. The genuinely viable direction is still container-based sandboxing, with execution-environment privileges reduced to the absolute minimum.
Finally
There is still a long way to go…