npmの依存関係混同攻撃を現行犯で検知
Snyk Security Research Team
2022年4月30日
0 分で読めます攻撃の最新情報:タイムライン
2022年5月10日:レッドチーム向けセキュリティ企業のCodeWhiteがTwitterで当社に連絡し、悪意あるパッケージの作成者であることを明かしました。顧客向けの攻撃シミュレーションの一環だったとのことです。手の込んだ攻撃に敬意を表します。
2022年5月4日:DigitalOceanから、C2のIPアドレスはセキュリティ関連企業のもので、確認のうえ通知するとの返答がありました。
2022年5月1日:npmが悪意あるパッケージをレジストリから削除しました。
npmマルウェアの事例
近年、さまざまなエコシステムで悪意あるパッケージが増え続けています。一般に、そうしたパッケージの大半は無害で、情報を収集するだけで感染したマシンに被害を与えることはありません。しかし、ときには目的と手段を備え、本番環境で実行可能な真に悪意あるパッケージに遭遇します。これはその一例です。
npmで本物のマルウェアを発見
Snykのセキュリティリサーチチームは、悪意あるパッケージをプロアクティブに検出することに注力しています。主な目標は、エコシステムのレジストリにパッケージが公開されたら、できるだけ早くスキャンして警告することです。そのため、悪意あるパッケージを検出するためのさまざまな仕組みを備えた堅牢なシステムを構築しました。
悪意あるパッケージを捕捉する仕組みを設置して間もなく、多数のライブラリが悪意あるものとして報告されました。しかし、調査を進めると、その多くは「軽度に悪意のある」パッケージに関する報告でした。
ここでいう「軽度に悪意のある」とは、パッケージが次のいずれかを行うものです。
DNSルックアップを介したマシン関連情報の流出(それ以上の動作はなし)
暗号資産マイナー(問題はあるものの、悪意あるものとしてはさほど興味深くない)
他の研究者がテスト目的で作成した「軽度に悪意のある」パッケージ
このリストにあるものの、その他のバリエーション
最終的に、非常に興味深いパッケージgxm-reference-web-auth-serverを発見し、悪意あるものとして判定しました。少し調べたところ何かが違うと感じたため、VMを立ち上げてtarballを調査しました。
npmパッケージに関する報告だったため、最初に確認したのは`package.json`ファイルです。予想どおり、パッケージ内のJavaScriptファイルを実行するpost-installスクリプトが含まれていました。ご存じない方のために説明すると、post-installスクリプトは、npmが関連する依存パッケージのインストールを終えた後にスクリプトを実行する、ごく一般的な方法です。また、悪意ある攻撃者が被害者のマシン上でスクリプトを実行する簡単な手段でもあります。
呼び出されるファイルを調べると、難読化されていました。しかし、難読化とは、コードを「隠す」ための一見凝った方法にすぎず、実際には比較的簡単に元に戻せることが多いものです(少なくともJavaScriptの世界では)。
次にパッケージ内で目を引いたのは、暗号化されたファイルでした。勘が働き、詳しく調べることにしました。
マルウェアのリバースエンジニアリング P1:ラッパー
前述のとおり、パッケージには難読化されたファイルと暗号化されたファイルの2つが追加で含まれていました。
また、package.jsonにはpost-installスクリプトが含まれていました。
最初は呼び出されるスクリプトを調べていましたが、意味不明な名前の依存関係ldtzstxwzpntxqnとlznfjbhurpjsqmrにも気づきました。これらについては、この記事の後半で少し掘り下げます。
confsettingsaaa.jsファイルを調べると、次の内容が見つかりました。
このファイルを解読するため、難読化解除ツールを使って読みやすくしました。コードを読み進めると、このファイル(マルウェアの第1部であることから、以降P1と呼びます)が、偽のサブドメインをpkgio[.]comに照会することで、OS情報とパッケージ情報を流出させていることがわかりました。
このコードは情報を流出させるだけで、被害はないと思うかもしれません。しかし、こうした情報流出は攻撃者の偵察に役立つため、標的とその環境を特定できます。攻撃者がDNSルックアップを悪用するのは、通常ファイアウォールやネットワークフィルターを通過できるためです。これにより攻撃者は自身のDNSサーバーに問い合わせを送り、リクエストを記録できます。
さらに読み進めると、興味深い点が見つかりました。まず、例外を捕捉した場合、すべてのtry/catchブロックが<code>fail</code>関数を呼び出します。次に、スクリプトが依存関係の1つlznfjbhurpjsqmrを使っていることがわかりました。
(2つの)依存関係の説明に入る前に、まずfail関数(またはその仕組み)を見てみましょう。この関数は呼び出されると、主に2つの処理を行います。まず、自身の痕跡を消します。「fail」関数はパッケージに関連するすべてのファイルを削除し、後始末をします。
マルウェアにとって興味深い仕組みですが、次の動作には警戒が必要でした。
後始末の後、この関数はpackage.jsonとindex.jsの偽のファイルを作成し、コンソールに次のメッセージを出力します。「公開リポジトリではなく、プライベートレジストリを参照してください。セキュリティチーム」
これを読んで、いくつか疑問が浮かびました。なぜ悪意あるパッケージは完全に自分自身を削除しないのでしょうか。なぜセキュリティチームが用意した正規のプレースホルダーを装うのでしょうか。このパッケージはどの組織を標的にしているのでしょうか。
これらの疑問の多くには答えられましたが、標的を特定して警告するために最善を尽くしたにもかかわらず、現時点ではどの組織が狙われているのか特定できていません。これらの疑問を念頭に、先へ進みましょう。
依存関係:ldtzstxwzpntxqnとlznfjbhurpjsqmr
前述のとおり、このパッケージは2つの依存関係ldtzstxwzpntxqnとlznfjbhurpjsqmrをインストールします。npmのページを確認すると、どちらも同じメンテナーが公開していました。そのメンテナーページは次のようになっていました。

これは興味深い発見で、パッケージに関するいくつかの疑いが裏付けられました。ただし、依存関係そのものは、正規のパッケージをコピーしただけのものでした。
パッケージ名 | 元のパッケージ | 用途 |
|---|---|---|
|
| npm installの簡易APIを提供するパッケージ(プログラムからパッケージをインストール)。 |
|
| グローバルのnpmをローカルのnodeモジュールとして読み込む。 |
後に残りのコードを監査して確認したとおり、これらのパッケージは元のパッケージと同じ目的で使われていました。なぜ攻撃者は元のパッケージを使わず、わざわざこれらのパッケージを作成したのでしょうか。わかりませんし、おそらく永遠にわからないでしょう。
被害者の選別、プライベートレジストリ、そしてP2へ
補足事項…
以下では「コードが中断する」と書いた場合、上記の
fail関数が呼び出され、後始末が行われたことを意味します。標的となる組織を特定できていないため、ここでは
ORGと呼びます。
P1の次の処理では、.npmrcファイルからプライベートレジストリの認証設定を取得します(ここでlznfjbhurpjsqmrが使われます)。設定またはファイルが見つからない場合(コードはマシン全体を検索します)、処理を中断します。
認証情報が見つかった場合、設定ファイルで指定されたプライベートレジストリから、名前が同じパッケージをいくつかのバリエーションを試しながらダウンロードしようとします。
プライベートレジストリでパッケージが見つからない、またはダウンロードに失敗した場合、コードは処理を中断します。プライベートレジストリからtarballをダウンロードできた場合は、次の処理を行います。
1. npm installでモジュールを.documentationというサブディレクトリにインストールします(インストールにはldtzstxwzpntxqn依存関係を使用)。失敗した場合は処理を中断します。
2. パッケージの現在の内容を、新たにインストールしたパッケージの内容に置き換えます。失敗した場合は処理を中断します。
3. ネットワーク関連ファイル2つと新しいpackage.jsonの内容を、POSTリクエストで流出させます。
4. 暗号化されたファイルを復号し、新たにデタッチされたプロセスで実行します(暗号化アルゴリズム、キー、IVはハードコードされています。このファイルについては次のセクションで詳しく説明します)。
5. 関連ファイルをすべて削除します(この手順では暗号化されたファイルとP1のコードのみを削除します)。
6. 終了します。
以上から、プライベートレジストリにgxm-reference-web-auth-serverパッケージが存在しない場合、またはアクセスできない場合、マルウェアはその標的を単にスキップするとわかります。
つまり、次のことが示唆されます。
このパッケージは特定の組織を標的にしている。
このパッケージの背後にいる攻撃者は、その組織のプライベートレジストリにこのパッケージが存在することを知っている。
これでP1の処理はすべて終わり、「マルウェアのラッパー」は終了します。
マルウェアのリバースエンジニアリング P2:エージェント
ラッパーにはファイルを復号するための情報がハードコードされていました(攻撃者のミスだと考えられます)。そのため、こちらも復号できました。実際に復号しました。
ラッパーと同様に、このファイルも難読化されており、意味不明な文字列が1行だけ記述されていました。しかし今回は、難読化解除ツールでも解除が難しく、謎めいた断片が残りました。
ほかの難読化解除ツールもいくつか試しましたが、よりよい結果は得られませんでした。最初の結果を使い、手作業で難読化を解除することにしました。少し苦労しましたが、最終的にファイルを人が読める形にできました。エージェントの内部動作を見ていきましょう。
エージェントの登録
エージェントが最初に指示されるのは、コマンド&コントロールサーバー(CNCまたはC2と呼びます)への登録です。登録すると、エージェントは後続の通信に使う重要な3つの文字列、つまりペイロード暗号化用のキーとIV、そしてUUIDを受け取ります。
このリクエストが完了すると、それ以降のC2サーバーとエージェント間の通信は、これらのキーとIVを使って暗号化・復号されます(アルゴリズムはハードコードされています)。このリクエストの直後、エージェントは自身の環境情報を含むPOSTリクエストをサーバーに送信します。
このリクエストを送信すると、エージェントは次の処理に移ります。
実行ループ
エージェントの実行ループは非常にシンプルです。いくつかのコマンドに対応するif-else文があり、C2サーバーの指示に従って実行します。たとえばdeleteコマンドを受け取ると自身を削除し、C2からコード片が送られるとそれを評価します。
エージェントが反応するコマンドをまとめると、次のとおりです。
一覧のとおり、execまたはevalを使うとリバースシェルを実行でき、攻撃者は感染したマシンを完全に制御できます。また、registerオプションを再度呼び出すと、エージェントとC2サーバー間で暗号化情報を交換し、新しい情報を生成できます。必要に応じて情報を変更するための機能です。
この機能は独自性や高度さを感じさせるかもしれませんが、C2エージェントの世界では、被害者のマシンに仕込むエージェントの標準的な機能です。それでも、このエージェントのソースコードを既知のC2フレームワークと関連付けることはできませんでした。
マルウェアのまとめ
マルウェアの全容がわかったところで、次のように結論づけられます。
このマルウェアは、まだ特定されていない1社だけを標的にしています。リバースエンジニアリングで得た情報から、その企業のプライベートレジストリには「gxm-reference-web-auth-server」パッケージが存在すると考えられます。
このパッケージは被害者のプライベートレジストリ内で自分自身を探しているため、これは依存関係混同攻撃、つまりサプライチェーン攻撃の一種に分類できます。
攻撃者は、その企業のプライベートレジストリに該当するパッケージが存在することを知っていた可能性が高いと考えられます。
この時点でパッケージの動作は明らかになっていましたが、C2サーバーが応答するか、そしてこれが進行中のキャンペーンかどうかを確かめることにしました。なりすましを試すときです。
攻撃者と「Among Us」で駆け引き
次の実験のアイデアはシンプルでした。通信の向こう側に誰かいるのか、いるとすれば活動中なのかを確かめたいと考えました。そのために、次のことを行う必要がありました。
ラッパーを使わずにエージェントを実行する(P1では.npmrcの認証情報がないためクライアントがフィルタリングされるなどの問題がある)
すべてのHTTP/Sトラフィックを傍受して記録する(C2サーバーが何を送受信するかを確認するため)
記録したデータを、一方向かつ不可逆で、追跡できない方法で私たちに転送する(攻撃者もマシンにアクセスできるため、これは重要です)
ただし、これらに着手する前に、まずサーバー自体について情報を集めることにしました。
向こうに誰かいるのか?
標準的なWHOISとNmapのスキャンを使って、サーバーの情報を収集しました。WHOISの結果から、サーバーはDigitalOceanでホストされていることが分かりました(サーバーのIPアドレスはDigitalOceanに報告しました)。また、Nmapのスキャンでは次のことが分かりました。
サーバーのセキュリティは十分ではなく、稼働していて接続を待ち受けています。
偽装者
計画に戻ると、次の準備が必要でした。
エージェントコード:復号の段階で入手しており、ラッパーとは別に実行できます(P1)。
インターセプター:エージェント自体がNodeJSのHTTP/Sモジュールを使用していたため、NodeJS環境で実装する必要がありました。
ログ記録パイプライン:HTTP/Sリクエストを非常に細かく処理でき、値の保存やSlackとの連携など、さまざまなインテグレーションもすでに用意されている Pipedreamを使うことにしました。
傍受には@gr2m/http-recorderライブラリを使いました。HTTP/Sリクエストを完全に取得し、操作するという目的にぴったりでした。
手順1と2を組み合わせると、次のスニペットができました。
あとは、データを私たちに送るだけです。Pipedreamを使い、send関数からペイロードをエンドポイントに送信し、処理できるようにしました。
データ処理では、メッセージを復号できるようにIVとキーを保存しました。また、Pipedreamのパイプラインを匿名のSlackワークスペースに接続し、メッセージを記録するようにしました。これにより、エージェントとC2サーバーがメッセージをやり取りするたびに、復号されて読みやすくなったすべての情報がSlackに通知されました。以下は、感染したマシンの構成図です。

こちらはSlackに出力された内容の例です。

こうして、私たちの擬似ハニーポットが完成しました。
もしもし、こちらは私です
偽のエージェントが稼働し、C2サーバーとの通信を開始してから間もなく、コマンドが届き始めました。最初のコマンドはlsで、その後、攻撃者は表示されているすべてのファイルに対してcatを試み、システム内を探索しようとしました。以下は、受信したコマンドの例です(base64からデコードしたもの)。

この時点で、相手側に誰かいるかどうかを確認するという目的は達成できたため、実験を終了することにしました。
これは、元の非公開「gxm-reference-web-auth-server」の所有者を狙った攻撃キャンペーンが進行中であることを示しています。
しかし、まだ続きがあります
実験を終える直前に、興味深い点に気づきました。C2のアドレス以外にハードコードされた値はほとんどありませんでしたが、その1つが、最初の/register呼び出しに含まれる「engine」プロパティの値でした。
もう検出を回避する必要がなくなったため、ほかの種類のターゲットがあるか確かめる目的で、その値をファジングすることにしました。しばらくすると、ほかのエンジンタイプが見つかりました。
これは、少なくともブラウザーエージェントとGolangエージェントが存在することを示しており、ほかにもあると考えても不思議ではありません。
情報開示と今後の対応
このマルウェアの内部動作は分かっていますが、どの組織や企業が標的なのかは特定できません。そのため、この場を使い(Twitterなども併用して)、コミュニティの皆さんにこのパッケージへの注意を呼びかけ、また注意を促していただきたいと考えています。この件についてご質問やご懸念があれば、お気軽にお問い合わせいただくか、@snyksecにDMをお送りください。
また、DigitalOceanにはC2サーバーをサービスから削除するよう、npmには悪意のあるパッケージについて知らせ、削除を依頼しました。
以上で、今回の調査は終了です。攻撃は複雑かつ巧妙ですが、今回の攻撃の最初の侵入経路は、簡単に緩和できる単純なパッケージ依存関係の混同だったことを指摘しておきたいと思います。この攻撃のワークフロー全体を確認するには、以下の画像をご覧ください。安全に、そして安心してお過ごしください。

オープンソースの依存関係を安全に
開発者を第一に考えたSnykのツールなら、脆弱なオープンソースの依存関係やその推移的依存関係を、ワンクリックで修正するプルリクエストを作成できます。



