この記事は、私自身の体験を紹介するものです。記事の内容を試した結果や、それによって生じた損害について、私は責任を負いかねます。実施は、ご自身の責任と判断でお願いします。 コマンド実行については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トークンを失効させないでください。
- まずそのマシンをネットワークから隔離する
net-health-checkerとpkg-runtimeを停止・削除する(systemdのユーザーサービスやLaunchAgentも含む)- そのあとでGitHubトークンを失効させる
- クリーンな別の端末から、すべての認証情報をローテーションする
- 感染したホストは再構築する
2より先に3を行うと、トークン監視がホームディレクトリを削除します。また、永続化の仕組みはパッケージの外に置かれるため、パッケージをアンインストールしても感染は解消しません。
日頃からできる対策
- 見慣れないパッケージを安易にインストールしない。特に公開されたばかりのものは要注意
- AIコーディングツールのMCP設定やフックに、身に覚えのないエントリがないか定期的に見る
- 他人のリポジトリを開くときは、VS Codeのワークスペース信頼やAIツールの承認設定を安易に「すべて許可」にしない
- リポジトリに
AGENTS.md、CLAUDE.md、.vscode/tasks.jsonなどの見覚えのない変更がないか、差分を確認する - 自分が公開しているパッケージに、覚えのないパッチリリースが出ていないか確認する
まとめ
strutil-core自体は約5時間で削除され、被害者も確認されていない- ただしワーム本体は、認証情報の窃取、自己増殖、AIツールの汚染、条件付きのファイル削除までそろった強力なもの
- 同系統はnpmとPyPIにも存在し、リポジトリ経由の経路もあるため、RubyGemsを使っていなくても確認する価値がある
- 確認するときは
pip listだけに頼らず、実体を直接探す - 痕跡が見つかったら、トークン失効より先に監視プロセスを止める
この記事のスクリプトは既知の痕跡を調べるものです。「OK」と表示されても絶対に安全という意味ではありませんが、最低限の確認として役立てば幸いです。