
セキュア・バイ・デフォルトのデモラボ。コンテナの堅牢化(distrolessイメージ、非root、読み取り専用ファイルシステム、実行時注入シークレット)により、重大なNext.js/React Server Actions RCE(CVE-2025-55182 “React2Shell”)を無力化できることを、安全なデプロイと安全でないデプロイの対比とエクスプロイトログ付きで示します。
このプロジェクトは、Next.js アプリケーション(特に Server Actions 経由)における重大なリモートコード実行(RCE)脆弱性と、インフラストラクチャの堅牢化が、コードの脆弱性が残ったままであっても攻撃を効果的に無効化する方法を示しています。
標準的な「安全でない」デプロイと、「Distroless」イメージと「読み取り専用ファイルシステム」を使用した堅牢化された「安全な」デプロイを対比させています。
ソフトウェアの脆弱性は避けられません。コードが失敗したとき、攻撃者の足場を広げるのを防ぐのはインフラストラクチャの役目です。
Next.js が使用する React Server Components (RSC) 実装には、重大な RCE(CVE-2025-55182、別名 React2Shell) が存在します。
spawnSync など)が可能になります。curl、wget、ls、cat)を使用して秘密情報を盗んだり、マルウェアをダウンロードしたりします。
child_process.spawnSync() を使用します。これはシェル(/bin/sh)を必要とせず、バイナリを直接実行します。chmod +x)に設定して実行します。以下のログは、アプリケーションの視点から見た攻撃試行の様子を示しています。この対比は、セキュリティ対策の有効性を明確に浮き彫りにします。
logs/server.safe.log)ログには繰り返し発生する失敗 (ENOENT) が記録されています。
spawnSync が ls、id、curl の実行を試みます。Distroless イメージにはこれらのバイナリが存在しません。シェルが無いというだけでなく、ツール自体が無いのです。[Instrumentation] Logging initialized. Writing to: /app/logs/server.safe.log
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"1. Write Initial Chunk to /tmp/hello_test","success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"2. Verify Binary was Written","verification":{...},"success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"4. Execute Binary","stdout":"","stderr":"","error_obj":{"message":"spawnSync /tmp/hello_test EACCES","code":"EACCES"},"success":true}`'
logs/server.unsafe.log)ログは、コマンド実行とファイルシステム操作の成功を裏付けています。
[Instrumentation] Logging initialized. Writing to: /app/logs/server.unsafe.log
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"1. Write Initial Chunk to /tmp/hello_test","success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"step":"4. Execute Binary","stdout":"Hello from Go binary!\\n","stderr":"","error_obj":null,"success":true}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"command":"id","args":[],"stdout":"uid=0(root) gid=0(root) ...","stderr":"","status":0,"signal":null}`'
⨯ Error: NEXT_REDIRECT
... digest: '`{"command":"cat","args":["/app/.env"],"stdout":"","stderr":"cat: can\'t open \'/app/.env\': No such file or directory\n","status":1,"signal":null}`'
(注: 安全でないログでは、上記の cat /app/.env は失敗しています。理由はファイルがルート直下に .env という名前で存在するためです。ただし、完全なログの ls -la でディレクトリ構造が明らかになるでしょう。)
標準的なシェルコマンドの実行を試みます。
id、ls、cat .env を実行し、機密データにアクセスできます。spawnSync /bin/sh ENOENT。コマンドを実行するためのシェルが存在しません。「ツール不足」を回避するため、カスタムバイナリをアップロードしようとします。
/tmp/malware に書き込みます。chmod +x を実行します。EROFS: read-only file system。攻撃者はバイナリを変数に読み込み、メモリから直接実行できるでしょうか?
global.payload = "...")に連結し、それを実行します。child_process 関数 (spawn、exec) はファイルパスを必要とします。バッファや文字列を直接実行することはできません。memfd_create(RAM 内に無名ファイルを作成する syscall)が必要です。memfd_create をネイティブに公開していません。これにアクセスするには、C++ アドオン(ffi-napi など)が node_modules に事前インストールされている必要があります。gcc、make)が含まれないため、攻撃者はこのアドオンをその場でビルドできません。「Distroless」イメージには、アプリケーションとその実行時依存関係のみが含まれます。パッケージマネージャー、シェル、標準 UNIX ツールは含まれません。
ls)したり、ファイルをダウンロード(curl)したり、権限を昇格させたりできないようにするためです。コンテナランタイムを設定して、ルートファイルシステムを読み取り専用でマウントします。
docker-compose.yml 内:
read_only: true
tmpfs:
- /tmp:noexec # CRITICAL: explicitly block execution!
/tmp へのバイナリ書き込みはできる(書き込みは成功)ものの、noexec フラグが原因で実行が EACCES(許可拒否)で失敗します。これにより、機能性(書き込み可能な tmp)とセキュリティのバランスが取れます。コンテナイメージに .env ファイルを同梱しないでください。攻撃者がファイルを読める場合(例: cat .env)、シークレットが危険にさらされます。
environment キー)。環境を起動:
安全なアプリと安全でないアプリの両方が単一の docker-compose.yml ファイルで定義されています。
docker compose up --build -d
エクスプロイトを実行: 特定のポートに対してエクスプロイトを実行すると、その違いを確認できます。
安全でないアプリをターゲット(ポート 3001):
# 1. 標準 RCE (LotL) - 成功
python exploit/poc.py http://localhost:3001
# 2. 高度な攻撃 (BYOL) - 成功
python exploit/poc_advanced.py http://localhost:3001
安全なアプリをターゲット(ポート 3000):
# 1. 標準 RCE (LotL) - 失敗 (ENOENT)
python exploit/poc.py http://localhost:3000
# 2. 高度な攻撃 (BYOL) - 失敗 (EACCES/EROFS)
python exploit/poc_advanced.py http://localhost:3000
クリーンアップ:
docker compose down
| 機能 | ❌ 安全でない環境 (ポート 3001) | ✅ 安全な環境 (ポート 3000) |
|---|
| ベースイメージ | node:20-alpine (ls、curl、wget などを含む) | gcr.io/distroless/nodejs20-debian12 (シェルなし、ツールなし) |
| ファイルシステム | 書き込み可能 (標準の Docker デフォルト) | 読み取り専用 (read_only: true) |
| シークレット | ディスク上の .env ファイル (cat .env に対して脆弱) | 環境変数 (実行時に注入) |
| ユーザー | root (デフォルト) | 非 root (Distroless により強制) |