用于杀伤链编排的 ReAct 提示
我们第一次尝试让单个模型编排完整的渗透测试活动时,它在第四十分钟就失败了。不是因为技术能力不足。该模型完全知道如何处理暴露的 Redis,如何处理未经身份验证的 Jenkins,以及如何将 SSRF 与云元数据串联起来。问题在别处,更乏味:在四十页的上下文中,代理已经忘记了我们一开始给它的范围。它开始扫描超出范围的网段,产生幻觉编造了两个不在我们工具箱中的工具(nmap-fast-aggressive 和 burp-pro-headless,都不存在),并提议利用客户甚至没有安装的 CVE。
那天晚上我们重写了编排器的很大一部分。从中诞生的是今天 Gandalf CLI 引擎的基础:对 ReAct 框架的一个相当固执的扩展,适用于多个模型在同一屋檐下协作的长时间、多阶段活动。本文试图讲述我们为什么走到了这个设计,我们借鉴了什么,我们必须自己发明什么,以及我们在哪里继续摔跟头。
ReAct,不带营销
ReAct 由 Yao 及其同事于 2022 年末发表(Yao et al., 2022)[1]。这个想法看似简单。与其只让模型给出一个答案或只让它执行一个动作,你要求它交错三件事:一个 Thought(关于需要做什么的明确自然语言推理),一个 Action(对外部工具的调用,通常具有良好的类型),和一个 Observation(工具返回的输出,它会回到上下文中)。循环。又一轮。如此往复直到模型决定它已经够了。
重要的是推理作为文本出现在 rollout 本身中。这同时解决了两件事。一方面,模型获得了 Chain-of-Thought 的能力(Wei et al., 2022)[2],也就是在行动之前分解问题。另一方面,它不会被困在自己的脑袋里:每隔几次迭代它就必须接触真实世界并带着真实的观察回来。这大大减少了与纯 CoT 相比的幻觉。
对于 HotpotQA 问题或 WebShop 配方来说很完美。对于运行渗透测试来说,不行。
当活动持续超过一小时时,ReAct 在哪里崩溃
三种失败模式,我们几乎在对真实目标进行的所有原版 ReAct 测试中都经历过。
第一个是压缩。一个体面的侦察会话中的 Thought/Action/Observation 跟踪占用的空间比看起来多得多。一次针对合理 /24 的 nmap -sV -p- 在 Observation 中就是数千个 token。乘以二十个主机,加上 HTTP 指纹识别,两小时后上下文就饱和了。当编排器开始修剪旧消息以适应新消息时,首先丢失的通常正是最重要的内容:交战规则、初始假设、客户的限制。
第二个是 drift(漂移)。如果没有在每一轮明确重新锚定目标,模型就会偏离方向。它从"找到一个初始向量"开始,最终列举不在范围内的提供商的子域。PentestGPT(Deng et al., 2023)[3] 很好地记录了这个问题,并提议将其分成模块。我们自己也得出了类似的结论。
第三个是工具幻觉。当模型经过多次迭代后,它开始发明工具箱中不存在的动作。masscan-with-banners、burp-active-scan、nuclei-cloud-edition。听起来合理但不存在的名字。如果你的 runner 严格,它会硬性失败。如果它宽松,更糟:它会做一些类似但不是你要求的事情。
我们添加了什么:按阶段交接和类型化制品
对我们来说关键的直觉是将杀伤链视为一个状态机,而不是一个单一的循环。每个阶段(Recon、Planning、Execution、Reporting)都有自己的代理、自己的提示和自己的模型。阶段之间不传递原始上下文:传递的是 artifacts。
一个 artifact 是一个类型化的结构,一个代理在关闭其阶段时产生,另一个代理在开启自己的阶段时消费。它不是完整的跟踪。它不是中间的想法。它是经过提炼的结论,在推进之前经过第二步审查验证。
今天我们在 Gandalf CLI 中默认使用的分布如下:Beorn 用于侦察,在这里我们想要一个便宜且对嘈杂观察具有抵抗力的模型。Gandalf 用于规划,在这里我们需要最精细的可用推理,并接受支付延迟。Gwaihir 用于执行,在这里我们优先考虑吞吐量和对工具反馈的快速响应。每一个都有其调整过的提示系统,最重要的是,它自己受限的工具箱。Beorn 不能调用 exploits。Gwaihir 不能修改范围。
后者是有意的。按阶段限制工具箱是我们找到的削减工具幻觉的最便宜方式。模型不会发明它看不到的东西。
一个追踪的示例
权作样本。一个 Beorn 代理用以下内容在一个主机上结束侦察阶段:
Thought: He completado el escaneo de servicios. El host expone
SSH (22), HTTP (80), HTTPS (443) y un Jenkins en 8080 con
header X-Jenkins: 2.346.1. Ese build tiene CVE-2024-23897
(arbitrary file read pre-auth). Esto es relevante.
Action: emit_artifact({
"phase": "recon",
"host": "10.42.0.17",
"services": [...],
"candidate_cves": ["CVE-2024-23897"],
"confidence": "high",
"evidence": "banner X-Jenkins: 2.346.1"
})
Observation: artifact_id=art_8c1a stored. handoff ready.
制品传递给 Gandalf,它进入规划阶段。Gandalf 看不到 Beorn 的完整跟踪。它只接收制品、交战规则和当前清单。它的工作是决定向量。
Thought: CVE-2024-23897 permite leer ficheros arbitrarios
sin autenticación vía el CLI de Jenkins. El objetivo del
cliente es validar acceso a credenciales. La ruta natural
es leer /var/jenkins_home/secrets/master.key y luego
credentials.xml. Confianza alta, ruido bajo. Acepto.
Action: emit_plan({
"vector": "cve-2024-23897-file-read",
"steps": [
{"id": 1, "tool": "jenkins-cli-cve", "target": "10.42.0.17:8080",
"args": {"file": "/var/jenkins_home/secrets/master.key"}},
{"id": 2, "tool": "jenkins-cli-cve", "target": "10.42.0.17:8080",
"args": {"file": "/var/jenkins_home/credentials.xml"}}
],
"abort_if": ["http_status != 200", "response_size < 16"]
})
Observation: plan_id=pln_2f9b validated. handoff to execution.
Gwaihir 拿起计划,逐步执行,返回原始和规范化的证据。如果一个步骤失败,它不会即兴发挥:它将控制权交还给 Gandalf 进行重新规划。这个决定,不让执行代理即兴发挥,是为我们大幅减少误报的决定之一。
阶段之间的交叉验证
在一个代理的输出和下一个代理的输入之间,我们插入了一个验证步骤,该步骤不来自原始代理。对于关键制品(候选 CVE、发现的凭据、利用决策),第二个模型,通常是处于关键模式的同一个 Gandalf,会审查制品是否格式正确,证据是否证明结论合理,以及下一步是否不会超出范围。
这在 token 上很昂贵。我们接受。我们最初尝试的替代方案,信任代理本身的自洽性,没有效果。模型倾向于同意自己。
与已有方案的比较
我们没有发明任何根本上新的东西。我们所做的是混合来自各个地方的部分。
与纯 Chain-of-Thought(Wei et al., 2022)相比,我们获得了对世界采取行动并验证的能力。CoT 本身对于封闭问题是一个好的提示,而不是对于有外部反馈的活动。
与 Tree of Thoughts(Yao et al., 2023)[4] 相比,我们简化了。ToT 提议在并行中探索几个推理分支并进行回溯。它很优雅,但当一个分支涉及对基础设施启动一个真正的扫描器时,token 和延迟的成本是望而却步的。我们确实采用的是在做出承诺之前评估候选项的想法。
与追求单个代理完全自主的 AutoGPT 及其后代相比,我们走的是相反的方向。该领域的综述(Wang et al., 2023)[5]很清楚:单体自主代理陷入循环,烧掉预算并失去线索。具有规范交接的多代理给了我们更多的可预测性。
与 PentestGPT(Deng et al., 2023)相比,我们共享按子任务模块化和隔离上下文的想法。主要区别是我们对制品进行类型化并强制交叉验证,我们不让模块之间的交接是自由文本。
诚实的权衡
有三个永久性的紧张关系,我们不解决,只管理。
第一个是 延迟与精度。每次交接都增加秒数。每次交叉验证更多。一个单体代理会在十五分钟内处理掉的活动,对我们来说需要四十分钟。我们已经决定这没关系。客户宁愿等待并收到没有幻觉的报告。
第二个是 token 预算。交叉验证意味着为关键决策支付两次。我们通过对低风险阶段使用便宜的模型并为规划保留昂贵的模型来弥补。没有这种分段,每次交战的成本将是不可承受的。
第三个是 debugging。当一个活动失败时,你必须重建哪个代理在什么上下文中决定了什么。记录完整的 Thought/Action/Observation 跟踪是不可协商的。没有那个你就不能做事后分析。但它占用的磁盘空间令人发指。
上个月的失败
我们以一个最近的例子结束,还很新鲜。一个针对 web 周边的活动。Beorn 检测到一个端点,它的响应带有 Server: Werkzeug/3.0.1 Python/3.11.4 头。其制品标记为"可能的 Flask 调试模式",置信度中等。Gandalf 在规划时决定尝试 /console 路径,期望找到 PIN 保护的调试器。
到那时为止,完美。问题出在 Gwaihir。工具箱有一个 werkzeug-debugger-pin-bruteforce 模块,该模块在内部分支中被标记为已弃用,但仍然可选。Gwaihir 选择了它,该模块以 WAF 标记为滥用的速率发出请求,他们在交战中途关闭了我们的源 IP。
根本原因不在模型。在工具箱。工具注册表有一个 deprecated: true 标志,加载器没有遵守。在加载器中改了三行代码和一个回归测试。但这个失败教会了我们更深层次的东西:阶段之间的交叉验证假设工具箱是正确的。如果工具箱说谎,整个编排器就说谎。
从那以后,我们在启动每个代理时都有一个额外的检查:它列出它将能够调用的工具,在日志中打印它们,并且当它第一次针对一个新客户执行时,一个人类签署这个集合。这并不优雅。但是当生产受到威胁时,没有任何代理,无论编排得多好,都不能取代一双眼睛。
Thought/Action/Observation 循环仍然是我们所知道的让 LLM 与世界对话的最佳原语。在真实活动中改变的是循环之间发生的事情。那是你赢或输的地方。
参考文献
- Yao, S. et al. (2022). ReAct: Synergizing Reasoning and Acting in Language Models. arXiv:2210.03629.
- Wei, J. et al. (2022). Chain-of-Thought Prompting Elicits Reasoning in Large Language Models. arXiv:2201.11903.
- Deng, G. et al. (2023). PentestGPT: An LLM-empowered Automatic Penetration Testing Tool. arXiv:2308.06782.
- Yao, S. et al. (2023). Tree of Thoughts: Deliberate Problem Solving with Large Language Models. arXiv:2305.10601.
- Wang, L. et al. (2023). A Survey on Large Language Model based Autonomous Agents. arXiv:2308.11432.