SUID Scripts


Introduction

Qu'est ce qu'un SUID Script ? C'est un script que vous souhaiteriez pouvoir exécuter en temps que simple utilisateur mais qui ferait des choses uniquement permises au root.
Voyons si il suffit de comprendre ce que signifie un suidbit...

Histoire d'un échec

Imaginons que vous soyez root vous voulez permettre à un simple utilisateur de lire le contenu de votre répertoire personnel. Vous écrivez donc un script qui permet d'afficher le contenu de votre répertoire personnel:

[root@rch17 test_TD1]# emacs voir_rep_root &

et on tappe:

#!/bin/sh
echo "Contenu du répertoire de" `whoami`
# ou echo "Contenu du répertoire de" $(whoami)
ls -a /root

verifier la création du fichier:

[root@rch17 test_TD1]# ls -altr voir_rep_root
-rw-r--r--    1 root     root          110 nov 19 11:49 voir_rep_root

vous le rendez suid root ( vous pourriez le placez dans /usr/bin pour que tous les utilisateurs puissent l'exécuter en tant que vous !) :

[root@rch17 test_TD1]# chmod 4755 voir_rep_root
[root@rch17 test_TD1]$ ls -altr voir_rep_root
-rwsr-xr-x    1 root     root          110 nov 19 11:49 voir_rep_root*


Vous testez que ça marche pour vous puis en temps que utilisateur normal (l'utilisateur toto par exemple)

[root@rch17 test_TD1]# ./voir_rep_root
Contenu du répertoire de root
.                      .gtkrc-1.2-gnome2
..                     .ICEauthority
alsa-driver-0.9.8      .ispell_english
.
.
.


[root@rch17 test_TD1]# su toto
[toto@rch17 test_TD1]$ ./voir_rep_root
Contenu du répertoire de toto
ls: /root: Permission denied

Vous constatez le problème ... Votre script est exécutable, appartient au root, a le SUID bit à 1 mais le système n'en tient pas compte.


"- La solution c'est chmod 4755 /bin/bash
- Qui a dit ça ? A la porte, tout de suite !"


En C, ça ne marche pas non plus !

Vous vous dites alors : "Je vais l'avoir en finesse...".
Si sur les programmes ça marche, on est tentés de se dire qu'on va écrire un programme qui appelle le script. Allons y, créons lanceur_de_script_V1.c :

[root@rch17 test_TD1]# emacs lanceur_de_script_V1.c &



on tape:

int  main(int argc, char *argv[]) {
             system("./voir_rep_root"); 
 } 

On le compile, on le rend exécutable par tout le monde, on met le SUID bit à 1 et on refait le test de tout à l'heure.

[root@rch17 test_TD1]#  gcc -o lanceur_de_script lanceur_de_script_V1.c
[root@rch17 test_TD1]# chmod 4111 lanceur_de_script
[root@rch17 test_TD1]# ls -altr lanceur_de_script
---s--x--x    1 root     root        11424 nov 19 11:56 lanceur_de_script*
[root@rch17 test_TD1]#./lanceur_de_script
Contenu du répertoire de root
.                      .gtkrc-1.2-gnome2
..                     .ICEauthority
alsa-driver-0.9.8      .ispell_english
.
.
.


[root@rch17 test_TD1]# su toto
[toto@rch17 test_TD1]$ ./lanceur_de_script
Contenu du répertoire de toto
ls: /root: Permission denied

Le début de la solution...

Pour suivre le raisonnement, ajoutez dans le programme une ligne :
printf ("UID %d - EUID %d\n", getuid(), geteuid());



comme ci-dessous :

[root@rch17 test_TD1]# emacs lanceur_de_script_V2.c

on tappe:

 
int  main(int argc, char *argv[]) {
             system("./voir_rep_root");

 printf ("UID %d - EUID %d\n", getuid(), geteuid());  
 }



Cela vous donne un début de réponse lors de l'exécution :

[root@rch17 test_TD1]#  gcc -o lanceur_de_script lanceur_de_script_V2.c
[root@rch17 test_TD1]# chmod 4111 lanceur_de_script
[root@rch17 test_TD1]# su toto
[toto@rch17 test_TD1]$ ./l
lanceur_de_script   lanceur_de_script2
[toto@rch17 test_TD1]$ ./lanceur_de_script
Contenu du répertoire de toto
ls: /root: Permission denied
UID 1019 - EUID 0

Oui, la réponse se situe dans la première ligne de sortie du programme. L'EUID du programme, celle fixée par le SUID bit est bien 0 (root) mais l'uid (celle utilisée pour l'appel du script) est 1019.

A quoi correspond 1019 dans le fichier /etc/passwd ?

Dans cet exemple c'est l'utilisateur toto. D'où un problème dans le comportement attendu ! En fait le script est appelé avec l'UID et non l'EUID. Il faut donc faire un pas supplémentaire et dire au programme de lancer le script en temps que root, c'est à dire faire en sorte que l'UID devienne l'EUID.

Ça marche mais c'est dangereux

Cela n'est rendu possible que parce que le programme est SUID root. Un programme lancé par le root peut prendre l'UID de n'importe qui, par contre un programme lancé par un utilisateur classique ne peut prendre comme UID que son EUID. C'est à dire l'UID de l'utilisateur qui l'a créé, sous réserve qu'il ait placé le SUID bit à 1.


Ce changement se fait grâce à la fonction setreuid. Les fonctions seteuid, setuid et setreuid servent à manipuler les UID et EUID d'un programme. setuid modifie prend un paramètre qu'il affecte à l'UID et l'EUID. seteuid prend un paramètre qu'il affecte à l'EUID. setreuid en prend deux, affect le premier à l'UID et le second à l'EUID. Si un de ces paramètres vaut -1 la valeur correspondante n'est pas changée.


DONC setuid(ID) est équivalent à setreuid(ID, ID) et seteuid(ID) est équivalent à setreuid(-1, ID).

Voici l'illustration de cette partie dans le fichier lanceur_de_script_V3.c :

#include <sys/types.h>
 
int  main(int argc, char *argv[]) {
            
 uid_t uid, euid;
  uid=getuid();
  euid=geteuid();
  printf ("UID %d - EUID %d\n", uid, euid);
  setreuid (euid, euid);
  uid=getuid();
  euid=geteuid();
  printf ("UID %d - EUID %d\n", uid, euid);
 system("./voir_rep_root");

 }

Une fois le code mis à jour :

[root@rch17 test_TD1]#  gcc -o lanceur_de_script lanceur_de_script_V3.c
[root@rch17 test_TD1]# ./lanceur_de_script
UID 0 - EUID 0
UID 0 - EUID 0
Contenu du répertoire de root
.                      .gtkrc-1.2-gnome2
..                     .ICEauthority
.
.
.

[root@rch17 test_TD1]# chmod 4111 lanceur_de_script
[root@rch17 test_TD1]# su toto
[toto@rch17 test_TD1]$ ./lanceur_de_script
UID 1019 - EUID 0
UID 0 - EUID 0
Contenu du répertoire de root
.                      .gtkrc-1.2-gnome2
..                     .ICEauthority
alsa-driver-0.9.8      .ispell_english
.
.
.

Vous voyez qu'après l'appel à setreuid, nous sommes non seulement UID root mais aussi EUID root et que de par le fait, tous les scripts que nous appelons s'exécutent avec l'identité root... Donc, ça marche !


Ce n'est pas la fin car vous vous rendez sûrement compte à présent que le script n'a pas besoin d'être SUID root pour que celà fonctionne...

[root@rch17 test_TD1]# chmod 711 voir_rep_root
[root@rch17 test_TD1]# ls -l voir_rep_root
-rwx--x--x    1 root     root          110 nov 19 11:49 voir_rep_root*
[root@rch17 test_TD1]# su toto
[toto@rch17 test_TD1]$ ./lanceur_de_script
UID 1019 - EUID 0
UID 0 - EUID 0
Contenu du répertoire de root
.                      .gtkrc-1.2-gnome2
..                     .ICEauthority
alsa-driver-0.9.8      .ispell_english
.
.
.


Imaginons, un instant que l'on remplace l'appel statique au script par un appel à un script passé en paramètre...

Voici l'illustration de cette partie dans le fichier lanceur_de_script_V4.c :

#include <unistd.h>
#include <string.h>
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>



int main(int argc, char *argv[]) {
  char test[100];
 uid_t uid, euid;
  uid=getuid();
  euid=geteuid();
  printf ("UID %d - EUID %d\n", uid, euid);
  setreuid (euid, euid);
  uid=getuid();
  euid=geteuid();
  printf ("UID %d - EUID %d\n", uid, euid);
strcpy(test,"" );
 printf(test);
strcat(test,argv[1]);

 printf(test);
             system(test);
         }

alors:

[root@rch17 test_TD1]#  gcc -o lanceur_de_script lanceur_de_script_V4.c
[root@rch17 test_TD1]#  chmod 4111 lanceur_de_script
[root@rch17 test_TD1]# su toto
[toto@rch17 test_TD1]$ ./lanceur_de_script "/bin/bash"
UID 1019 - EUID 0
UID 0 - EUID 0
[root@rch17 test_TD1]# 

Ca devient pratique mais aussi trop dangereux, n'importe quel script pouvant être exécuté en temps que root...

C'est trop dangereux !

Ca marche ! Mais c'est dangereux... D'autre part, le setreuid vous permet certes de gagner l'accès au privilèges du root dans un programme SUID mais il vous permet aussi de les abandonner lorsque vous n'en avez plus besoin.
Celà demande des exemples, allons y, créons le fichier createur_de_fichier.c en temps que root :

#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>

extern char **environ;
extern int errno;

int main (int argc, char **argv) {
  FILE * fd;
  uid_t uid, euid;

  uid=getuid();
  euid=geteuid();
  printf ("UID %d - EUID %d\n", uid, euid);
  if (!(fd = fopen("/root/test", "w")))
    printf ("Je n'ai pas pu créer le fichier /root/test avant setreuid\n");
  else {
    printf ("J'ai pu créer le fichier /root/test avant setreuid\n");
    fclose(fd);
  }
  setreuid (euid, uid);
  uid=getuid();
  euid=geteuid();
  printf ("UID %d - EUID %d\n", uid, euid);
  if (!(fd = fopen("/root/test", "w")))
    printf ("Je n'ai pas pu créer le fichier /root/test après setreuid\n");
  else {
    printf ("J'ai pu créer le fichier /root/test après setreuid\n");
        fclose(fd);
  }
}

Dans le fichier ci-dessus on tente de créer un fichier en c dans un programme SUID puis après avoir inversé l'uid et l'euid. On se rend alors compte que contrairement au cas précédent, c'est l'uid qui compte lors des actions du programme.

[root@rch17 test_TD1]#  gcc -o createur_fichier createur_fichier.c
[root@rch17 test_TD1]#  chmod 4111 createur_fichier
[root@rch17 test_TD1]# su toto
[toto@rch17 test_TD1]$ ./createur_fichier
UID 1019 - EUID 0
J'ai pu créer le fichier /root/test avant setreuid
UID 0 - EUID 1019
Je n'ai pas pu créer le fichier /root/test après setreuid

Pour ce qui est du danger, il suffit de créer un programme en c permettant d'exécuter un script passé en paramètre mais qui vérifie si ce script fait partie d'une liste de scripts autorisés stockée chez le root, dans le répertoire /root/.authoscripts/, dans le fichier liste. D'autre part, les scripts sont cherchés uniquement dans ce répertoire, ainsi, il n'y a que le root qui puisse en ajouter. Il y a quelques instructions à taper :

[root@rch17  /root]# mkdir .authoscripts
[root@rch17 /root]# cd .authoscripts/
[root@rch17  .authoscripts]# touch liste
[root@rch17 .authoscripts]# cat >> liste
voir_rep_root
< Ctrl+D >
[root@rch17 .authoscripts]#
[root@rch17  .authoscripts]# mv voir_rep_root ./

Créons maintenant la prise en charge des SUID Scripts. On ajoute aux contraintes qu'il doit être possible de passer des paramètres aux scripts.
Tout cela nous donne ce code ci :

#include <unistd.h>
#include <errno.h>
#include <stdio.h>
#include <sys/types.h>
#include <string.h>

extern char **environ;
extern int errno;

int main (int argc, char **argv) {
  FILE * fd;
  uid_t uid, euid;
  int isOK = 0;
  char tmpBuff[256];

  if (argc<2) {
    fprintf(stderr, "USAGE : suscript nom_du_script param_1 param_2 ...\n");
    return 1;
  }
  if (!(fd=fopen("/root/.authoscripts/liste", "r"))) {
    fprintf(stderr, "ERREUR : Impossible d'ouvrir le fichier liste dans\
 /root/.authoscripts.\n");
    return 2;
  }
  while (!feof(fd)) {
    fscanf(fd, "%s", tmpBuff);
    if (!strcmp(argv[1], tmpBuff)) {
      isOK++;
      break;
    }
  }
  fclose (fd);
  if (!isOK) {
    fprintf(stderr, "ERREUR : %s n'est pas un script autorisé...\n", argv[1]);
    return 3;
  }
  uid=getuid();
  euid=geteuid();
  setreuid (euid, euid);
  sprintf (tmpBuff, "/root/.authoscripts/%s", argv[1]);
  execve (tmpBuff, &argv[1], environ);
  printf ("Erreur : %d - %s\n", errno, strerror(errno));
  return errno;
}

Que l'on compile et range ainsi :

[root@rch17 /root]# gcc -o suscript suscript.c
[root@rch17 /root]# chmod 4111 suscript
[root@rch17 /root]# mv suscript /tmp

Dont on vérifie le bon fonctionnement grâce à la série de commandes qui suit.

[root@rch17  /root]# cd .authoscripts/
[root@rch17 .authoscripts]# cat > script_test
#!/bin/sh
echo id: `whoami`
echo "Params :" $*
< Ctrl+D >
[root@rch17  .authoscripts]# chmod a+x script_test
[root@rch17 .authoscripts]# echo "script_test" >> liste
[root@rch17  .authoscripts]# su toto
[toto@rch17  .authoscripts]$ cd
[toto@rch17 toto]$ suscript script_test 1 2 3 4 5 6 7 8 9 0
id: root
Params : 1 2 3 4 5 6 7 8 9 0



Si vous considérez que c'est vraiment trop dangereux vous pouvez utiliser un exécutable plutôt qu'un shell pour des actions bien particulières...

Exemple: Ce fichier est capable de lire le contenu d'un fichier texte.

/* fichier affichefich.c */
#include <stdio.h>
 
int main( int argc, char *argv[] ) {
   FILE *f;
   char c;
   /* 1- Ouvre le fichier specifie en argument (argv[1]) en lecture
    */
   f = fopen( argv[1], "r" );
    
   /* 2- recopie caractere par caractere son contenu sur stdout
    */
   do {
      c = fgetc(f);
      if (c != EOF)
         fputc(c, stdout);
   }
   while (c != EOF);
}

utilisation:

[root@rch17 test_TD1]# g++ -o lire_fichier lire_fichier.cpp

[root@rch17 test_TD1]# echo cache > mon_test
[root@rch17 test_TD1]# ls -al mon_test
-rw-r--r--    1 root     root            6 nov 19 13:57 mon_test
[root@rch17 test_TD1]# chmod 700 mon_test
[root@rch17 test_TD1]# ls -al mon_test
-rwx------    1 root     root            6 nov 19 13:57 mon_test*
[root@rch17 test_TD1]# chmod 4111 lire_fichier
[root@rch17 test_TD1]# su toto
[toto@rch17 test_TD1]$ ./lire_fichier mon_test
cache
[toto@rch17 test_TD1]$ more test
test: Permission denied

c'est plus sûr!

En savoir plus

man execve et entre autres, la section NOTES :

NOTES
       SUID and SGID processes can not be ptrace()d SUID or SGID.

       A maximum line length of 127 characters is allowed for the
       first line in a #! executable shell script.

       Linux ignores the SUID and SGID bits on scripts.

man getuid

man setuid

man setreuid

man getlogin

man cuserid

man chmod