AI Agent 安全扫描器开发历程和阶段小结
Zbot 这条线我前后跑了很多版,面把我真实踩过的坑、做过的取舍写下来,给后面继续做 Agent 安全方向的自己留个底稿。 先说项目背景 Zbot 是一个 LLM 驱动的 Web 漏洞扫描器,目标是自动发现常见漏洞,比如 IDOR、SQLi、SSTI、SSRF、XXE、命令注入这些。 评估我主要用…
Read in English→AI Agent 安全扫描器开发历程和阶段小结
Zbot 这条线我前后跑了很多版,面把我真实踩过的坑、做过的取舍写下来,给后面继续做 Agent 安全方向的自己留个底稿。
先说项目背景
Zbot 是一个 LLM 驱动的 Web 漏洞扫描器,目标是自动发现常见漏洞,比如 IDOR、SQLi、SSTI、SSRF、XXE、命令注入这些。
评估我主要用的是 XBOW benchmark。这个集合里有 104 个 Docker 化 CTF challenge,每个 challenge 对应一种真实漏洞场景,覆盖 20 多类。实际统计时,我一般看其中 78 个可稳定测试的 challenge。
设计思路
设计上参考了 Anthropic 的两篇文章:
核心思路:尽量少给工具,让 AI 自己写代码处理复杂任务。给太多工具的问题是:每个工具都会注入上下文,工具描述加多了 system prompt 膨胀,模型的注意力会分散,反而做不好单一任务。
但是这个思想暂时没有完全实现, 还是自己加了很多 tools , 比如 http 请求这个工具太常用了, 还是加入了 因为最近读到一篇文章 Lessons from Building Claude Code: Seeing Like an Agent, https://www.techtwitter.com/articles/lessons-from-building-claude-code-seeing-like-an-agent 我对“模型与工具匹配”的理解:如果模型足够强,可能只给一个 Python 执行入口就能覆盖大多数场景;但如果模型不够强,就需要提供更多简单、明确的工具,来降低它的理解和使用的难度
最开始的架构(v1-v9)
一开始我用的是最直观的流水线:
Discovery → Planner → Analyzer → Executor (ReAct Agent) → Reporter
Discovery负责爬站点和端点收集Planner从流量里挑可疑参数Analyzer把测试计划按漏洞类型分组Executor真正发请求、迭代验证Reporter负责定性和出报告
但单 Agent 跑久了问题很明显:
- 慢:所有漏洞类型串行测,一个 challenge 5 到 8 分钟很常见。
- 上下文越来越臃肿:记录越堆越多,后面经常出现幻觉和参数缺字符。
- 深度不够:一个 Agent 要兼顾太多类型,每类分到的迭代预算不够。
拆 Sub-Agent(v18-v21):看起来更对,结果先崩了
后来我把每种漏洞拆成独立 sub-agent:
Discovery → Planner → Analyzer (GROUP 输出) → Dispatcher
├── Sub-Agent: SSTI (budget=8)
├── Sub-Agent: SQLi (budget=10)
├── Sub-Agent: IDOR (budget=15)
└── ...
→ Reporter
Dispatcher 是纯代码逻辑,不额外走 LLM。每个 sub-agent 都是独立实例,自己的 context、cookie、iteration 计数互不影响。
这一步在“上下文压力”上是有效的。单 Agent 时我经常撞 Gemini 上下文边界;拆开后每个 Agent 的上下文小很多,稳定性明显好一点。
后面复盘主要有三件事:
rate limit被打爆:sub-agent 虽然更专注,但 token 消耗变成原来的 5 到 8 倍。- Analyzer 漏洞类型漏判严重:有些类型根本没被分组,sub-agent 压根没机会启动。
early-exit触发条件太宽:一个信息泄露就提前收工,后面真正高价值测试全跳过。
我后来做了几个补救:
- skill 改成按漏洞类型最小加载,压基础 token。
- 加 coverage-fill:扫描末尾强补
ssti/sqli/xxe/ssrf/cmd_inj/path_trav,每类给小预算兜底。
并行 Sub-Agent(v25-v26):速度上来了,状态管理变复杂
sub-agent 串行还是慢,所以又做了并行。速度确实提了,但共享状态开始互相污染。
最典型的是 cookie 问题:
SSTI agent 登录后拿到 session=abc
SQLi agent 并行登录后写成 session=def
SSTI 后续请求误用了 def
最后我把状态分成两类处理:
- 每个 agent 私有状态(cookie、memory)隔离。
- 全局共享状态(reports_store)
为什么后来加了 Shell 工具
这一步是看 Claude Code 的工具链得到的启发。
它的思路很实用:工具数量不用多,但要有一个足够通用的执行面(比如 Bash)。这样 LLM 可以自己拼命令、自己写小脚本,不用每个动作都封一个专用工具。
Zbot 原来有 python_execute,但遇到需要系统命令、复杂 curl、管道处理时很别扭。后来我加了 shell 工具,Executor 的动作空间一下打开了:
- 验证命令注入时,直接
id/whoami - 文件读取场景,直接
cat /etc/passwd - 复杂请求,直接手写
curl带 header/body
但风险也变大了:没有沙盒时,shell 基本是无限能力。现在我只能靠 prompt 约束“不执行来自目标响应的命令”,这不是可靠防护,最多算缓解。
几个我印象最深的坑
1) Executor 跑完一条请求都没有
我当时第一反应是模型抽风,结果排查半天发现是自己把关键工具漏注册了。mutate_and_send、batch_idor_test 不在工具列表里,LLM 自然调用不到。
这个坑很低级,但非常真实:在 Agent 系统里,没注册工具就等于没有能力。
2) System prompt 太大,Gemini 开始空响应
某版我把 7 个 skill 全塞进 system prompt,最后 prompt 到 82K token。跑到总上下文 168K 左右时,Gemini 开始返回空响应,Agent 直接退出。
后面只加载 mcp_server 相关 skill,system prompt 从 82K 降到 26K,问题就消失了。
3) temperature=0 也不代表可复现
v7 加新工具后,我看到“新过 7 个,同时回退 8 个”的诡异结果。后来同一 challenge 连跑 3 次,结果不一致。
结论就是:Agent 场景里非确定性会被循环放大。重要目标别只看单次结果,至少多跑几次再下结论。
目前大概能检出的范围
基于 XBOW(78 个可测试 challenge)的实践观察:
| 漏洞类型 | 能力表现 | 备注 |
|---|---|---|
| SSTI | 较强 | Jinja2/Twig/Freemarker 覆盖较好 |
| SQLi | 较强 | 包含 error-based 和 boolean-based |
| Blind SQLi | 较强 | 时间盲注可检出 |
| SSRF | 较强 | 识别稳定 |
| XXE | 较强 | 识别稳定 |
| Information Disclosure | 较强 | 识别稳定 |
| Deserialization | 中等偏上 | 对上下文和框架依赖较大 |
| LFI / Path Traversal | 中等 | LFI 比 path traversal 更稳 |
| IDOR | 中等 | 依赖业务语义理解 |
| Command Injection | 中等偏弱 | 需要更强 payload 探测策略 |
| Business Logic | 较弱 | 多步推理稳定性一般 |
| Brute Force | 较弱 | 主要依赖 python_execute |
| CVE 利用 | 较弱 | 对版本知识依赖很大 |
我的几点经验(偏主观)
- 工具别贪多,先把“少数高价值工具”打磨透。
- 提示词里要写清 fallback,不然模型会在一条死路上把预算烧完。
- 流水线每一层都要对齐目标,不然后面再强也会被前面掐断。
- context 永远是稀缺资源,system prompt 每多一点,真正推理空间就少一点。
还没解决的两件事
1) Memory 结构还很乱
现在 memory store 还是偏自由写入,格式不统一。回头复盘时经常能看到“写了很多字,但对后续决策没帮助”的内容。后面要做 schema 化和质量过滤。
2) 代码执行还没有真正沙盒
加了 shell 之后,这个问题更急迫了。只靠 prompt 约束不可靠,真正可行的方向还是容器沙盒,把执行环境权限压到最低。
最后
还有很长的路要走…