0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

PyPI Hades 攻撃(Mini Shai-Hulud) 2次被害の pantheon-agents に見る狡猾な攻撃手口を検証する

0
Posted at

PantheonOS (pantheon-agents) とは

Stanford の計算生物学ラボ (GitHub: aristoteleo) が公開しているゲノミクス/バイオインフォマティクス研究者・データサイエンティスト向けの LLM エージェントフレームワークで、月間 200DL 程度のニッチなパッケージ。

GitHub Advisory にヤバそうな情報が上がってきたので、ナニコレと思って調べてみた。
Trojanized pantheon-agents 0.6.1 and 0.6.2 on PyPI ship a credential stealer (supply-chain account compromise)

使ってる人はあんまりいないと思われるので、直接被害を受ける人は少数なはずだが、調べてみたら手口が巧妙で怖かったので、対策を検討する。

pantheon-agents は Hades 攻撃で long-lived PyPI API token を盗られたとのこと。

なお、現在は悪性バージョンは削除されている。トークンが盗まれたのが 6月8日、正規版 0.6.4 の publish が 7月6日 なので、汚染版が「最新版」として降ってくる状態だったのは最大4週間程度と見られる。

Hades とは

Hades は、2025年に Node.js エコシステムで猛威を振るった Shai-Hulud ワームを Python エコシステムに移植したもので、全体では 19〜26パッケージが汚染されたとのこと。

2026年6月8日に、汚染されたバイオインフォマティクス系パッケージ6件が1分以内にまとめて publish された。今回の pantheon-agents はこの6件には含まれておらず、同じ分野のパッケージとして二次被害を受けた側になる。

この Hades は、盗む対象が広域で、

  • AWS/GCP/Azure の認証トークン
  • Kubernetes secrets
  • GitHub の PAT と Actions トークン
  • PyPI/npm/RubyGems の publish 用認証情報
  • SSH keys
  • Docker registry 設定
  • .env
  • シェル履歴
  • AIアシスタントの設定ファイル

と、汚染されたらその環境にある認証系の情報は全滅。

今回で言えば、バイオ系コミュニティの(人間の)依存関係グラフを起点とし、そのコミュニティのメンテナー権限を刈り取っていくようになっている。悪性パッケージに汚染されると、その人が publish している別のパッケージが汚染される。

Hades の巧妙な罠

Hades は、Shai-Hulud を Node.js から Python に移植する際に、ワームを Python で書き直すのではなく、JavaScript 実行のための Bun ランタイムを利用しているため、一般的な Python 監視では、Python プロセスが JavaScript ランタイムを起動するという想定外の動きを検知せず、node 監視では、プロセスが node ではなく bun であるため検知されづらい。Python で書き直すのが面倒だったということもあるんだろうけど、狡猾に監視網をすり抜けている。

Endor Labs はこれを「Python を意識したプロセス監視と、node サブプロセス生成を検知する EDR の両方を回避するため」と分析している。

そして、Hades は永続化として、GitHub Actions のワークフロー注入、IDE の hook ファイル(.claude/settings.json, .vscode/tasks.json, .gemini/settings.json)への書き込みが確認されている。

この永続化がしんどいのは、hook ファイルを(少なくとも私は)常にチェックしたりはしていないのと、GitHub Actions に注入されてしまうと、ローカルでパッケージを削除しようが、認証情報をローテーションしようが、何なら PC を初期化しようが、攻撃者が再びコードを実行できる状態にあるという点。なので、ローカルマシンにある全ての認証情報をローテーションする必要がある。

なのだけど、なのだけども!

盗まれたトークンをローテーションしようとすると、gh-token-monitor という常駐プロセスが待ち構えている。

脅し文句自体は、被害者が調査中に必ず目にする場所に事前に仕込まれている。

IfYouRevokeThisTokenItWillWipeTheComputerOfTheOwner

Wiz は git のコミットメッセージとして現れると報告しており、「トークン名ではない」と明示的に否定している。一方で npm トークンの description に付けられたとする情報もあり、出典によって食い違っている。

そしてデーモン自体は何も言ってこない。黙って rm -rf ~/ を実行するだけで、ATR-2026-00525 にも被害者向けメッセージの記述はない。つまりこの「脅し」は、公開された脅威情報を読んでいる人にしか届かない。

概要は、

項目 内容
名称 gh-token-monitor
macOSの常駐方法 LaunchAgent ~/Library/LaunchAgents/com.user.gh-token-monitor.plist
Linux user 単位の systemd サービス ~/.config/systemd/user/gh-token-monitor.service
本体スクリプト ~/.local/bin/gh-token-monitor.sh
盗んだトークンの保管先 ~/.config/gh-token-monitor/token
動作 60秒ごとに api.github.com/user を盗んだトークンで叩く
発火条件 HTTP 401/4xx = トークンが失効
発火時の動作 rm -rf ~/ (ホームディレクトリ全削除)

※ LaunchAgent / systemd のパス、ポーリング間隔、発火条件、rm -rf ~/ は ATR-2026-00525 で確認できる。本体スクリプトとトークン保管先の2行は同ルールには記載がないため、別出典による。

いわゆる Deadman Switch。「盗まれたトークンを失効させる」というインシデント対応で真っ先にやるべき正しい操作をトリガーにしている。

なぜ黙って消さないのかというと、StepSecurity の分析では、「データ破壊のリスクを突きつけることで、セキュリティチームが盗まれた認証情報を即座に失効させるのをためらわせ、アクセスを維持する時間を稼ぐ」ことを意図しているとのこと。

なるほど、極悪。

悪性の pantheon-agents

時系列

version repo (GitHub) PyPI
0.6.0 v0.6.0 = 0ba8274a9 (2026-05-24 21:42 UTC) 2026-05-24 21:41 に正規アップロード
0.6.1 v0.6.1 = 511d34977 (2026-05-25 22:42 UTC)
コミットメッセージ: feat(_ping): include cpu_count + mem_total_mb in activity st...
認証情報搾取ツール入り
(現在は削除済み)
0.6.2 タグなし。リポジトリに一切存在しない 認証情報搾取ツール入り
(現在は削除済み)
0.6.3 なし なし
0.6.4 v0.6.4 = 8cc6ae86f (2026-07-06 18:41 UTC) 2026-07-06 18:42 に正規アップロード

repo 上の v0.6.1 は、死活監視に CPU コア数とメモリ量を含めるという無害な変更で、おそらくタグは打ったが、PyPI へは publish していなかった。攻撃者はその空いた番号を横取りして、汚染版を PyPI に publish した模様。

こうなると、利用者は、v0.6.1 が出たなと思って repo を見に行くと v0.6.1 タグが存在し、diff も無害な機能追加。注意深く確認して「問題なし」と誤認してしまう。

巧妙なのにそれを崩しての v0.6.2 は一体何?

考えられるのは2つで、

  • ペイロードにバグがあって、その修正
  • 復旧経路の先回り

復旧経路の先回りは、PyPI は append-only なので、v0.6.1 が汚染されていると気づいたメンテナーが正規の修正を出すなら v0.6.2 になるが、それを先につぶしておいたというもの。repo 上で v0.6.2 がタグ付けされても、PyPI 上には、悪性パッケージが既にあり、上書きできない。実際、メンテナーが v0.6.3 を飛ばして、v0.6.4 を publish していることからもこういった攻防はあったものと思われる。なお PyPI は削除されたバージョン番号の再利用も許さないので、番号を潰しておく先読みは削除後も有効なまま残る。実際 0.6.1 / 0.6.2 は現在 API からキーごと消えており、0.6.00.6.4 と飛んでいる。

仕組み

pantheon-agents は、wheel で配布(ビルド済み)されており、sdist(ソースコード)とは違い、インストール時に、パッケージ内のコードを実行したりはしない。なので、できることはファイルを置くだけなのだが、置かれるファイルに問題がある。

置かれるのは、ローダーとペイロードの2つで、そのローダーは、.pth になっている。これは、 Python 自身が起動時に読み込んで実行するファイルであり、import しなくても実行されてしまう。つまりは、安全と思わせつつ、タイミングをずらして実行する設計になっている。

ローダー(pantheon_agents-setup.pth)が実行されると、GitHub から Bun を調達し、ペイロードを起動(_index.js)。難読化されたペイロードが認証情報を根こそぎ搾取し、ワーム化する。 GitHub Actions ワークフロー注入、 IDE hook 注入、gh-token-monitor などの永続化関連は報告がないが、.pth が site-packages に配置された時点で、それ自体が永続化なので、あとは目立たないようになっている。

具体的には、ローダーは exec() に難読化した文字列を渡し、import を1文字エイリアスに機械的にリネームすることで呼び出しを隠している。Bun は /tmp/b/bun に置かれて実行され、/tmp/.bun_ran という sentinel ファイルで再実行を抑止する。_index.js はホームディレクトリではなく、インストールされたパッケージのディレクトリ内にある。

IoC 場所
ローダー site-packages/pantheon_agents-setup.pth
Bun 本体 /tmp/b/bun
実行済みマーカー /tmp/.bun_ran
ペイロード パッケージディレクトリ内の _index.js

問題点の整理

私は pantheon-agents を使うわけでもないし、バイオ系でもないけども、このような狡猾な攻撃手口を見てみると、他人事とは思えない。整理すると、問題点は以下の7つに分類される(した)。

  • A: 何を信じるか
  • B: いつ発火するか
  • C: 何を盗られるか
  • D: どこに残るか
  • E: どう対応するか
  • F: どう広がるか
  • G: いつ知ることができるか

これは pantheon-agents の対策ではなく、これまで見てきた手口に対して、一般化してその対策をまとめるものである。どこまでやるかは、組織内で議論していただきたい。

A: 何を信じるか

pantheon-agentsv0.6.1 では、正規 repo と PyPI 公開物が異なった。これは「異なる可能性がある」ではなく実際には、「同じである保証がない」である。

これは、PyPI だけの話しではなく、npm でも、Composer の dist zip でも、何等かのパイプラインにある curl -OL .../hoge.zip なども全て同じ話し。

対策例

  • バージョン厳密固定+ハッシュ検証
  • 可動参照を使わない
  • 配布元が公開したチェックサムと照合
  • プロキシ/ミラーで隔離
  • provenance を持つ発行元を優先

B: いつ発火するか

pantheon-agents は、.pth を置く。これは Python インタープリタ起動時に走るので、import していなくても、環境に入っていれば発火済み。つまり、「使用」ではなく「存在(インストール)」がトリガーになっている。npm の postinstall, Composer の post-install-cmd も同じ。

使ってないから大丈夫という感覚は成り立たない。

対策例

  • ビルド/インストールを認証情報のない環境で行う
  • インストールを人間の判断点にする(AI任せにしない)
  • install スクリプトを既定で無効化 (--ignore-scripts, --no-scripts, --only-binary=:all:...)
  • プロジェクトごとに環境を分離(グローバルにしない)

C: 何を盗られるか

盗まれる対象は、~/.aws, ~/.pypirc, ~/.npmrc, SSH key, .env, シェル履歴など。

開発ツールのエコシステム全体が「長期有効な認証情報をホームディレクトリに平文で置く」ことを前提に設計されていることが、たった1回の不正なインストールを「全滅」に変えている根本原因。頑張って検知するのではなく、盗まれても使えない形にする対応が必要。

対策例

  • 長期認証情報そのものをなくす(CI は OIDC 連携でクラウド鍵を保持しない。ローカルは SSO/STS の一時認証に)
  • ハードウェア保護鍵(YubiKey, Secure Enclave)
  • 公開・デプロイ用の資格情報を開発端末に置かない (publish は CI からのみ)
  • OS キーチェーン/シークレットマネージャー
  • スコープと期限を絞る(組織全体 PAT をやめ、fine-grained + 短 TTL)
  • 長時間プロセスの環境変数に置かない(env は使用時点で注入)

D: どこに残るか

永続化の注入が、IDE の hook ファイルというのは、hook ファイルは「データファイルの顔をしたコード」であるとも言える。設定ファイルと認識しているので、レビューする習慣が薄く、しかし、これが実行されてしまう。

また、永続化が GitHub Actions などローカルを離れパイプラインに注入されるのは、恐ろしい。ローカルマシンを初期化しても残るし、なによりパイプラインが置かれている共有インフラは認証情報を持っている。永続化+伝播が同じ仕掛けで実現可能。

対策例

  • まず何があるか列挙して一覧にする
  • 変更をベースラインと突き合わせる(バージョン管理下に置けるものは置き、置けないものはローカルで定期 diff)
  • clone してきた repo の設定を自動で信頼しない
  • 依存更新のレビュー項目に含める(依存の diff に .pth や scripts が増えていないか)

E: どう対応するか

インシデント対応の標準手順が攻撃面になっている。IoC や挙動の公開情報を鵜呑みにして手順を組めない。これはもはや詰みなのでは?と思える。

対策例

  • 順序を事前に書いておく(検知→保全→封じ込め→失効→復旧)
  • 保全を最優先(イメージ取得・データ退避を、何か触る前に)
  • 失効ではなくネットワーク遮断で封じ込める
  • 失効は隔離/電源断の後に行う
  • 公開 IoC の細部を前提にしない

F: どう広がるか

pantheon-agents のメンテナーは被害者であると同時に、意図せず加害者になった。開発者の認証情報が盗まれた場合に、攻撃者が到達できる先に対して、封じ込めの義務が自己防衛とは別に発生する。

対策例

  • 端末の資格情報がどこまで届くかを一覧化
  • 開発端末に本番デプロイキーを置かない
  • 保護ブランチ・必須レビュー・署名コミット
  • 通知経路を整備する

G: いつ知ることができるか

先に知ることはできない。監視は誰かが公開したあとにしか効かない。常に後手。

対策例

  • 知識ゼロで効く統制を監視より先に置く
  • 新規リリースの隔離期間を設ける
  • 監視は予防ではなく対応のためと位置づける
  • 検知が実際に動くことを検証する

ちなみに、pantheon-agents の MAL レコード公開が 6月6日で悪性版の publish が 6月8日と記録の方が2日速い。レコード ID の採番日とバッチ処理日の差か、攻撃が6月8日より前だったかは分からない。そして、Advisory は8月26日と11週遅れで公開されているが、現在の Advisory の記述では「アカウント復旧と Trusted Publishing への移行後に PyPI 配布を再開する」と書かれているが、0.6.4 は7月6日に既に publish 済みで、内容と現状が合っていない。Advisory は 6 月に想起されたが、何らかの理由で公開が8月26日にずれたのではないかと思われる。裏付けるものはないけど。

参考

一次情報

キャンペーン分析

検査ツール

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?