この記事は、私自身の体験を紹介するものです。記事の内容を試した結果や、それによって生じた損害について、私は責任を負いかねます。実施は、ご自身の責任と判断でお願いします。 コマンド実行についてはUbuntuで確認しており特に変更などはしませんが、自己責任でお願い致します。 因みにスクリプトはClaude Fable5.2で作ってます(笑)

はじめに

2026年11月6日、セキュリティ企業のSocketが、RubyGemsに公開された悪意あるパッケージ strutil-core の分析結果を公開しました。

「RubyGemsは使っていないから関係ない」と思った方も、少しだけお付き合いください。私自身もRubyは使っていませんが、調べてみると次の2点から他人事ではないと分かりました。

  • 同じ系統のワームが、npmとPyPIにも存在している
  • 汚染されたリポジトリを AIコーディングツールやVS Codeで開くだけ で、ワームが再実行される可能性がある

この記事では、何が起きたのかを簡単に整理したうえで、自分の環境に痕跡がないかを確認するスクリプトを紹介します。

何が起きたのか

問題のパッケージは strutil-core のバージョン1.0.1です。文字列操作の小さなライブラリを装っていますが、インストール時に約5.48MBの認証情報窃取ワームが実行されます。

経緯は次のとおりです。

  • 2026年11月1日、無害な1.0.0が公開され、その2時間以内に悪意ある1.0.1が公開された
  • Socketのスキャナーが公開から70秒後に検知した
  • 約5時間後、RubyGemsが両バージョンを削除した

このgemに依存する公開プロジェクトはなく、被害者も確認されていません。それでも注目されているのは、中身のワームが強力だからです。

このワームは「testmetest」と呼ばれるワームの新しいビルドで、公開されてしまった「Shai-Hulud」ツールキットから派生した「Miasma」ファミリーの一員とされています。

このワームがやること

1. インストールしただけで動く

gem install や bundle install を実行した時点でワームが動きます。アプリ側でライブラリを読み込む必要はありません。

2. 認証情報を根こそぎ集める

クラウド(AWS、Azure、Google Cloud)、CI、パッケージレジストリ、SSH、Docker、Kubernetes、暗号資産ウォレット、そしてAIコーディングツールの認証情報まで収集し、攻撃者のサーバー(paleasbest[.]top)へ送信します。

3. 自分で増える

盗んだ認証情報を使い、被害者が管理するパッケージにワームを混入して再公開する機能を持っています。対象はRubyGemsだけでなく、npmとPyPIも含まれます。

4. AIコーディングツールを汚染する

個人的に一番気になったのがこの点です。

  • AIツールのMCP設定に workspace-tools という名前のサーバーを追加し、ワーム本体を実行させる
  • 盗んだGit認証情報を使い、リポジトリに .claude/settings.json、.cursor/mcp.json、CLAUDE.md、AGENTS.md、.vscode/tasks.json などを書き込む

つまり、汚染されたリポジトリをクローンしてAIツールやVS Codeで開くと、ツールの信頼設定や承認設定によってはワームが再実行されます。パッケージマネージャーの種類は関係ありません。

5. トークンを失効させるとファイルを消す

盗んだGitHubトークンが有効かどうかを45秒ごとに確認し、無効になった(HTTP 401または403が返った)時点でホームディレクトリを削除する仕組みが入っています。

感染に気づいた人が最初にやるのは、たいていトークンの失効です。その「正しい初動」が削除の引き金になるという、非常に悪質な作りです。

RubyGemsを使っていなくても確認したほうがいい理由

  • PyPIに兄弟パッケージがある: pystr-codec のバージョン0.1.0〜0.1.2が、同じ送信先を使っていると報告されています
  • 元になったワームはnpm由来: testmetest自体がnpmのワームです
  • リポジトリ経由の経路がある: 上で書いたとおり、他人のリポジトリを開く人なら誰でも対象になり得ます

確認スクリプト(Ubuntu / WSL用)

以下をターミナルにそのまま貼り付けると、~/check.sh を作成して実行します。読み取り専用で、ファイルの変更や削除は一切行いません。

cat > ~/check.sh <<'EOF'
#!/usr/bin/env bash
# strutil-core / pystr-codec 系ワームの痕跡チェック v2(読み取りのみ・何も変更しません)
# 使い方: bash check.sh [追加で調べるディレクトリ ...]
hit=0
flag() { echo "[!] $1"; hit=1; }
dirs=("$HOME" "$@")

echo "== 1. 悪意あるパッケージ(ファイルシステム全体・少し時間がかかります) =="
# pip や gem がどれを向いているかに関係なく、実体を直接探す(/mnt = Windows ドライブは除外)
while IFS= read -r p; do flag "パッケージの痕跡: $p"; done \
  < <(find / \( -path /proc -o -path /sys -o -path /dev -o -path /run -o -path /mnt \) -prune -o \
        \( -iname 'pystr_codec*' -o -iname 'pystr-codec*' -o -iname 'strutil-core*' \) -print 2>/dev/null)

echo "== 2. 常駐化の痕跡 =="
for f in \
  "$HOME/.local/share/runtimes/sync.py" \
  "$HOME/.config/index.js" \
  "$HOME/.local/bin/net-health-checker.sh" \
  "$HOME/.config/net-health-checker" \
  /var/tmp/.fontconfig_state; do
  [ -e "$f" ] && flag "不審なファイル: $f"
done
ls "$HOME/.config/systemd/user" 2>/dev/null | grep -E 'pkg-runtime|net-health-checker' >/dev/null && flag "不審な systemd ユーザーサービスがあります"
systemctl --user list-units --all 2>/dev/null | grep -E 'pkg-runtime|net-health-checker' >/dev/null && flag "不審なサービスが登録されています"

echo "== 3. AIツール設定の汚染 (${dirs[*]}) =="
while IFS= read -r f; do flag "workspace-tools の記述: $f"; done \
  < <(grep -rIl --exclude-dir=node_modules --exclude-dir=.git \
        --include=settings.json --include=config.json --include=mcp.json \
        --include=.mcp.json --include=.claude.json --include=.aider.conf.yml \
        'workspace-tools' "${dirs[@]}" 2>/dev/null)

echo "== 4. リポジトリへの混入 (${dirs[*]}) =="
while IFS= read -r f; do flag "不審なファイル: $f"; done \
  < <(find "${dirs[@]}" -path '*/.github/init.js' -not -path '*/node_modules/*' 2>/dev/null)

echo
if [ "$hit" -eq 0 ]; then echo "OK: 痕跡は見つかりませんでした"; else echo "要確認: 上の [!] の項目を確認してください"; fi
EOF
bash ~/check.sh

何を調べているのか

項目 内容
1. 悪意あるパッケージ pystr-codec と strutil-core の実体がファイルシステム上にないか
2. 常駐化の痕跡 バックドア(sync.py)、ワーム本体のコピー(~/.config/index.js)、トークン監視(net-health-checker)、関連するsystemdサービスがないか
3. AIツール設定の汚染 設定ファイルに workspace-tools というMCPサーバーが書き込まれていないか
4. リポジトリへの混入 .github/init.js が紛れ込んでいないか

結果の見方

  • OK: 痕跡は見つかりませんでした と表示されれば、既知の痕跡はありません
  • [!] が表示された場合は、その項目を確認してください
  • 自分で workspace-tools という名前のMCPサーバーを設定している場合は、それも検出されます(誤検知)

使い方の補足

  • ホーム以外の場所も調べたい場合は、引数でディレクトリを追加できます(例:bash ~/check.sh /opt/app)
  • 一般ユーザーで実行すると、読み取り権限のないディレクトリは黙ってスキップされます。漏れなく調べたい場合は sudo bash ~/check.sh /home/ユーザー名 のように実行してください(sudo では $HOME が /root に変わることがあるため、自分のホームを引数で指定します)
  • Windowsドライブ(/mnt)は対象外です。Windows側は次のスクリプトで確認します

実際に試して気づいた落とし穴:pip list を信用しすぎない

最初に作ったスクリプトは、pip list の結果からパッケージの有無を判定していました。ところが実際にコンテナで試したところ、これでは見落としがあると分かりました。

  • pip コマンドが見ていたのは、pyenvのほぼ空の環境だった
  • アプリが実際に使っているのは /opt/venv で、そちらは調べられていなかった
  • ファイル検索も $HOME 配下に限定していたため、ホーム外の環境は対象外だった

pip list が教えてくれるのは「いまPATH上にあるpipが見ている環境」だけ です。venvやpyenvを複数使っていたり、Dockerコンテナ内でアプリ用の環境が別の場所にあったりすると、肝心の環境を素通りします。

そこで上記のv2では、pip に頼らず、ファイルシステム全体からパッケージの実体を直接探す方式に変更しました。コンテナ内でパッケージの有無だけを確認したい場合は、次の1行でも同じことができます。

find / \( -path /proc -o -path /sys \) -prune -o \( -iname 'pystr_codec*' -o -iname 'pystr-codec*' -o -iname 'strutil-core*' \) -print 2>/dev/null

何も表示されなければ、該当パッケージは入っていません。

確認スクリプト(Windows用)

PowerShellを開き、以下をそのまま貼り付けてください。こちらも読み取り専用です。

# strutil-core / pystr-codec 系ワームの痕跡チェック(読み取りのみ・何も変更しません)
$hit = $false
function Flag($m) { Write-Host "[!] $m" -ForegroundColor Red; $script:hit = $true }
$h = $env:USERPROFILE

Write-Host "== 1. 悪意あるパッケージ =="
foreach ($p in 'pip', 'pip3') {
  if (Get-Command $p -ErrorAction SilentlyContinue) {
    if (& $p list 2>$null | Select-String -Pattern '^pystr-codec' -Quiet) { Flag "pystr-codec がインストールされています ($p)" }
  }
}
if (Get-Command gem -ErrorAction SilentlyContinue) {
  if (& gem list 2>$null | Select-String -Pattern '^strutil-core' -Quiet) { Flag "strutil-core がインストールされています" }
}

Write-Host "== 2. ワーム本体・常駐化の痕跡 =="
foreach ($f in "$h\.config\index.js", "$h\.local\share\runtimes\sync.py", "$h\.config\net-health-checker") {
  if (Test-Path -LiteralPath $f) { Flag "不審なファイル: $f" }
}
Get-ScheduledTask -ErrorAction SilentlyContinue |
  Where-Object { $_.TaskName -match 'pkg-runtime|net-health-checker' } |
  ForEach-Object { Flag "不審なタスク: $($_.TaskName)" }

Write-Host "== 3. AIツール設定・リポジトリの確認(数分かかることがあります) =="
$cfg = 'settings.json', 'config.json', 'mcp.json', '.mcp.json', '.claude.json', '.aider.conf.yml'
$files = Get-ChildItem -Path $h -Recurse -Force -File -Include ($cfg + 'init.js' + 'node_win.napi.node') -ErrorAction SilentlyContinue |
  Where-Object { $_.FullName -notmatch '\\(node_modules|\.git)\\' }
foreach ($f in $files) {
  if ($f.Name -eq 'node_win.napi.node') {
    Flag "不審なファイル: $($f.FullName)"
  } elseif ($f.Name -eq 'init.js') {
    if ($f.Directory.Name -eq '.github') { Flag "不審なファイル: $($f.FullName)" }
  } elseif (Select-String -LiteralPath $f.FullName -Pattern 'workspace-tools' -Quiet -ErrorAction SilentlyContinue) {
    Flag "workspace-tools の記述: $($f.FullName)"
  }
}

Write-Host ""
if ($hit) { Write-Host "要確認: 上の [!] の項目を確認してください" -ForegroundColor Yellow }
else { Write-Host "OK: 痕跡は見つかりませんでした" -ForegroundColor Green }

Windows版については、次の点にご注意ください。

  • 元記事が詳しく説明している常駐化の仕組みはLinuxとmacOSのものです。Windowsについては、ワーム内にWindows用の部品(node_win.napi.node)が含まれているという記述にとどまります
  • そのためWindows版は「判明している痕跡がないことの確認」であり、完全な安全証明ではありません
  • Windows版のパッケージ確認は pip list ベースです。Windows側に複数のPython環境がある場合は、前の章で書いた落とし穴が当てはまります

もし痕跡が見つかったら

対応の順番が重要です。慌ててGitHubトークンを失効させないでください。

  1. まずそのマシンをネットワークから隔離する
  2. net-health-checker と pkg-runtime を停止・削除する(systemdのユーザーサービスやLaunchAgentも含む)
  3. そのあとでGitHubトークンを失効させる
  4. クリーンな別の端末から、すべての認証情報をローテーションする
  5. 感染したホストは再構築する

2より先に3を行うと、トークン監視がホームディレクトリを削除します。また、永続化の仕組みはパッケージの外に置かれるため、パッケージをアンインストールしても感染は解消しません。

日頃からできる対策

  • 見慣れないパッケージを安易にインストールしない。特に公開されたばかりのものは要注意
  • AIコーディングツールのMCP設定やフックに、身に覚えのないエントリがないか定期的に見る
  • 他人のリポジトリを開くときは、VS Codeのワークスペース信頼やAIツールの承認設定を安易に「すべて許可」にしない
  • リポジトリに AGENTS.md、CLAUDE.md、.vscode/tasks.json などの見覚えのない変更がないか、差分を確認する
  • 自分が公開しているパッケージに、覚えのないパッチリリースが出ていないか確認する

まとめ

  • strutil-core 自体は約5時間で削除され、被害者も確認されていない
  • ただしワーム本体は、認証情報の窃取、自己増殖、AIツールの汚染、条件付きのファイル削除までそろった強力なもの
  • 同系統はnpmとPyPIにも存在し、リポジトリ経由の経路もあるため、RubyGemsを使っていなくても確認する価値がある
  • 確認するときは pip list だけに頼らず、実体を直接探す
  • 痕跡が見つかったら、トークン失効より先に監視プロセスを止める

この記事のスクリプトは既知の痕跡を調べるものです。「OK」と表示されても絶対に安全という意味ではありませんが、最低限の確認として役立てば幸いです。

参考