ai-ml

战术 RAG:从 Writeup 到行动

去年,在针对一个内部基础设施的项目中,我花了四十分钟寻找一个非常具体的细节:有人是如何针对一个配置错误并暴露 JMX 的特定 Java 服务版本进行权限提升的。我知道我读过。在某篇 writeup 里,在 Twitter 的某个帖子里,在某个 Discord 里。但我想不起来在哪。Google 只返回一堆 SEO 垃圾,浏览器收藏夹是一座坟场,而我的 ~/notes 文件夹里堆着 1,300 个未索引的 markdown。

四十分钟。在限时渗透测试中,这就是一个你拿不到的 Domain Admin。

Beorn 的想法就是从那里来的:一个不是搜索文档,而是向正在作业的智能体提供可操作上下文的 RAG。如今它索引了 9115 个向量、28 个主题集合,响应时间不到 50 毫秒。但到达这里的过程比听起来要混乱得多。

通用 RAG 在攻击场景下行不通

第一个原型完全照本宣科:langchain、OpenAI 的嵌入、本地 ChromaDB、每 512 token 进行一次递归分块。对于"什么是 Kerberoasting?"这样的问题,它能用。对于"我在一台运行 IIS 10 应用程序池的 Windows Server 2019 上拿到了 SeImpersonatePrivilege,给我接下来的路径"这种问题,它败得很惨。

问题不在模型。问题在于大部分 RAG 文献假定你的语料库是企业 FAQ、产品文档或 PDF 政策。攻击型 writeup 是另一回事:它们混合了叙述("我注意到 banner 返回了 Apache 2.4.49")、字面 payload、工具输出,以及只有你已经知道该 TTP 才能理解的隐含结论。如果你按固定长度切分,就会切断 payload、把 CVE 和它的解释分开,排序就成了掷骰子。

PentestAgent 团队在他们 NeurIPS 2024 的论文中记录了一种模式(Shen et al., 2024)[1]:攻击知识是过程性的,而非陈述性的。你寻找的不是一个事实。你寻找的是一份以你的上下文为条件的配方。而这彻底改变了检索器的设计。

索引 9115 篇 writeup 后我们学到的东西

按语义单元分块,而不是按 token

我们从递归分块转向了结构感知分块。每篇 HTB writeup 都有相对可识别的阶段:侦察、初始立足点、横向移动、权限提升、持久化。我们用一个预处理器解析它们,它会查看 markdown 标题、代码块和显式分隔符。每个分块都带有元数据:

{
  "box": "Sauna",
  "os": "Windows",
  "phase": "privesc",
  "ttp": "asreproast",
  "tools": ["impacket", "hashcat"],
  "cve": [],
  "difficulty": "easy",
  "lang": "en"
}

平均分块大小落在 380 个 token,但方差很大:有些只有 90(一条命令及其输出),有些则有 700(关于 Kerberos delegation 的完整解释)。NVIDIA 团队今年发表了一项实证分析(Wang et al., 2024)[2],表明最优分块大小取决于领域,强制统一会让精度下降 8% 到 15%。这与我们的观察一致。

多语言嵌入,因为 writeup 不只有英文

我们语料库的近三分之一是西班牙语、法语或俄语。我们试过 OpenAI 的 text-embedding-3-smalle5-multilingual-large,最后选定了 bge-m3(Chen et al., 2024)[3],它在 100 多种语言上同时支持稠密、稀疏和多向量检索,上下文长度达 8192 token。在同一集合中混合多种语言而不损失召回率方面,它对我们效果最好。

一个不容忽视的细节:智能体的查询通常以技术性英语形式出现("CVE-2021-26855 SSRF chain"),但指向的是用西班牙语写的解释。如果没有高质量的多语言嵌入,跨语种召回率就会塌陷。使用 bge-m3,我们在一个 200 条查询的手工评估集上测得 nDCG@10 为 0.81;而使用 OpenAI 的模型,我们卡在 0.69。

Reranking,因为稠密 top-k 会说谎

按余弦相似度返回前 10 个向量是教程教你的做法。也是在生产环境中会出问题的做法。第一遍返回的是看似合理的候选,但顺序是嘈杂的。我们在前 50 个候选上加了一个 bge-reranker-v2-m3 重排器,保留前 5 个。代价:+18 毫秒。Precision@5 提升:从 0.64 到 0.87。

# Simplified pipeline
candidates = vector_store.search(query_emb, k=50)
reranked = reranker.score(query, [c.text for c in candidates])
top_k = sorted(zip(candidates, reranked), key=lambda x: -x[1])[:5]

为正在进行渗透的智能体交付上下文

Beorn 不是聊天机器人。它作为一项服务运行在 Gandalf Gateway 之后,查询由智能体在无人干预的情况下发起。这改变了契约:响应必须能在毫秒级被另一个 LLM 摄取,而不是由人去阅读。

来自最近一次项目的真实查询:

beorn query \
  --filter "os=linux,phase=privesc" \
  --context "kernel 5.4.0, sudo 1.8.31, capabilities cap_dac_read_search+ep" \
  "binary with capabilities exploitation"

响应:

{
  "latency_ms": 41,
  "results": [
    {
      "chunk_id": "htb-academy-linux-privesc-cap-dac",
      "score": 0.94,
      "ttp": "T1548.001",
      "summary": "cap_dac_read_search permite leer cualquier fichero...",
      "command": "getcap -r / 2>/dev/null | grep dac_read",
      "next_steps": ["read /etc/shadow", "extract hashes",
                     "john --wordlist"]
    }
  ]
}

智能体收到的是这个,而不是一段散文。给 Gandalf "这有一段文字,你自己揣摩该做什么" 和 "这是 MITRE TTP、初始命令、接下来三步" 的区别,就是一个临场发挥的智能体与一个执行型智能体的区别。

真实指标

Beorn 已经在我们的内部生产环境运行了四个月。我们监控的数字:

  • 延迟 p50:41 毫秒。P99:78 毫秒。无缓存。
  • 在 312 条真实查询的标注集上,Precision@5:0.87。
  • Recall@20:0.93。
  • 语料覆盖:9115 个分块,28 个集合。
  • 语言:EN 64%、ES 22%、FR 8%、RU 4%、其他 2%。

一个比表格更有说服力的具体案例:在最近一次 Active Directory 评估中,智能体从初始立足点开始,6 分钟内就识别出了 ESC8 攻击路径(ADCS HTTP enrollment)。在没有 Beorn 的、类似的过往演练中,等价的阶段需要我们花费 40 到 90 分钟手动检索利用细节。这不是魔法。这只是不再把时间浪费在回忆细节藏在哪里上。

我们不会再做的事

第一:我们一开始把所有东西塞进一个巨大的集合里,靠元数据作为过滤。糟糕的主意。当你达到几千个向量时,后置过滤变得昂贵,召回率也会下降,因为来自不同领域的嵌入在同一个空间里相互竞争。当前的 28 个集合是按来源类型和操作系统划分的,由网关来编排它们。

第二:我们低估了去重。三篇不同的 writeup 用几乎相同的措辞解释同一种技术。如果没有语义去重,top-5 给你的就是同一段话的五个版本。我们加了一步 MMR(Maximal Marginal Relevance),top-k 的主观可用性显著上升,尽管用传统指标很难衡量。

第三,这一点让我们很痛:第一个版本没有持续评估。我们上传新的嵌入、更换 reranker,全凭感觉。直到一次模型更换让我们的 precision@5 跌了八个点,整整一周没人察觉。现在我们有一个 300 多条查询、带验证答案的 golden set,任何更改在上线前都要评估。Gao et al.(2024)[4] 的综述也记录了这一教训:没有评估循环,RAG 系统会老得很难看。

RAG 不是魔法,也不是搜索引擎。它是你的智能体本应一直拥有的操作记忆。这样看待它,会从分块一直改变到输出契约的设计。

如果你的团队正在构建类似的系统,我能给的最有用的建议是:从契约开始。不是从模型开始。消费者——无论是人还是智能体——需要接收什么才能行动?这决定了分块、元数据和响应格式。其余一切都是工程。

参考文献

  1. Shen, X., et al. (2024). PentestAgent: Incorporating LLM Agents to Automated Penetration Testing. arXiv:2411.05185.
  2. Wang, Y., et al. (2024). Finding the Best Chunking Strategy for Accurate AI Responses. NVIDIA Technical Blog.
  3. Chen, J., et al. (2024). BGE M3-Embedding: Multi-Lingual, Multi-Functionality, Multi-Granularity Text Embeddings. BAAI / Hugging Face.
  4. Gao, A., et al. (2024). Retrieval Augmented Generation for Robust Cyber Defense. PNNL-36792.