Challenge Overview

For this challenge, the source code was provided. The web application uses Javascript technology with NodeJS and the EJS templating system (https://ejs.co/).

Let’s start by analyzing the code to find the vulnerability that will allow us to retrieve the flag.

We observe 2 routes :

  • /
  • /generate
app.get('/', async (req, res) => {
    res.render('index');
});

app.post('/generate', async (req, res) => {
    const { value } = req.body;
    try {
        let newQrCode;
        // If the length is too long, we use a default according to the length
        if (value.length > 150)
            newQrCode = new QRCode(null, value.lenght)
        else {
            newQrCode = new QRCode(String(value))
        }
        
        const code = await newQrCode.getImage()
        res.json({ code, data: newQrCode.value });
    } catch (error) {
        res.status(422).json({ message: "error", reason: 'Unknow error' });
    }
});

We will focus on the /generate route.

The function will retrieve the value of the parameter value from the request body, then check if the length of the value is greater than 150 characters. In this case, the generated QRCode will have the value: "Error while getting a funny line". Otherwise, a QRCode will be generated with our value parameter.

class QRCode {
    constructor(value, defaultLength){
        this.value = value
        this.defaultLength = defaultLength
    }

    async getImage(){
        if(!this.value){
            // Use 'fortune' to generate a random funny line, based on the input size
            try {
                this.value = await execFortune(this.defaultLength)
            } catch (error) {
                this.value = 'Error while getting a funny line'
            }
        }
        return await qrcode.toDataURL(this.value).catch(err => 'error:(')
    }
}

Moreover, we observe an execFortune() function that uses the exec function. The exec function allows executing system commands.

const { exec } = require("child_process");
function execFortune(defaultLength) {
    return new Promise((resolve, reject) => {
        exec(`fortune -n ${defaultLength}`, (error, stdout, stderr) => {
            if (error) {
                reject(error);
            }
            resolve(stdout? stdout : stderr);
        });
    });
}

We note that if we have control of the defaultLength variable, then a command injection is possible via the different bash syntax :

  • $()
  • ;
  • |
  • &&
  • ${}
  • ...

This variable is passed to the execFortune() function in case the requested value size is greater than 150 but we have no control over it…

Let’s take a closer look at the next line :

const { value } = req.body;

This line will retrieve the entire body and store it in the value variable without verification. We can manipulate this to create an array of values and have control over value.length.

Object

We managed to pass an object and define a new value for the variable value.length !

There is still one problem to solve. The server checks that the value of length is greater than 150 before creating the QRCode object.

Can we have a payload that both has a value greater than 150 and contains an injection command ? The answer is no, this type of payload does not work because the comparison blocks it.

  • 160;id
  • 160|ls

And then when we open our eyes, we realize that the developer made a typo in the name of the variable that is sent to the QRCode object. He didn’t call it length but lenght!!!

It’s not the same variable that’s being sent, so we can add a second one with the correct name and the value for our injection.

if (value.length > 150)
    newQrCode = new QRCode(null, value.lenght)
else {
    newQrCode = new QRCode(String(value))
}

Command

  • PWNME{E4Sy_P34sI_B4CkdO0R}