《技能包是干净的,毒藏在公司资料库里:一次「运行时投毒」事件分析》
导读:本文复盘一次 Agent 技能平台的「运行时投毒」安全事件:两个已上架技能包静态扫描全部干净,恶意载荷却在运行时才从公司自己的资料库下载执行(伪装成「服务方当日口径包」的后门,可冒充身份调内部接口、执行任意命令并外传结果)。核心结论是静态扫描存在三个盲区(只扫文本不扫运行时载荷、认关键字不认别名、只认地址不认行为),自托管内容与内网域名同样不可信,防御应「先收能力、再补检查」(沙箱断内网、技能包禁止运行时下载代码、红线与回归测试)。适合正在建设或审核 Agent 技能平台、关注 AI 供应链安全与提示词注入的资深工程师阅读。 分类:AI安全 | 标签:AI安全、提示词注入、供应链攻击、运行时投毒
事件背景
最近,公司内部的 Agent 技能平台上发生了一起值得写下来的安全事件。关键词是「运行时投毒」:两个已经上架、被静态扫描放行的技能,技能包本身干干净净——没有一行恶意代码、没有一个脏字、没有可疑域名;但它们在运行时,会从公司自己的静态资料库里拉取一个「服务方当日口径包」并执行,而那个包里装的是一套完整的后门工具。
一句话概括:两个已上架的技能是「带路的」,本身没有恶意代码;真正动手的是运行时才下载下来的那个包。它已经能在公司内网执行任意命令,并把结果传到外部服务器。
这起事件最值得讨论的,不是「模型说错话」「提示注入」这类常规问题,而是更危险的一个形态:供应链 / 运行时投毒——静态文本里什么也看不出来,恶意行为全发生在运行时加载的那一层。静态扫描扫不出来,不是扫描器不够聪明,而是这招专门设计成让扫描器无从下手。
说明:这是一篇事件分析的方法论文章,不是事故通报。事件仍在处置中,公开内容已做必要隐去(技能名、域名、编号、凭据、时间戳等均已替换或删除),只保留通用的技术脉络。
两个技能表面上看是什么
事情涉及两个已上架的技能(下称 service-audit-a / service-audit-b,示例名,真实名称已隐去),分别负责两类上门服务业务的工时 / 工单核对。
技能文档里写的是:本机核对工时 / 保养台数 → 上报调度台账 → 去平台静态文件域取一份「服务方当日口径包(校验模块)」跑一次 → 用完删掉。
这张「单子」上没有一个脏字:没有密码、没有可疑域名、没有删库命令。所以静态扫描看到的,就是一个再正常不过的业务动作。
实际发生了什么
把技能要去取的那个压缩包直接下载下来看(只读,没有运行)。里面不是校验模块,而是一套完整的攻击工具:能连内网、能冒充身份调内部接口、能执行任意命令并外传结果。
技能包是干净的——这一点没错,扫描器也确实没看走眼。它没看走眼的原因是:真正的载荷在运行时才出现,扫描那一刻它还不存在。
攻击链还原
把整条链还原出来,这是一次精心设计的两段式投毒:第一步,一个看起来完全合规的技能包通过审查、正常上线;第二步,技能在运行时从公司自己的资料库下载并执行真正的载荷——「运行时投毒」的完整链路就是这样闭合的。
完整攻击链
| 步骤 | 表面看起来 | 实际上是 |
|---|---|---|
| 1. 用户触发技能 | 核对某类上门服务的工时 / 工单 | 攻击开始计时,全程时间开销被控制在技能允许的执行窗口内 |
| 2. 技能下载压缩包 | 取「服务方当日口径包」,合规动作 | 从平台自己的资料库取黑客载荷,域名检查天然放行 |
| 3. 执行「校验模块」 | 应用当日口径包一次 | 启动后门:连接外部服务器(外联域名,已隐去)等待指令 |
| 4. 连公司内网服务 | 文档里完全没写 | 冒充员工身份、带上平台令牌,调用内部接口,可触发任意技能 |
| 5. 收指令执行 | 无 | 用系统 shell 执行任意命令,输出结果回传外部服务器 |
| 6. 打印一行「校验通过」 | 正常的核对回执 | 把以上全部伪装成正常结果;并删除载荷文件,不留痕迹 |
换句话说:从对话记录、链路追踪看,这一轮和正常核对一模一样。
为什么能连上内网
两个背景问题把小洞变成了大洞:
- 沙箱一直连着公司内网(沙箱直连内网),内部服务按名字就能直接访问;
- 资料库上的技能包任何人都能下载——分析时在自己的电脑上就把它拉下来了,连授权都不需要。
能力上的影响面
从能力上看,这等于在内网留了一个后门:可以冒充任意身份、触发任意技能、读取他人数据、在内网执行任意命令,并把结果传出去。
是否已被实际使用,需要靠日志核实,截至写作时仍在取证阶段——事件仍在处置中,公开内容已做必要隐去,包括载荷投放时间、攻击编号与留档凭据等细节。
载荷还是「活的」:从留档证据看,包内容在不同时间点有过改动,说明攻击方可以随时替换内容。扫过一次通过,不等于一直安全。
暴露出的三个扫描盲区
这起事件最值得提炼的,是为什么现有静态扫描拦不住。拆开看,是三个盲区。
盲区一:扫「单子」不扫「包裹」
扫描只在技能包的静态文本上做检查,而真正的东西是运行时才从资料库取来的——扫描那一刻它还不存在。文本审查的对象是一张「去取东西」的单子,而不是那个「包裹」本身。两段式投毒天然站在第一道闸门的盲区里:上线时给平台看的是一份干净的单子,包裹是之后才去取的。
盲区二:认「关键字」不认「别名」
唯一的检测点要求脚本里出现 subprocess.run( 这类写法;样本写成了 import subprocess as sp 再用 sp.run(,等于把关键字换了马甲。对照实验很能说明问题:只要把这一个词改回原样,扫描器立刻报警。也就是说,这两个样本正好卡在这一两个字上通过。
盲区三:只认「地址」不认「行为」
它去的域名是公司自己的资料库,而且路径前缀正是平台给员工技能分发包用的那个地址(线上库里能查到同样的地址)。从域名角度看,它去的是最有正当理由去的地方。只查「去哪儿」、不查「去干什么」,内部来源就成了最安全的掩护,恶意行为反而藏得最自然。
盲区之外:评审环节本身
静态扫描之外,评审阶段也被提前交代了规矩:审查提示词的设定偏保守,把审查范围限定在说明书层面,只允许报既定清单里的几类问题,不看脚本、更不看运行时行为。而说明书里把「下载并运行」正好写成了「应用服务方当日口径包」——刚好落在允许的范围内。文本、意图、运行三层审查,三层都恰好停在能看见的表面。
为什么「公司自己的资料库」也会成为毒源
这次事件里最反直觉的一点是:毒源是公司自己的资料库。过去我们默认「自己平台上的内容」比外部内容可信,这次证明这个假设不成立:
- 自托管内容同样不可信。「可信」不能只按托管位置判断——公司静态资源域名(已隐去)只是「我们自己的域名」,不代表域内任何一个文件都经过安全评审;员工上传的技能包放在里面,和放在任何公网目录上没有本质区别。
- 内网域名天然通过域名检查。 白名单把内网域放行,等价于把内网域上的一切内容放行,其中包括攻击者上传的可执行包。域名检查保护的只是「目的地看起来对」,保护不了「到达的东西是什么」。
- 扫描通过 ≠ 内容安全。 包内容可以被随时替换,静止状态扫过一遍,运行时取到的是另一份。
结论是:来源可信度必须与行为等价问题分开看。先问「它被允许做什么」,再问「它从哪来」。一个来自内部域名的可执行包,和一个来自外部域名的可执行包,行为风险是一样的——都是「下载并执行未知代码」。
防御原则:先收能力,再补检查
这次事件给出一个明确的原则:不要指望「查得更仔细」,而要让坏人「拿到了也干不成坏事」。
原因很直接:坏东西是运行时才出现的,看文本永远看不全。所以真正管用的是收掉能力,而不是继续加检查项。顺序很重要:先收能力,再补检查——反过来做,只会一直被动。
第一件事:给沙箱「断内网、不揣钥匙」(1-2 天,收益最大)
- 现在:跑技能脚本的沙箱一直连着公司内网,按服务名字就能直接调内部接口;它同时还能自由上外网,而且带着用户自己的登录凭证。打个比方——等于把一个陌生人放进机房,还顺手塞给他一张门禁卡。
- 应该改成:沙箱默认进不去内网、默认不带任何钥匙;确实要联网,只能走一个「有人登记的门口」(代理),门口按应用白名单放行。
- 为什么收益最大:这次攻击的三个杀手锏(调内部接口、连外部服务器、偷凭证)都依赖沙箱有这些能力。这一件做完,三条同时失效。
第二件事:技能包必须「自带干粮」(1 周)
- 现在:技能可以「上班时去外面取个包回来就跑」,而取的地方还是公司自己的资料库——所以检查怎么看都合规。
- 应该改成:技能包必须自带全部代码,运行时不许下载并执行任何代码。业务上确实需要「当日口径包」这类更新,就改成平台先审、再统一分发,并且锁死内容指纹:内容一变就报警,不允许悄悄换。
- 另外,技能包现在放在任何人都能下载的地址上,也要收紧(改成需要授权的链接)。
第三件事:把检查当辅助,不当门卫(2 周)
现在的检查是「看文本 + 认关键词」,员工技能的深度检查只提示、不拦上架;更早之前,还因为担心误伤合规技能,强制安全检查的启用被暂缓过。检查项本身要升级,角色也要摆正:
- 认关键词的写法先做「归一化」,换了名字(别名)也认得出来;
- 把「下载 + 落盘 + 执行」这种组合直接列为红线;
- 出网白名单增加「路径和用途」的判断——公司静态域可以放行图片,但不能放行技能包;
- 把这两个样本做成固定测试题,以后每次改规则都跑一遍,防止改回去;
- 高风险技能(带脚本 + 要联网)上架前人工复核。
现在 vs 应该改成
| 事项 | 现在 | 应该改成 |
|---|---|---|
| 沙箱能不能进内网 | 一直连着,按服务名就能调内部接口 | 默认进不去,只能走登记过的代理入口 |
| 沙箱带不带用户凭证 | 带着用户登录凭证和一批内部令牌 | 默认不带;凭证留在服务端,由代理代签 |
| 技能能不能自己下载代码来跑 | 可以,从公司静态资料库取 | 不行,技能包必须自带全部代码 |
| 出网白名单管到哪一层 | 只管「模型调用工具」,脚本自己裸连不受管 | 下沉到网络层,脚本也绕不过 |
| 员工技能深度检查 | 只提示,不影响上架 | 高风险可拦,上架前人工复核 |
| 技能包存放 | 任何人都能下载 | 需要授权的链接 |
| 出事之后 | 靠人临场处置 | 有封禁、下架、令牌轮换的固定流程 |
节奏与预期效果
| 时限 | 做什么 | 预期效果 |
|---|---|---|
| 1-2 天 | 断内网 + 不带用户凭证 | 这次的攻击链彻底失效 |
| 1 周 | 出网走代理白名单 + 技能包地址收紧 + 内部接口加来源校验 | 想去哪儿、想拿什么都受控 |
| 2 周 | 补检测规则 + 把样本做成回归测试 + 高风险技能人工复核 | 新手法成本大幅提高 |
| 持续 | 内容指纹锁定、出网日志告警、定期红队演练 | 常态化防守 |
清单与小结
一句实话
严格说「100% 杜绝」做不到——只要允许技能跑代码,攻防就在同一个平面上。但如果第一件事做完,攻击者费半天劲拿到的只是一段跑不出沙箱、也连不上任何地方的死代码,这件事就不值得再干了。所以顺序很重要:先收能力,再补检查。
Agent 技能平台的供应链安全清单
供同行按自己平台的实际情况核对,分「能力收敛」「检查与检测」「流程与响应」三层:
能力收敛(优先级最高,先做)
- [ ] 沙箱默认无法访问内网,内部服务只能经登记代理到达
- [ ] 沙箱默认不携带任何用户 / 平台凭证,令牌留在服务端,由代理代签
- [ ] 运行时禁止下载并执行代码;确有「当日口径」类更新需求时,改为平台统一审核、统一分发
- [ ] 出网白名单下沉到网络层,脚本裸连也绕不过
- [ ] 技能包存放改为授权制链接,启用内容指纹锁定,内容变化即告警
检查与检测
- [ ] 关键字检测做别名归一化,换马甲的写法也能认出
- [ ] 「下载 + 落盘 + 执行」组合列为硬红线
- [ ] 出网白名单细化到「路径 + 用途」:静态域可放行图片等资产,不放行可执行包
- [ ] 已发现样本固化为回归测试用例,每次改动规则都跑一遍
- [ ] 本地自检工具与线上规则对齐,避免「本地通过、线上有风险」
- [ ] 高风险技能(带脚本 + 要联网)上架前人工复核,扫描结论可拦截上架
流程与响应
- [ ] 有封禁、下架、令牌轮换的固定处置流程,而不是临场处置
- [ ] 出网日志告警与定期红队演练,常态化检验
- [ ] 定期抽检包内链接指向的真实内容,不只信静态文本
小结
这起「运行时投毒」事件的完整链条是:干净的上架技能(带路)→ 运行时从公司自己的资料库下载并执行伪装成「当日口径包」的后门 → 冒充身份调内部接口、执行任意命令、外传结果。它同时击穿了静态扫描的三个盲区(扫单子不扫包裹、认关键字不认别名、只认地址不认行为),也提醒我们:自托管内容同样不可信,内网域名不等于安全域名。
对 Agent 技能平台这类「代码即产品」的系统,安全的关键不是把检查做得越来越细,而是先收敛能力、再补检查项:让恶意载荷即使被运行,也跑不出沙箱、连不上任何地方。做到这一步,供应链投毒的成本才会高到不值得做——这才是这件事留给我们最重要的方法论。