コードはモデルより先に動いた
Pluto Security は、小さな Qwen2 のテスト用チェックポイントを装った Hugging Face のリポジトリを 31 個見つけた。どれにもモデルは入っていなかった。入っていたのは、trust_remote_code でモデルを読み込んだ瞬間に動く Python ファイルで、隠れたマイナー、起動フック、一部のビルドではリバースシェルを仕込む。目玉のリポジトリは Hugging Face のスキャナーを通過して 28,412 回ダウンロードされ、削除後もほぼ同じコピーが公開されたままだった。コーディングエージェントはテストを書くとき、小さなテスト用モデルを探しにいく。Bromure Agentic Coding では、その読み込みは VM の中で動き、マイナーはプールに届かず、Security Timeline が各接続をそれを開いたツール呼び出しに結びつけ、巻き戻しでペイロードが仕込んだものを消せる。
重みファイルは 42 バイトで、テンソルは一つもなかった。仕事をしたのは Python ファイルで、モデルが読み込まれた瞬間、モデルのコードが何かをする前に動いていた。
10 月 29 日、Pluto Security は Load/Mine を公開した。9 月から Hugging Face と中国の ModelScope で続いているキャンペーンの分析だ。Pluto は 7 つのアカウントにまたがる 31 個の悪意あるリポジトリを見つけた。どれも qwen2-tiny のような名前の小さな Qwen2 テスト用チェックポイントを装い、記録されたダウンロードは合計 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 ファイルがする。
- 居座る。 マイナーを 2 分ごとに再起動するウォッチドッグを
/tmp/.cch/svc.pyに書き込み、~/.bashrc、/root/.bashrc、/root/.profileに起動フックを追記する。第一波のビルドはさらに、qx-5090-opという名前の SSH 鍵をauthorized_keysに、cron ジョブを/etc/cron.d/.sync-helperに仕込む。 - マイナーを取ってきて隠す。 ダウンローダーは空のプロキシハンドラーで作られているので、企業が頼りにしているかもしれない
HTTP_PROXYとHTTPS_PROXYの設定を無視する。40.6 MB のバイナリは UPX のマーカーが書き換えられていて、upx -dでは展開できない。匿名のメモリ上ファイルから[kworker/u20:1]というプロセス名で動き、ポート 8048 で Kryptex 経由で Pearl を採掘する。 - 扉を開ける。 3 つのビルドは、
212.32.253.3のポート 443 か 9443 へのリバースシェルを追加する。切れるたびにつなぎ直す。 - ホストを狙う。 関連する第二波のスクリプトは、特権コンテナからホストのディスクをマウントし、その下のサーバーで 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 が同じマイナーとリバースシェルを載せたまま公開されていることを確認した。
小さなテスト用モデルこそ、エージェントが探すもの
このキャンペーンは、Xinference や GPUStack のような推論プラットフォームを動かす GPU サーバーを狙って作られた。だがこの餌は開発者のマシンでも効く。モデルを呼ぶコードのテストを書くとき、コーディングエージェントは小さな Qwen2 チェックポイントを探すからだ。埋め込みパイプラインやファインチューニングのスクリプトにテストを追加してとエージェントに頼めば、数秒で読み込める小さなモデルを探しにいく。このキャンペーンの名前、qwen2-tiny とその派生は、まさにその検索に引っかかるように付けられている。
フラグを立てるのに、エージェントが不注意である必要はない。リポジトリが独自コードを同梱していると、Transformers は止まって、呼び出し側に trust_remote_code=True を渡すよう告げる。失敗するテストを直しているエージェントにとって、それは修正方法がメッセージに書いてあるエラーだ。引数を足して、テストを再実行する。テストが通るかどうかはもう関係ない。マイナーはインポートの最中に起動している。
ノートパソコンでは、このファイルはあなたとして動く。~/.bashrc の起動フックは、その後に開くすべてのターミナルで動く。リバースシェルは運営者に、あなたの ~/.ssh、クラウドの認証情報、gh のログイン、HF_TOKEN に手が届くプロンプトを渡す。これらのモデルを読み込んだ人への Pluto の助言は、マシンを侵害されたものとして扱い、プロセスが触れられたすべての認証情報をローテーションすることだ。
Pluto の堅牢化の助言には、目指すべき構成を表す一文がある。信頼できないモデルのコードは「with limited privileges, restricted egress and no production secrets」(権限を絞り、外向き通信を制限し、本番のシークレットを置かずに)動かすこと。
同じテストを Bromure Agentic Coding で
Bromure Agentic Coding は、Claude Code、Codex、Grok、Kimi、omp を Mac 上の Ubuntu VM で動かす。各マシンは専用のディスク、専用のホーム、専用のネットワークポリシーを持ち、そのポリシーはエージェントの手が届かない Mac 側で適用される。以下は、同じテストを Bromure のマシンで実行した場合を、ペイロードの段階ごとに見たものだ。
マイナーはプールに届かない
VM が開く接続はすべて、Mac 上の Bromure の仮想スイッチとプロキシを通る。ポート 80 と 443 はスイッチでプロキシへ振り向けられるので、空のプロキシハンドラーを持つダウンローダーも結局は捕まる。企業のプロキシ設定をすり抜けるためのキャンペーンの小細工は、ここではすり抜ける相手がない。傍受は VM 内のどの設定にも依存しないからだ。
Sandbox Policy で Unmatched traffic を Deny にするか、マシンを OpenShell + Sentry エンジンに移す。後者では、ルールが許可しないものはすべて拒否される。スターターポリシーは GitHub、npm、PyPI の読み取りと Ubuntu のミラーを許可しており、ダウンロード用に huggingface.co を足せばよい。するとマイナーの prl-us.kryptex.network:8048 への接続はどのルールにも一致せず、拒否される。212.32.253.3 へのリバースシェルも、どちらのポートでも同じだ。OpenShell のマシンでは既定でオンの Tell the agent when policy blocks it により、エージェントは何がブロックされたか、そして止まってあなたに尋ねるべきことを会話の中で知る。次の 10 分を回避策探しに使うことはない。
Enforce binaries を有効にした OpenShell のマシンでは、各接続はそれを開いたプログラムとも照合され、各プログラムのハッシュは初回実行時に固定される。匿名のメモリ上ファイルから起動したマイナーは、どのルールにも名前のないプログラムだ。プロセス名を [kworker/u20:1] に変えても変わらない。アテスターが識別するのはバイナリそのもので、自称する名前ではないからだ。
どのツール呼び出しがやったかが見える
運営者が [kworker/u20:1] という名前を選んだのは、忙しいサーバーで ps や top を眺める人をだますためだ。カーネルセンチネルを有効にした Bromure のマシンでは、VM のカーネルがすべてのプログラム起動とすべての接続を、エージェントが止められない専用チャネルで Mac に報告する。ブロックされた接続はそれぞれ、背後のチェーンとともに Security Timeline に載る。たとえば Tool pytest tests/test_embed.py: claude → bash → python → TCP 212.32.253.3:443 のように。
その行をダブルクリックすると、network lineage が一部始終を見せる。あなたが与えたプロンプト、呼び出しの前にエージェントが書いたこと、正確なコマンド、エージェントから接続したプロセスまでのすべてのプロセス、そしてどの層が拒否したか。テストの実行が生の IP アドレスに接続しようとすれば、モデルを読み込んだコマンド入りの赤い行が出る。
Guardrails で Quarantine に設定したエージェントウォッチドッグは、同じシグナルにあなたを待たずに反応する。ブロックの連発、あるいはセッションが落ち着いた後の新しい接続先が 1 分以内にしきい値を超えると、Bromure は数ミリ秒で VM のネットワークを切り、一時停止する。アラートには見たものが並ぶ。Keep quarantined はネットワークを切ったまま VM を再開するので、中を調べられる。
VM の中の鍵は代用値
ペイロードのあるビルドがもっと先まで進んだとしよう。既定のファイアウォールのままにしておいたマシンだ。運営者は Ubuntu VM のシェルを握るが、そこにある認証情報はすべて代用値だ。環境変数の GH_TOKEN も ~/.git-credentials のトークンも偽物で、Mac 上のプロキシが GitHub へ向かう途中でだけ本物に差し替える。Hugging Face のトークンを Other API keys に環境変数名 HF_TOKEN、ホストフィルター huggingface.co で登録すれば、VM が受け取るのはそこでしか使えない brm_… の代用値だ。SSH 鍵はそもそも VM に入らない。エージェントは署名を頼めるが、鍵のバイト列は Mac に残る。
代用値は仕掛け線でもある。ペイロードが GitHub の代用値を HTTPS の GitHub 以外へ送れば、プロキシはそのリクエストを HTTP 451 でブロックし、VM を一時停止し、どの認証情報がどのホストへ向かっていたかを示すアラートを開く。
後始末はクリック 2 回
感染したホストに対する Pluto の後始末リストは長い。ウォッチドッグを見つけ、シェルのフックを外し、cron ジョブを消し、6 つの 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 で決まる
VM の設定が何であれ、ポート 80 と 443 はプロキシへ向かう。一致しない通信を拒否にしておけば、プールもシェルも拒否される。
どの接続にも名前がある
カーネルセンチネルは各接続を、背後のツール呼び出しとプロセスチェーンに結びつける。偽装したマイナーも、モデルの読み込みとして現れる。
鍵ではなく代用値
VM 内の GitHub、クラウド、Hugging Face のトークンは、それぞれのホストでしか使えない偽物だ。ほかへ送ればアラートが鳴る。
いつでもきれいなマシン
Reset disk と Rewind home で、VM は永続化も含めてテスト前の状態に戻る。
モデルのリポジトリはパッケージとして扱う
Pluto は報告の中で指標を挙げている。独自コードを有効にしてこれらのモデルのどれかを読み込んだなら、/tmp/.cch/、採掘用の引数を持つ [kworker/u20:1] プロセス、起動フックを確認し、そのマシンが触れられたすべての認証情報をローテーションしてほしい。どのリポジトリでも trust_remote_code をオンにする前に、その modeling_*.py を読み、読んだコミットに固定すること。
Pluto の報告が示すのは、スキャナーがファイル形式として調べている間に、モデルのリポジトリがコードのリポジトリとして働いていたということだ。エージェントがテスト用のモデルを選んでいるなら、いつかこうしたファイルを読み込む。Bromure のマシンなら、そのファイルは代用値の鍵と、Mac で適用される外向き通信ポリシーを持つ VM の中で動き、あとでマシンをきれいなチェックポイントへ戻せる。
trust_remote_code のチェックそのものが破られた以前の事例は、フラグはノーと言った を。エージェントがインストールするパッケージを Bromure がどう審査するかは、そのリリースは公開から二時間だった を読んでほしい。