Skip to content

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.

Chaque processus Linux porte deux identifiants utilisateur distincts :

IdentifiantRô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 (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.

#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(nouveau_ruid, nouveau_euid) fixe explicitement les deux identifiants du processus.

Au moment de l’appel :

  • geteuid() retourne l’EUID courant → l’UID de utilisateur-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

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-cible

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.

Terminal window
echo "/bin/cat /chemin/vers/fichier-protege" > /tmp/ls
chmod +x /tmp/ls
Terminal window
export PATH=/tmp:$PATH
Terminal window
./binaire-suid
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ée
Terminal window
# Confirmer que /tmp apparaît en premier
echo $PATH
# Vérifier que le shell trouve bien le faux ls
which ls
# → /tmp/ls

Pourquoi /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.

Appeler les commandes avec leur chemin absolu

Section titled “Appeler les commandes avec leur chemin absolu”
// Vulnérable
system("ls /chemin/vers/fichier");
// Correct
system("/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é.

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 shell
char *args[] = {"/bin/ls", "/chemin/vers/fichier", NULL};
char *env[] = {NULL}; // environnement vide
execve("/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.

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().