Basics

Les formulaires d’upload sont souvent sujet à d’énormes failles de sécurité. En effet, ces formulaires permettent aux utilisateurs d’uploader un fichier (généralement une image) directement sur le serveur cible. Or si le formulaire n’est pas sécurisé alors rien n’empêchera un utilisateur malveillant d’upload un fichier PHP par exemple. Dans cet article nous verrons donc comment exploiter cette faille et surtout comment sécuriser nos formulaires.

I/ Exploitation

Bien, avant tout il va nous falloir des fichiers de test que je vous invite à télécharger ici. Pour créer un formulaire d’upload en HTML il faut utiliser la balise input suivie de l’attribut :

<!DOCTYPE html>
<html>
  <body>
    <h2>Upload your profile pic mate!</h2>
    <form method="post" action="traitemnt.php" enctype="multipart/form-data">
      <input tupe="hidden" name="MAX_FILE_SIZE" value="10000000"/>
      <input type="file" name="file"/>
      <input type="submit" value="Envoi"/>
    </form>
  </body>
</html>

Et voici le résultat visuel :

upload1.png

Dans ce formulaire on peut voir la présence de l'attribut enctype. Cet attribut définit comment les données du formulaire doivent être encodées lorsqu’elles sont envoyées au serveur. Il existe trois types d’encodage :

application/x-www-form-urlencoded est la valeur par défaut. Tous les caractères sont encodés, les espaces sont remplacés par des « + » et les caractères spéciaux sont convertis en valeur ASCII HEX (par exemple le caractère ‘ est encodé en %27).
multipart/form-data est utilisé dans le cas ou on utilise un formulaire d’upload (c’est pour ça que je l’ai utilisé dans notre code HTML). Aucun caractère n’est encodé.
text/plain : aucun caractère n’est encodé et les espaces sont remplacés par des « + ».

Nous avons aussi un champ caché que l’on a nommé « MAX_FILE_SIZE » et dont la valeur vaut 1000000. Ce champ indique que le fichier à uploader ne doit pas peser plus de 10000000 octets.

Et voici le contenu du fichier traitement.php:

Et le code final qui permet de traiter le fichier uploadé:

<?php
echo "Tentative d'upload ud fichier : ".$_FILES["file"]["name"]."<br>";
echo "Fichier de type : ".$_FILES["file"]["type"]."<br>";
echo "Erreurs : ".$_FILES["file"]["error"]."<br>";

# Liste des types mime que nous acceptons
$type_mime = array("image/jpg", "image/jpeg", "image/gif", "image/png");
# On récupère le type du fichier uploadé
$type_mime_file = $_FILES["file"]["type"];
# Si le type du fichier uploadé est dans la liste des mime acceptés
if (in_array($type_mime_file, $type_mime)){
  echo "Extension correcte, déplacement du fichier dans le répertoire upload.";
  move_uploaded_file($_FILES["file"]["tmp__name"], "upload/".$_FILES["file"]["name"]);
}
else {
  echo "Extension incorrecte, upload échoué";
}

Faisons quelques tests. Si j’upload un fichier .png :

upload6.png

Ça passe. Si j’upload un fichier .txt :

upload7.png
Nous avons une erreur. Un développeur peu soucieux de la sécurité pourrait se dire que son application est sécurisée. Grave erreur! La suite de cette page présentera les différentes manières de bypass les protections d'un formulaire d'upload pour, à la fin, montrer comment le protéger de façon sûr. Et pour commencer nous allons nous intéresser au MIME.

  • Type MIME

Le type MIME, aussi connu comme étant le content-type, est un identifiant de format de données sur Internet. Un type MIME est composé d’au moins deux parties : un type et un sous-type. Par exemple un fichier .png aura pour type « image » et pour sous-type « png ». Bien évidemment il existe plusieurs type MIME dont vous trouverez une liste non exaustive ici  !

Si on reprend le code de notre fichier traitement.png on se rend compte que ce qui permet de faire la différence entre un fichier qui sera uploadé et un fichier qui ne le sera pas, eh bien c’est le type MIME. Oui mais voilà, le type MIME est une donnée qui est envoyée dans la requête et s’il y a un envoi de requête cela veut dire que l’on peut l’intercepter… Et la modifier  !

Pour cela nous aurons besoin d’un proxy web (coucou Burp Suite). Du coup si on se met en écoute,  qu’on upload un fichier .php :

upload2.png

Et qu’on change le type MIME en image/png :

upload3.png

On arrive à upload notre backdoor !

upload4.png

On vient donc de se rendre compte qu’il ne faut pas se fier au type MIME du fichier ! Du coup peut être qu’on pourrait s’intéresser à l’extension du fichier ?

  • Blacklister les extensions dangereuses

Voici un des codes PHP que l’on pourrait utiliser :

<?php
echo "Tentative d'upload ud fichier : ".$_FILES["file"]["name"]."<br>";
echo "Fichier de type : ".$_FILES["file"]["type"]."<br>";
echo "Erreurs : ".$_FILES["file"]["error"]."<br>";

# On explose le nom du fichier afin de récupérer son extension
$extension = end(explode(".", $_FILES["file"]["name"]));
echo "Ce fichier a l'extension : ".$extension."<br>";
# Liste des extensions refusées
$extensions_refusees = array("html", "php");
# Liste des types mime que nous acceptons
$type_mime = array("image/jpg", "image/jpeg", "image/gif", "image/png");
# On récupère le type du fichier uploadé
$type_mime_file = $_FILES["file"]["type"];
# Si le type du fichier uploadé est dans la liste des mime acceptés
if (in_array($type_mime_file, $type_mime) && !in_array($extension, $extensions_refusees)){
  echo "Extension correcte, déplacement du fichier dans le répertoire upload.";
  move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
}
else {
  echo "Extension incorrecte, upload échoué";
}

Ici, en plus de vérifier le type MIME, on vérifie aussi l’extension du fichier envoyé. Si l’extension est .html ou .php alors on bloque l’upload. Sauf que là encore il y a un problème… Bah ouais on bloque l’extension .php mais on ne bloque pas l’extension .php3 ni .php4 (ou encore phtml et pwhl). Du coup si on upload un fichier backdoor.php3 qui contient ce code :

<?php
phpinfo();
?>

Sans oublier de modifier le MIME type eh bien notre fichier sera uploadé et exécutable :

upload5.png

Certains pourraient me dire : « Bah ok, il suffit de rajouter toutes les extensions .php dans la blacklist ». Ce à quoi je vous répondrai que c’est vrai. Mais avant de lire cet article, est ce que vous saviez qu’il y avait plusieurs extensions .php ? Non ?  Eh bien moi non plus quand j’ai commencé ! Donc cette solution n’est en définitive pas suffisante en terme de sécurité.

Voici une liste des extensions atypiques qui permettent d'exécuter du PHP sans utiliser l'extension .php:

phtml
php4
php3
php5
phps
php7
pht
phar
phpt
pgif
phtml
phtm

A la place de vérifier que l’extension ne fait partie de la blacklist, on pourrait faire l’inverse : vérifier que l’extension fait bien partie d’une whitelist !

  • Double Extension Exploit :

Le code que l’on pourrait utiliser dans cette situation ressemblerait plus à mois à ça :

<?php
echo "Tentative d'upload ud fichier : ".$_FILES["file"]["name"]."<br>";
echo "Fichier de type : ".$_FILES["file"]["type"]."<br>";
echo "Erreurs : ".$_FILES["file"]["error"]."<br>";

# On explose le nom du fichier afin de récupérer son extension
$extension = end(explode(".", $_FILES["file"]["name"]));
echo "Ce fichier a l'extension : ".$extension."<br>";
# Liste des extensions refusées
$extensions_acceptees = array("png", "gif", "svg", "jpeg");
# Liste des types mime que nous acceptons
$type_mime = array("image/jpg", "image/jpeg", "image/gif", "image/png");
# On récupère le type du fichier uploadé
$type_mime_file = $_FILES["file"]["type"];
# Si le type du fichier uploadé est dans la liste des mime acceptés
if (in_array($type_mime_file, $type_mime) && in_array($extension, $extensions_acceptees)){
  echo "Extension correcte, déplacement du fichier dans le répertoire upload.";
  move_uploaded_file($_FILES["file"]["tmp_name"], "upload/".$_FILES["file"]["name"]);
}
else {
  echo "Extension incorrecte, upload échoué";
}

A l’inverse de l’exemple précédent, ici on ne va accepter que les fichiers dont l’extension est png, jpeg, gif ou png. Encore une fois il y a un gros problème… En effet qu’est ce qui nous empêche d’uploader un fichier qui a deux extension ? Eh bien justement… Pas grand chose :

upload8.png

Vous noterez que notre fichier a à la fois l’extension php et png. Du coup il passe avec succès les tests du fichier reception.php et en plus de ça le code PHP est interprété !

upload9.png

ATTENTION : pour cette exploitation il a fallu que je modifie le fichier de configuration d’Apache2 afin de le rendre vulnérable. Sur un fichier de configuration par défaut vous n’aurez pas cette faille.

  • Autres vecteurs d’attaque :

Il existe deux autres vecteurs d’attaques. Le premier c’est le Null Byte qui est une conséquence directe du fait que le langage PHP a été écrit en C. Pour délimiter la fin d’une chaîne de caractère en C il est obligatoire d’ajouter un caractère spécial : \0. Cela a mené à une grosse faille de sécurité puisque tout ce qui était situé après le Null Byte dans du code PHP était tout simplement supprimé. Autrement dit, si on upload un fichier qui porte ce nom : backdoor.php%00.png eh bien la partie %00.png va tout simplement être supprimée. Et notre fichier sera uploadé.

Le second vecteur est celui des images commentées. Sur la plupart des outils de retouche photo vous avez la possibilité d’inclure des commentaires dans le fichier. Par exemple sur GIMP si vous allez dans l’onglet « Image » -> « Propriété de l’image » vous aurez la possibilité d’écrire un commentaire. En utilisant cette fonctionnalité vous serez en mesure d’écrire du code PHP directement dans une photo, de l’upload sur le serveur et d’exécuter le code.