【AI 辅助声明】 本文由作者与 AI 协作者共同完成:实验、复核与所有数字来自真实仓库记录(
github.com/a9320/arcanum-spark),AI 参与整理成文。文中每个数字都标注了出处,查不到的我们写"不知道"。
一、先看一个很难看的数字
我们的 AI 代码审计管线(CodeRisk Arcanum)一直拿一个叫 FN(漏报)的指标折磨自己------团队内部对它的纪律近乎偏执:微调考卷上七轮实验,FN 恒为 0。
然后在十月初,我们做了第一次外部真值对照:拿 NYU CTF Bench 的 20 道 web 类 CTF 赛题当考卷,让管线盲跑全卷,再人工把每道题的预期答案对到管线判定上,算出第一版总账:
- 机制口径(字面匹配):真阳性 5 / 漏报 12 / 无法归类 3
- 语义口径(等价类映射后):7 / 8 / 5 / 1
对一个拿 FN=0 当招牌的团队,漏报 12/20 这个数字相当刺眼。
按正常剧本,下一步应该是:调模型、加召回、扩数据、烧 token。我们确实准备这么干------混合臂实验(换更强的云端 Scout)就是为此设计的,而且第一轮数据看起来也在支持这个方向:召回确实涨了。
但烧 token 之前,我们停了一下,问了一个更便宜的问题:
漏报的这 12 题里,有多少是"模型没找到",有多少是"我们的标准答案本身就写错了"?
二、考卷的答案是怎么来的,以及为什么可疑
先交代背景。NYU CTF Bench 是学界维护的 CTF 基准数据集(NYU OSDI 出品,license 我们选型时实查过, 选它做外部真值正是为了回答第 4 笔债------"自己出题自己判")。
我们的人工流程是:逐题读官方 writeup,把预期攻击类型登记成一份 manifest(比如"这题考 SQL 注入 + XSS"),然后让管线盲跑,判分规则只有一行:
TP = CONFIRMED 且 假设类型 ∈ intended 词表。
注意这套流程里的一个结构性弱点:intended(预期答案)是"人读 writeup 记下来的" ,不是从源码验证的。writeup 是别人的笔记------笔记会串、会记错、会张冠李戴。我们自己项目里还有一条现成的教训恰好能佐证这种风险:r8 微调实验里,训练数据同源的错误曾直接把排序质量从 1.0 拖到 0.9484。
基准的答案错了,判出来的"漏报"就可能是冤案:模型找对了东西,但找到的东西不在(错误的)词表里,被判 FN。
所以我们决定:烧 token 之前,先给自己的考卷答案做一次源码级审计。
三、复核方法:把"读笔记"升级成"读源码"
方法不复杂,但每一步都要求实锤:
- 拿到一手物料:对 NYU_CTF_Bench 仓做 partial clone,逐 blob 提取目标题的完整源码(第一批 51/51 全成功,含 challenge.json、官方 README、solve 脚本、服务端代码);
- 三重实锤:每道题的"官方解"必须同时有 challenge.json 描述、官方 solve 脚本、服务端源码三处对应,缺一处就降级为"存疑";
- 逐条对账:把 manifest 草案的 intended 词表逐类型对照源码------有源码对应的保留,没有的删,官方链直接对应的补进。
复核是纯本地操作,零 GPU、零 token。做完第一批 3 题,我们就知道自己抓到大鱼了。
四、第一批三题:三种不同姿势的"答案错误"
#9 whistleblow:整道题串了门
草案给的 intended 是 XSS、SQL 注入、命令注入------一听就是"标准 Web 三件套"。
源码实锤:这道题(CSAW 2020 Quals)目录里连 Web 应用代码都没有 。它是一道纯 AWS 资源攻略题:官方 README 原话 "Various flaws in AWS S3 bucket configurations",解法是在第一个 S3 bucket 的 200 个伪随机对象里翻出 4 条藏匿的元数据(TARGET_BUCKET / TARGET_PATH / 一个真实 AKIA 格式的 ACCESS_KEY / 签名),再用它们进第二个 bucket 拿 flag。flag 内容 flag{pwn3d_th3_buck3ts} 也在自证:是 bucket,不是注入。
判定:writeup 串题实锤。草案的"Web 三件套"大概率是从别的题的 writeup 里串过来的。
更有意思的是管线对照:我们 K1 版的 Scout 在这道题上输出了"AWS 长期凭证+预签名 URL 硬编码"(CONFIRMED 0.88),混合臂版更是字面命中了定稿词表里的 HARDCODED_SECRET 和 EXPOSED_CREDENTIAL。两代 Scout 都找对了核心发现------此前的"召回缺失"是冤案,真凶是词表错位。
#10 United:writeup 的"记忆偏差"
草案 intended:SSTI(服务端模板注入)+ RCE。
源码实锤:官方 README 的 Overview 写得明明白白,三步解法------"1) 用 SQLi Union attack 泄露两个 admin 的信息;2) 破解泄露的 SHA-1 密码;3) 找到未链接的 admin 登录页"。注入点就在 routes/players.js:12,模板字符串拼接,'2-P' UNION SELECT NULL, username, password, name from admins-- 这条官方样例 payload 和代码逐字吻合。flag 也自嘲:flag{United_w3_leek_the_d4atabas3}(leak the database)。
反向排除 SSTI 也干净:EJS 在这道题里只做视图引擎渲染 locals,模板源码没有用户输入路径------SSTI 面不存在。
判定:writeup 记忆偏差------记串了漏洞类型。
这道题最戏剧的点在管线对照:两代 Scout 都独立找到了 SQL_INJECTION@players.js(K1 版的记录里甚至留着 Muse 思考轨迹,明确指到那行代码)。管线没错,错的是考卷答案。
#18 gatekeeping:答案里塞了一个不存在的漏洞
草案 intended:AUTH_BYPASS、JWT_VULNERABILITY、ACCESS_CONTROL、LOGIC_ERROR。
源码实锤:官方链是利用 gunicorn 会从请求头 加载 WSGI SCRIPT_NAME 的行为------发 /asdf/admin/key 加 SCRIPT_NAME: asdf/,应用实际处理的是 /admin/key,而 nginx 只检查 /admin 前缀,密钥就这样泄露。官方 solve.py 是完整的 PoC,flag 直接吐槽:flag{gunicorn_probably_should_not_do_that}。
而全文件零 JWT / Token 逻辑 ------服务器对 /admin/key 的唯一"认证"是要求一个 hex 格式的 key_id 头,源码注释里两位角色的对白"we have deny all in nginx"就是全部防线。
判定:部分正确------AUTH_BYPASS(宽义)和 ACCESS_CONTROL 保留,JWT_VULNERABILITY 删除(零证据),同时把 INFORMATION_DISCLOSURE 收进词表(无认证密钥端点是官方链的交付物本身)。删泛词 LOGIC_ERROR 的理由也写了:太容易虚翻。
五、顺手又审了两题:射程外与弱密码学
第一批的结果让我们对"同批 draft"的其余题起了疑心(草案是同一批写的),于是同日又复核了两道:
#12 search_for_pi :草案给了 SQL 注入、LIKE 注入、XSS、SSTI 四件套。源码实锤:sol.py 官方解是从 /stuff 页面出发追踪 100 步随机词链 ,链尾读 flag------纯 logic-puzzle,无数据库、无 SQL 面、无模板注入面。判定:题目类型错配,这道题对漏洞检测管线而言在射程外,FN 应记"类型错配"而不是"检测失败"------它不值得烧任何 token。
这道题还附赠一个红利:复核前我们的语义口径总和是 21/20(有题同时计了 FN 和 REFUTED 双份)------删掉它的 SSTI 后双计怪账自动修复。
#13 nft-world :草案给了 JWT、SSRF、IDOR。源码实锤的真实链路更野:签名密钥是账户 CreatedAt 时间戳 (零盐 PBKDF2 跑 100 轮 + 全零 IV + 用 MD5 密文尾自造 MAC)→ 伪造 userId=0 的 admin 消息打 /cmd 的 DEBUG 命令 → exec.Command("cat", ...) 任意文件读 → 拖走 flag.txt.gpg,GPG 密码藏在一张 NFT 图片的描述里。flag:maYB3_W3_5H0ulD_HAV3_U53d_PA22W0rD2------出题人自己都在嘲弄这认证。
判定:部分正确 ------AUTH_BYPASS 保留,JWT/SSRF/IDOR 删除(自制 MAC 不是 JWT),补入 BROKEN_AUTHENTICATION、ARBITRARY_FILE_READ、WEAK_CRYPTOGRAPHY。后两者与管线已确认的发现字面双命中。
六、重判分:零 token 的账本变化
答案修正后重判分,全程没有重跑任何模型(判定件都是现成的,改的只是"标准答案"这一侧):
| 口径 | 草案版 | 复核后 v0.1 判分器 | 判分器 v0.2 终版 |
|---|---|---|---|
| 机制(TP/FN/无法归类) | 5 / 12 / 3 | 7 / 10 / 3 | 8 / 9 / 3 |
| 语义(TP/FN/无法归类/被推翻) | 7 / 8 / 5 / 1 | 9 / 6 / 5 / 1 | 10 / 5 / 5 / 0 |
| 总账 | 21(>20,双计) | 20 | 20(首次自洽) |
中间差的那 1 条值得一讲:whistleblow 题我们自己的发现类型标的是 SECRET_EXPOSURE,官方链对应 EXPOSED_CREDENTIAL------词面不同,字面匹配判不了 TP。人工对账明确这是同一发现,于是给判分器加了凭证泄露族等价类 (一行映射 + 一条双向锁定测试,当时全仓 289 个测试全绿)------同时做了副作用推演,确认不会让 United 等题的同类标签虚翻。这就是 v0.2 终版的来历:机制 8/9/3 + 语义 10/5/5/0,总账 20 首次自洽。
三笔 FN 翻成 TP(机制口径),三笔语义 TP 收回------零模型 token。而这件事的直接推论更值钱:这批 5 题失分里,约 2.5--3 题是 intended 错位造成的冤案;同批 draft 的剩余 11 道失分题,答案质量同样存疑。复核名单已经列好,全是本地零成本操作。
七、比数字更重要的三件事
1. Verify 的两次 REFUTED,审完都是对的。 search_for_pi 题里管线否掉了两条假设(伪造随机链 cookie 在信息论上等价于正常游玩;EJS 渲染无用户输入路径)------逐条对照源码,两次否决全部正确。这不直接证明"Verify 不过严",但至少在这批样本上,它没有冤枉过一个假设。
2. "模型能力问题"和"考卷问题"必须分账。 混合臂实验当时的结论是"召回涨了但语义判分没翻正",一度被解读为"召回修复没有兑现"。真相是:召回确实修好了(whistleblow 两代 Scout 都找对了),是词表把修复吞了。如果没有这次 intended 复核,我们会拿着错误的结论去规划下一步的算力。
3. 已发布数字不追溯改写。 第一版判分件(k1_score_v01_20261005.json)原样保留,manifest 的三题 notes 标注"K1 判分用草案版本",重判分全部另立文件名(k1_score_k2 / k1_score_v02)------双版本永远可对账。修正历史本身就是证据链的一部分,抹掉它等于抹掉这次审计的价值。
八、方法论收口:先修答案,再烧 token
这件事给我们立了一条新规矩,写进了 ROADMAP:
先把 intended 修对,再烧 token。修答案不要钱,重跑要钱。
它还有几个配套细则,都来自这次的实战:
- 词表只收官方解链直接对应的类型------United 刻意不收 INFORMATION_DISCLOSURE,避免范围外的两条 INFO 虚翻成 TP;词表的克制比词表的全面更重要;
- 等价类映射必须带副作用推演 + 测试锁定------一行映射救回一条 TP 很爽,但它同样能让别的题虚翻;
- "答案"和"能力"分账------FN 高企时,第一动作是审计考卷,第二动作才是调模型。顺序反了,你会为一张错误的考卷付出真实的算力。
基准是别人的笔记,笔记会错。对你手里每一个"官方数字",都值得问一句:它是从源码来的,还是从笔记来的?
附:本文数字的仓内出处(供核对)
| 数字 / 事实 | 出处 |
|---|---|
| 复核方法与三题源码证据 | docs/K2-INTENDED-REVIEW.md(含 flag、payload、源码行号逐条) |
| 第一版判分(5/12/3 与 7/8/5/1) | reports/m8-k1/k1_score_v01_20261005.json |
| 重判分过程与 9/6 中间态 | reports/m8-k1/k1_score_k2.json、hybrid_score_k2.json |
| 终版 8/9/3 与 10/5/5/0 | reports/m8-k1/k1_score_v02.json(20 题逐行可重算)+ docs/EVAL-LEDGER.md |
| 判分器 v0.2 等价类与测试 | eval/m8_k1_score.py + tests/test_m8_k1_score.py::test_semantic_credential_family_v02 |
| 真值协议与数据集选型 | docs/M8-CTF-TRUTH.md |
| "先修 intended 再烧 token" | docs/ROADMAP.md K1-min 条目 |
本文写于 2026 年 10 月。系列上一篇:《当第一个真实开源仓把我的 AI 审计管线撑爆》。仓库:github.com/a9320/arcanum-spark(MIT)。