代码比模型先运行
Pluto Security 发现了 31 个伪装成小型 Qwen2 测试检查点的 Hugging Face 仓库。没有一个里面有模型。每个都放了一个 Python 文件,只要用 trust_remote_code 加载模型就会立即运行,安装隐藏的挖矿程序、启动钩子,部分版本还会打开一个反向 shell。主打仓库通过了 Hugging Face 的扫描器,下载量达到 28,412 次,下架后还有一份几乎一模一样的副本留在线上。编码代理写测试时会去找小型测试模型。在 Bromure Agentic Coding 中,这次加载在虚拟机里运行,挖矿程序连不上矿池,Security Timeline 把每条连接对应到发起它的工具调用,一次回滚就能清除载荷植入的东西。
权重文件只有 42 字节,里面没有任何张量。真正干活的是一个 Python 文件,模型一加载它就运行,任何模型代码都还没开始动。
10 月 29 日,Pluto Security 发布了 Load/Mine,分析了一场自 9 月起在 Hugging Face 和中国的 ModelScope 上进行的攻击活动。Pluto 在七个账号中发现了 31 个恶意仓库。它们伪装成小型 Qwen2 测试检查点,名字类似 qwen2-tiny,合计记录到超过 32,000 次下载。BleepingComputer 同一天也做了报道。
Pluto 用一句话概括了这场活动:“The code runs before the model does”(代码比模型先运行)。
加载模型如何变成挖矿
这次攻击没有用投毒的权重,也没有用 pickle 漏洞。它用的是一个功能。Hugging Face 的模型可以附带自己的 Python 代码,只要调用方传入 trust_remote_code=True,Transformers 库就会执行这段代码。仓库的 config.json 在 auto_map 条目里指定一个模块和一个类,例如 modeling_evil2.EvilModel。Transformers 下载这个 .py 文件并导入它。文件顶层的一切都会在导入时执行,甚至在类被返回之前。
在这些仓库里,model.safetensors 只有 42 字节。里面没有模型。所有工作都由 Python 文件完成:
- 它先扎根。 它在
/tmp/.cch/svc.py写入一个每两分钟重启挖矿程序的看门狗,并在~/.bashrc、/root/.bashrc和/root/.profile追加启动钩子。第一波的版本还会在authorized_keys植入一把名为qx-5090-op的 SSH 密钥,并在/etc/cron.d/.sync-helper放一个 cron 任务。 - 它下载挖矿程序并藏起来。 下载器用空的代理服务器处理器构建,因此会忽略企业可能依赖的
HTTP_PROXY和HTTPS_PROXY设置。40.6 MB 的二进制文件改过 UPX 标记,让upx -d拒绝解包。它从内存中的匿名文件运行,进程名为[kworker/u20:1],通过 Kryptex 在 8048 端口挖 Pearl。 - 它开一扇门。 三个版本会加上连往
212.32.253.3443 或 9443 端口的反向 shell,断线就重连。 - 它冲着主机去。 第二波一个相关的脚本会从特权容器挂载主机的磁盘,在底下的服务器上拿到 root。
扫描过关,又卷土重来
Hugging Face 用杀毒软件以及 Protect AI 和 JFrog 的合作工具扫描上传内容。Pluto 说,这些工具找的是已知恶意软件和不安全的模型格式,不看 Python 文件做了什么。第二波的主打仓库 MENXIANGYU/qwen2-tiny“scanned clean while it collected 28,412 downloads”(累计 28,412 次下载期间扫描一直没问题)。一个合作扫描器对载荷文件的判定是“not a machine-learning model”(不是机器学习模型)。
Pluto 把第一波报告给了 Hugging Face 和 ModelScope,两者都删除了账号。几天之内,19 个新仓库在新账号下出现。Pluto 发布报告几小时后,操作者公开了一个容器逃逸脚本。Hugging Face 停用了主打仓库,但 BleepingComputer 证实,一份几乎一模一样的副本 htt0118/qwen2-tiny 仍在线上,带着同样的挖矿程序和反向 shell。
小型测试模型正是你的代理会找的东西
这场活动是为运行 Xinference、GPUStack 等推理平台的 GPU 服务器打造的。但这个诱饵在开发者的电脑上同样有效,因为编码代理在为调用模型的代码写测试时,会去找一个小型 Qwen2 检查点。让代理给你的嵌入流水线或微调脚本加一个测试,它就会去找一个小到几秒就能加载的模型。这场活动的名字,qwen2-tiny 及其变体,就是为了对上这种搜索。
代理不必粗心也会打开这个开关。仓库附带自定义代码时,Transformers 会停下来,告诉调用方要传入 trust_remote_code=True。对一个正在修失败测试的代理来说,这就是一个消息里印好修法的错误。它加上参数,重跑测试。测试过不过已经无所谓:挖矿程序在导入时就启动了。
在笔记本上,这个文件以你的身份运行。~/.bashrc 里的启动钩子会在你之后打开的每个终端里运行。反向 shell 给操作者一个命令提示符,你的 ~/.ssh、云凭据、gh 登录和 HF_TOKEN 都在手边。Pluto 给加载过这些模型的人的建议是:把这台机器当作已被攻陷,并轮换这个进程能接触到的每一组凭据。
Pluto 的加固建议中,有一句描述了你想要的环境:运行不可信的模型代码时,要“with limited privileges, restricted egress and no production secrets”(权限受限、出站受限,而且没有生产环境的机密)。
同一个测试,在 Bromure Agentic Coding 里
Bromure Agentic Coding 在你 Mac 上的 Ubuntu 虚拟机里运行 Claude Code、Codex、Grok、Kimi 和 omp。每台机器都有自己的磁盘、自己的主目录和自己的网络策略,策略在 Mac 上执行,代理碰不到。下面是同一个测试在 Bromure 机器上的情况,一次看载荷的一个阶段。
挖矿程序连不上矿池
虚拟机发起的每条连接都会经过 Mac 上 Bromure 的虚拟交换机和代理服务器。80 和 443 端口在交换机处就被转进代理服务器,所以带着空代理服务器处理器的下载器照样会被拦截。这场活动用来绕过企业代理服务器设置的手法在这里无处可绕,因为拦截不依赖虚拟机里的任何设置。
在 Sandbox Policy 中把 Unmatched traffic 设为 Deny,或者把机器切换到 OpenShell + Sentry 引擎,那里除非有规则允许,否则一律拒绝。入门策略允许只读的 GitHub、npm 和 PyPI 以及 Ubuntu 的镜像站,你再为下载加上 huggingface.co。这样一来,挖矿程序连往 prl-us.kryptex.network:8048 的连接不匹配任何规则,会被拒绝。连往 212.32.253.3 的反向 shell 也一样,两个端口都是。OpenShell 机器默认开启 Tell the agent when policy blocks it,代理还会在对话中读到什么被拦了,以及它应该停下来问你,而不是花接下来十分钟想办法绕过去。
在开启 Enforce binaries 的 OpenShell 机器上,每条连接还会对应到发起它的程序,每个程序的哈希在第一次运行时就被固定。从内存匿名文件启动的挖矿程序,是没有任何规则提到的程序。把进程名改成 [kworker/u20:1] 也改变不了这一点,因为证明器识别的是二进制文件本身,而不是它给自己起的名字。
你能看到是哪个工具调用干的
操作者选 [kworker/u20:1] 这个名字,是为了骗过在繁忙服务器上看 ps 或 top 的人。在启用内核哨兵的 Bromure 机器上,虚拟机的内核会通过一条代理杀不掉的私有通道,把每次程序启动和每条连接报告给 Mac。每条被拦的连接都会连同背后的调用链出现在 Security Timeline 中,大致像 Tool pytest tests/test_embed.py: claude → bash → python → TCP 212.32.253.3:443。
双击那一行,network lineage 会呈现完整经过:你给的提示、代理在调用前写了什么、确切的命令、从代理到发起连接的那个进程之间的每个进程,以及是哪一层拒绝的。当一次测试运行去连一个裸 IP 地址,你会看到一行红色,里面就是加载模型的那条命令。
在 Guardrails 中设为 Quarantine 的代理看门狗,会根据同样的信号行动,不用等你。一分钟内拦截次数激增,或者会话稳定后出现新的目的地,超过阈值,Bromure 就会在几毫秒内切断虚拟机的网络并暂停它。告警会列出它看到的内容。Keep quarantined 会在网络仍然切断的状态下恢复虚拟机,让你进去查看。
虚拟机里的密钥是替身
假设载荷的某个版本走得更远,在一台你保留默认防火墙的机器上。操作者拿到了一个 Ubuntu 虚拟机的 shell,而里面的每组凭据都是替身。环境中的 GH_TOKEN 和 ~/.git-credentials 里的令牌都是假的,Mac 上的代理服务器只在发往 GitHub 的途中才换成真的。把你的 Hugging Face 令牌加到 Other API keys,环境变量名设为 HF_TOKEN,主机过滤设为 huggingface.co,虚拟机拿到的就是只在那里有效的 brm_… 替身。SSH 密钥从不进入虚拟机:代理可以请求签名,密钥字节留在 Mac 上。
替身也是绊线。如果载荷把 GitHub 替身发到 HTTPS 的 GitHub 以外的任何地方,代理服务器会以 HTTP 451 拦下请求、暂停虚拟机,并弹出一条告警,指出是哪组凭据、原本要发往哪台主机。
清理只要点两下
Pluto 为受感染主机列的清理清单很长:找出看门狗、移除 shell 钩子、删除 cron 任务、从六个 authorized_keys 路径中去掉 SSH 密钥、检查有没有恶意注册,如果没把握就从可信镜像重建。Bromure 机器本来就有这个可信镜像。Reset disk 会从基础镜像重新克隆系统盘,把 /etc/cron.d/.sync-helper 和载荷写在主目录以外的其他东西一并带走。Rewind home… 会把 /home/ubuntu 回滚到之前某次启动时的检查点。Bromure 每次机器启动都会创建一个,所以测试前的 ~/.bashrc 就在其中。你与主机共享的文件夹在 Mac 上,不受影响。
在开启 Enforce binaries 的 OpenShell 机器上,这份清单的大部分根本写不进去。代理没有 root 权限,所以 /root/.bashrc 和 /root/.profile 的钩子、cron 任务以及对 root 的 authorized_keys 的写入都会被拒绝,每次拒绝都是 Timeline 里的一行 Guest sandbox。
出站在 Mac 上决定
不管虚拟机的设置怎么写,80 和 443 端口都会进代理服务器。把未匹配的流量设为拒绝,矿池和 shell 都会被拦。
每条连接都有名字
内核哨兵把每条连接对应到背后的工具调用和进程链。伪装过的挖矿程序,照样会显示为那次模型加载。
是替身,不是密钥
虚拟机里的 GitHub、云和 Hugging Face 令牌都是假的,只在各自的主机上有效。发到别处就会触发告警。
随时一台干净的机器
Reset disk 和 Rewind home 会把虚拟机恢复到测试前的样子,持久化也一并清除。
把模型仓库当作软件包对待
Pluto 在报告中列出了指标。如果你曾在启用自定义代码的情况下加载过这些模型,请检查 /tmp/.cch/、带挖矿参数的 [kworker/u20:1] 进程和启动钩子,然后轮换那台机器能接触到的每一组凭据。在对任何仓库开启 trust_remote_code 之前,先读它的 modeling_*.py,并固定你读过的那个 commit。
Pluto 的报告表明,模型仓库在扫描器把它当作文件格式检查的同时,实际上像代码仓库一样运作。如果你的代理会给测试挑模型,迟早会加载这样的文件。在 Bromure 机器上,这个文件在一个只有替身密钥、出站策略由 Mac 执行的虚拟机里运行,事后你还能把机器回滚到干净的检查点。
想看 trust_remote_code 检查本身失效的更早案例,请读标志说了不。想了解 Bromure 如何筛查代理安装的软件包,请读那个版本才发布两小时。