QRDoor Code
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
.
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))
}
PWNME{E4Sy_P34sI_B4CkdO0R}