Vulnerabilite_RCE

Lors des tests d’intrusion que nous menons, nous sommes régulièrement confrontés à des vulnérabilités qui permettent d’exécuter des commandes sur un système. Celles-ci peuvent prendre différentes formes en fonction du type d’application et des fonctionnalités impactées. Vous trouverez dans cet article un exemple de vulnérabilité RCE rencontrée lors d’un audit d’une application web codée en PHP.

Qu’est-ce qu’une RCE (Remote Code Execution) ?

En sécurité informatique, l’exécution de code arbitraire (ACE) est la capacité d’un attaquant à exécuter n’importe quelle commande ou n’importe quel code de son choix sur une machine cible ou dans un processus cible. Un programme conçu pour exploiter une telle vulnérabilité est appelé un exploit d’exécution de code arbitraire. La possibilité de déclencher une exécution de code arbitraire sur un réseau (en particulier via un réseau étendu tel qu’Internet) est souvent appelée exécution de code à distance, ou RCE pour Remote Code Execution.

Une RCE est particulièrement dangereuse, car elle permet souvent d’obtenir un accès privilégié à un système. Par exemple, une vulnérabilité RCE sur une application web permettra souvent d’exécuter des commandes sur le serveur qui l’héberge et donc de s’y introduire. Cela donnera donc accès à l’attaquant à tout ou partie des fichiers du serveur.

Présentation de la vulnérabilité

Le but de la fonctionnalité testée était de permettre à l’utilisateur d’uploader des fichiers sur une plateforme pour qu’ils puissent être réutilisés ailleurs. Lorsque le fichier uploadé est un fichier audio ou vidéo, l’application PHP va lancer une commande sur le serveur afin de récupérer la durée du fichier et ainsi de pouvoir la communiquer à l’utilisateur.

Voici le code (simplifié et modifié pour l’exemple) de l’application :

<?php
$message = "";
function upload($file): string
{
    $base_dir = __DIR__ . "/../upload/";
    $ext = strtolower(pathinfo("/" . $file["name"], PATHINFO_EXTENSION));
    $filepath = $base_dir . uniqid() . '.' . $ext;
    $filetype = mime_content_type($file["tmp_name"]);

    move_uploaded_file($file["tmp_name"], $filepath);

    if (str_starts_with($filetype, "video/") || str_starts_with($filetype, "audio/")) {
        $command = sprintf("ffprobe -i \"%s\" -show_entries format=duration -v quiet -of csv=\"p=0\"", $filepath);
        $duration = shell_exec($command);
        return "Media file of duration " . $duration . " uploaded.";
    } else {
        return "File uploaded";
    }
}

if ($_SERVER["REQUEST_METHOD"] === "POST") {
    $file = $_FILES["formFile"] ?? null;
    $message = $file === null ? "Missing file." : upload($file);
}
?>

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Upload media file</title>
    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.2/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-EVSTQN3/azprG1Anm3QDgpJLIm9Nao0Yz1ztcQTwFspd3yD65VohhpuuCOmLASjC" crossorigin="anonymous">
</head>
<body>

<div class="container">
    <header>
        <h1>Upload media file</h1>
        <hr>
    </header>
    <div class="row justify-content-md-center">
        <div class="col col-lg-6">
            <form action="" method="post" enctype="multipart/form-data">
                <div class="mb-3">
                    <label for="formFile" class="form-label">File to upload</label>
                    <input class="form-control" type="file" name="formFile" id="formFile">
                </div>
                <div class="col-auto">
                    <button type="submit" class="btn btn-primary mb-3">Upload</button>
                </div>

                <?php if (!empty($message)) { ?>
                    <div class="alert alert-primary" role="alert">
                        <?= htmlentities($message, ENT_QUOTES) ?>
                    </div>
                <?php } ?>

            </form>
        </div>
    </div>
</div>

</body>
</html>

Ce que nous pouvons constater dans un premier temps, c’est qu’une commande est construite ligne 13 grâce à la fonction PHP « sprintf » avec comme paramètre le chemin du fichier précédemment uploadé. Cette commande est ensuite exécutée ligne 14 et son résultat est renvoyé à l’utilisateur pour être affiché sur la page.

File_1

Le chemin du fichier est construit ligne 7 à partir des éléments suivants :

  • La variable $basedir : sa valeur est en dur dans le code, donc non manipulable
  • La sortie de la fonction PHP uniqid : sa valeur n’est pas manipulable
  • La sortie de la PHP pathinfo à laquelle on demande de renvoyer l’extension du fichier uploadé : c’est là que se situe la vulnérabilité

En effet, la fonction pathinfo, lorsqu’on lui demande l’extension du fichier (avec le flag PATHINFO_EXTENSION), va simplement renvoyer tout ce qui se trouve après le dernier point du chemin qui lui est passé) Il est donc possible d’injecter du code malveillant dans l’extension du fichier, de sorte que celui-ci soit insérer dans la commande construite ligne 13.

Cependant, compte tenu du fonctionnement de la fonction PHP « pathinfo », notre commande ne doit pas comporter de point ni de slash. Il faut donc ruser pour pouvoir exploiter cette vulnérabilité.

Exploitation de la vulnérabilité RCE

Pour exploiter cette vulnérabilité, nous allons commencer par tenter d’injecter une simple commande dans le nom du fichier. Nous allons ajouter un \ » pour sortir des doubles quotes dans lesquelles notre commande se trouve, puis nous allons ajouter un point-virgule ( ;), puis notre commande, et nous allons ajouter un autre point-virgule et un # pour commenter le reste de la ligne et faire en sorte qu’elle ne nous gêne pas.

Notre payload est donc la suivante :

\";id;#

Nous allons rejouer la requête, en injectant cette payload dans l’extension du fichier. Le nom complet du fichier est donc : 02.mp3\";id;#

Cliquer pour agrandir

Nous constatons que la sortie de la commande « id » nous est renvoyée. Nous allons ensuite tenter de lire le fichier « /etc/passwd ». Or, comme mentionné plus haut, à cause du fonctionnement de la fonction qui récupère l’extension, nous allons devoir ruser pour pouvoir insérer des commandes contenant des points et de slashs. Il y a de nombreuses manières de s’y prendre (par exemple en encodant les caractères). Comme notre application est en PHP, l’exécutable php et très certainement présent sur le serveur. Nous allons donc l’utiliser pour exécuter du code PHP en ligne de commande.

Nous allons créer des variables qui contiennent les caractères « / » et « . » générés à partir de leur charcode respectifs, puis nous allons les utiliser pour construire nos commandes. Le code PHP sera le suivant :

$sl=chr(47); // Code du caractère /
$dot=chr(46); // Code du caractère .
echo shell_exec(\"cat ${sl}etc${sl}passwd\"); // Lancement et récupération avec echo de la commande système en utilisant les variables précédentes en remplacement des caractères / et .

La payload complète sera donc la suivante :

02.mp3\";php -r '$sl=chr(47);$dot=chr(46);echo shell_exec(\"cat ${sl}etc${sl}passwd\");';#

Voici un exemple complet de commande permettant de lire le fichier « /etc/passwd » :

Burp_2
Cliquer pour agrandir

Voici un autre exemple permettant de récupérer un fichier contenant un « . » dans son chemin.

La payload est la suivante :

02.mp3\";php -r '$sl=chr(47);$dot=chr(46);echo shell_exec(\"cat ${sl}etc${sl}resolv${dot}conf\");';#
Cliquer pour agrandir

À partir de là, nous pouvons exécuter n’importe quelle commande sur le serveur. Celles-ci seront exécutées avec les droits de l’utilisateur faisant tourner le service Web (par exemple www-data), et nous ne sont la plupart du temps pas root, mais les droits sont souvent suffisants pour compromettre le système et les données qu’il héberge.

Comment corriger cette RCE ?

Pour corriger ce problème, la première recommandation serait évidemment de ne jamais utiliser de données manipulables par les utilisateurs dans des commandes passées au système.

Cependant, si cela s’avère nécessaire, il est important de bien s’assurer que les données qui sont utilisées sont propres et correctement sécurisées. Par exemple, dans notre cas, nous pourrions simplement mettre en place une liste blanche d’extension autorisée et ne pas lancer la commande si celle-ci n’est pas dans la liste :

$allowedExtensions = ["mp3", "mpeg"];
if (!in_array($ext, $allowedExtensions)) {
    return "File not allowed";
}

Il est également possible, par exemple, d’utiliser une regex dans la fonction « filter_var » PHP pour n’autoriser que les lettres et les chiffres :

if (!filter_var($ext, FILTER_VALIDATE_REGEXP, ["options"=>array("regexp"=>"/^\w+$/")])) {
    return "Invalid extension";
}

Conclusion

De manière générale sur une application qu’on développe, il est important de bien comprendre d’où proviennent les données que l’on manipule et surtout quelles vont être les sorties des fonctions que nous utilisons. En effet, dans cet exemple, la fonction PHP « pathinfo » est assez basique et n’est pas faite pour s’assurer que ce qu’elle renvoie est réellement tel que ce à quoi on pourrait s’attendre pour une extension (uniquement des lettres et des chiffres, assez courte…).

Il est également important de se demander si, lorsqu’on utilise une donnée dans une fonctionnalité, celle-ci peut poser un problème de sécurité, particulièrement lorsque ces données sont renvoyées à l’utilisateur (par exemple pour des XSS), ou lorsqu’elles sont utilisées, par exemple, dans des commandes systèmes (RCE) ou SQL (injections SQL). Vous pouvez consulter notre article sur les bonnes pratiques pour PHP par rapport aux injections et XSS. Il est donc important de filtrer autant que possible ces données et/ou de les encoder de sorte que du contenu malveillant ne puisse pas être interprété.

Auteur : Romain Garcia