siunam's Website

My personal website

Home Writeups Blog Projects About E-Portfolio

QRDoor Code

Table of Contents

  1. Overview
  2. Background
  3. Enumeration

Overview

Background

Author: Eteck#3426

A company needed a website, to generate QR Code. They asked for a freelance to do this job

Since the website is up, they’ve noticed weird behaviour on their server

They need you to audit their code and help them to resolve their problem

Flag is situed in /app/flag.txt

Enumeration

Home page:

In here, we can type something to generate a QR code:

Burp Suite HTTP history:

When the “Generate” button is clicked, it’ll send a POST request to /generate, with JSON data of our input.

If no error occurred, it’ll response us with a JSON data, in code key, it has the QR code image in base64 encoded, in data, it has our input.

We can scan the QR code via a Linux command zbarimg:

┌[siunam♥earth]-(~/ctf/PwnMe-2023-8-bits/Web/QRDoor-Code)-[2023.05.06|12:07:17(HKT)]
└> zbarimg qr.png
QR-Code:anything
scanned 1 barcode symbols from 1 images in 0.03 seconds

As excepted, it has our input’s data.

Now, let’s look at the source code!

┌[siunam♥earth]-(~/ctf/PwnMe-2023-8-bits/Web/QRDoor-Code)-[2023.05.06|12:08:26(HKT)]
└> file source.tar 
source.tar: POSIX tar archive
┌[siunam♥earth]-(~/ctf/PwnMe-2023-8-bits/Web/QRDoor-Code)-[2023.05.06|12:08:28(HKT)]
└> tar xf source.tar                                                                                 
┌[siunam♥earth]-(~/ctf/PwnMe-2023-8-bits/Web/QRDoor-Code)-[2023.05.06|12:08:37(HKT)]
└> ls -lah source             
total 32K
drwxr-xr-x 4 siunam nam 4.0K Feb  1 04:46 .
drwxr-xr-x 3 siunam nam 4.0K May  6 12:08 ..
-rwxr-xr-x 1 siunam nam  192 Feb  1 04:46 docker-compose.yml
-rwxr-xr-x 1 siunam nam  183 Feb  1 04:54 Dockerfile
-rwxr-xr-x 1 siunam nam   55 Jan 17 22:37 .dockerignore
-rwxr-xr-x 1 siunam nam  429 Jan 17 22:22 package.json
drwxr-xr-x 2 siunam nam 4.0K Feb  1 04:46 src
drwxr-xr-x 2 siunam nam 4.0K Feb  1 04:46 views

In src/index.js, we can view the logic behind this web application.

POST route /generate:

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' });
    }
});

In here, it first checks the our input’s length is greater 150.

If not, initialize the QRCode object instance with our input’s value as newQrCode.

Then, send the response JSON data with the code key’s value via newQrCode’s getImage() method and data key’s value of our input.

Let’s look at the QRCode class!

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:(')
    }
}

In the getImage() async method, it’ll invoke function execFortune() with the input’s length IF no this.value.

Function execFortune():

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

Right off the bat, we can see a sink (Dangerous function): exec().

The exec() is being imported from the child_process library:

const { exec } = require("child_process");

That being said, if we can somehow inject our input to the exec() function, we can get OS command injection!

Note: The fortune program is to generate a random funny line from a database of quotations, based on the input size.

┌[siunam♥earth]-(~/ctf/PwnMe-2023-8-bits/Web/QRDoor-Code/source)-[2023.05.06|12:30:38(HKT)]
└> fortune -n 1337 
Q:	What is purple and conquered the world?
A:	Alexander the Grape.

fortune man page:

FORTUNE(6)                                UNIX Reference Manual                               FORTUNE(6)

NAME
       fortune - print a random, hopefully interesting, adage
[...]
   Options
       The options are as follows:
[...]
       -n length
              Set the longest fortune length (in characters) considered to be ``short'' (the default  is
              160).  All fortunes longer than this are considered ``long''.  Be careful!  If you set the
              length too short and ask for short fortunes, or too long and ask for  long  ones,  fortune
              goes into a never-ending thrash loop.
[...]

However, I don’t think we can control the input’s length to an evil payload…

Then in the getImage() method, if this.value is not null, it’ll generate a QR code with our input’s data and convert it to data:image/png;base64,abcd... URL format.

After poking around, I still couldn’t find the vulnerability in this challenge…