Il est né ?
Vous êtes ici : xgarreau.org >> aide >> admin >> suscript : Scripts SUID
Version imprimable

Scripts SUID

Cet article fait suite à une question qui avait été débattue sur la liste lea_aide@club.voila.fr au premier semestre 2001. Comme ce sujet m'avait passionné et qu'il pourrait intéresser d'autres personnes, j'ai écrit cet article.
Afin d'éviter une polémique, je dis tout de suite que la réponse apportée ci-dessous m'avait été inspirée par la lecture de l'article "Eviter les failles dès le développement de vos applications" paru dans le Linuxmagazine-france de Décembre 2000. Ceci dit, le problème est traité ici dans une optique différente qui est de permettre à un administrateur de permettre aux utilisateurs de lancer quelques scripts choisis en temps que root.

Introduction

Qu'est ce que j'appelle 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.
J'en vois qui sourient en me prenant pour un débile se jeter sur leur shell pour me prouver que je suis un tocard qui ne connait pas chmod 4755. Lisez donc encore quelques lignes avant de m'envoyer un mail d'insultes.

Un échec

Voyons si cet article s'adresse à vous !
Imaginons que vous soyez root vous voulez permettre à un simple utilisateur de lire le contenu de votre répertoire personnel, dans un souci de transparence, pour prouver qu'ils n'y trouveront pas de photos pornographiques. Vous écrivez donc un script qui permet d'afficher le contenu de votre répertoire personnel ( je présuppose chez vous des bases de shell ;-) ) :

[root@ZAZOU /root]# cat > voir_rep_root
#!/bin/sh
echo "Contenu du répertoire de" `whoami`
# ou echo "Contenu du répertoire de" $(whoami)
ls -a /root
< Ctrl+D >
et vous le rendez suid root et vous le placez dans /usr/bin pour que tous les utilisateurs puissent l'exécuter en temps que vous ! :
[root@ZAZOU /root]# chmod 4755 voir_rep_root
[root@ZAZOU /root]# mv voir_rep_root /usr/bin/
(Les plus assidus d'entre vous constaterons que mon PC ne s'appelle plus Rooty mais là n'est pas la question)
Vous testez que ça marche pour vous puis en temps qu'utilisateur normal (l'utilisateur xavier par exemple)
[root@ZAZOU /root]# voir_rep_root
Contenu du répertoire de root
.            .bash_history  .cshrc          .toprc  Mail
..           .bash_logout   .mysql_history  .vimrc
.Xauthority  .bash_profile  .parsecrc       .wmrc
.Xdefaults   .bashrc        .tcshrc         .zshrc
[root@ZAZOU /root]# su xavier
[xavier@ZAZOU /root]$ voir_rep_root
Contenu du répertoire de xavier
ls: /root: Permission non accordée
Vous avez compris le problème ça y est ? Votre script est exécutable, appartient au root, a le SUID bit à 1 mais n'en tient pas compte.

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

Bill ? Porte ? Je vous laisse méditer là dessus et passe à l'étape suivante.

En C, ça ne marche pas non plus (au début) !

Vous vous dites alors : "Je suis très intelligent, je vais l'avoir en finesse...". Hé hé hé ! Je rigole parce que c'est ce que je me suis dit moi aussi ...
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.c :

#include <unistd.h>
#include <errno.h>
#include <stdio.h>


extern char **environ;
extern int errno;


int main (int argc, char **argv) {
  execve("/usr/bin/voir_rep_root", NULL, environ);
  printf ("Error : %d\n", errno);
  return errno;
}
( Parenthèse : A ceux qui seraient tentés de dire : "Oui mais pourquoi on met return errno ?", je réponds d'aller lire le man execve où ils pourront constater que si execve ne rencontre pas d'erreur, alors il ne renvoie rien puisque l'image du programme est TOTALEMENT remplacée par celle du programme appelé (ce programme étant l'interpréteur dans le cas d'un script). )
On est content d'avoir fait ce beau programme. Alors on le compile, on le rend exécutable par tout le monde, on met le SUID bit à 1, on le déplace dans /usr/bin et on refait le test de tout à l'heure.
[root@ZAZOU /root]# gcc -o lanceur_de_script lanceur_de_script.c
[root@ZAZOU /root]# chmod 4111 lanceur_de_script
[root@ZAZOU /root]# mv -f lanceur_de_script /usr/bin/
[root@ZAZOU /root]# lanceur_de_script
Contenu du répertoire de root
.            .bash_history  .cshrc          .toprc  Mail
..           .bash_logout   .mysql_history  .vimrc  lanceur_de_script.c
.Xauthority  .bash_profile  .parsecrc       .wmrc
.Xdefaults   .bashrc        .tcshrc         .zshrc
[root@ZAZOU /root]# su xavier
[xavier@ZAZOU /root]$ lanceur_de_script
Contenu du répertoire de xavier
ls: /root: Permission non accordée

Le début de la compréhension

Oui, pour comprendre, rendez vous à la fin de l'article ;-) ... Pour suivre le raisonnement, ajoutez dans le programme une ligne :
printf ("UID %d - EUID %d\n", getuid(), geteuid()); comme ci-dessous :

#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) {
  printf ("UID %d - EUID %d\n", getuid(), geteuid());
  execve("/usr/bin/voir_rep_root", NULL, environ);
  printf ("Error : %d\n", errno);
  return errno;
}
Celà vous donne un début de réponse lors de l'exécution (non ?) :
[root@ZAZOU /root]# chmod 4111 lanceur_de_script
[root@ZAZOU /root]# mv -f lanceur_de_script /usr/bin/
[root@ZAZOU /root]# su xavier
[xavier@ZAZOU /root]$ lanceur_de_script
UID 501 - EUID 0
Contenu du répertoire de xavier
ls: /root: Permission non accordée
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 501. Or, 501 correspond d'après mon fichier /etc/passwd à l'utilisateur xavier. 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.

Ca marche mais c'est dangereux

Celà 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.
Bref ! 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_2.c :

#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) {
  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);
  execve("/usr/bin/voir_rep_root", NULL, environ);
  printf ("Error : %d\n", errno);
  return errno;
}
Une fois le code mis à jour :
[root@ZAZOU /root]# gcc -o lanceur_de_script_2 lanceur_de_script_2.c
[root@ZAZOU /root]# chmod 4111 lanceur_de_script_2
[root@ZAZOU /root]# mv lanceur_de_script_2 /usr/bin/
[root@ZAZOU /root]# su xavier
[xavier@ZAZOU /root]$ lanceur_de_script_2
UID 501 - EUID 0
UID 0 - EUID 0
Contenu du répertoire de root
.            .bash_history  .cshrc          .toprc  Mail
..           .bash_logout   .mysql_history  .vimrc  lanceur_de_script.c
.Xauthority  .bash_profile  .parsecrc       .wmrc   lanceur_de_script_2.c
.Xdefaults   .bashrc        .tcshrc         .zshrc
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 ! Fin de l'article.
Mais non, ce n'est pas la fin de l'article car vous vous rendez sûrement compte à ce niveau que le script n'a pas besoin d'être SUID root pour que celà fonctionne...
Imaginons, un instant que l'on remplace l'appel statique au script par un appel à un script passé en paramètre... Ca devient super pratique mais aussi super dangereux, n'importe quel script pouvant être exécuté en temps que root...

Nicking ze danger !

Ouaip, ça marche ! Mais c'est dangeureux et cet article est là pour vous en faire prendre conscience... D'autre part, un autre point à comprendre est que setreuid vous permet certes de gagner l'accés au privilèges du root dans un programme SUID mais qu'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 pour prendre conscience du côté éphémère de la toute puissance :

#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@ZAZOU /root]# gcc -o createur_de_fichier createur_de_fichier.c
[root@ZAZOU /root]# chmod 4111 createur_de_fichier
[root@ZAZOU /root]# mv -f createur_de_fichier /usr/bin/
[root@ZAZOU /root]# su xavier
[xavier@ZAZOU /root]$ createur_de_fichier
UID 501 - EUID 0
J'ai pu créer le fichier /root/test avant setreuid
UID 0 - EUID 501
Je n'ai pas pu créer le fichier /root/test après setreuid
[xavier@ZAZOU /root]$

Pour ce qui est du danger, il suffit de créer un programme en c permmettant 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@ZAZOU /root]# mkdir .authoscripts
[root@ZAZOU /root]# cd .authoscripts/
[root@ZAZOU .authoscripts]# touch liste
[root@ZAZOU .authoscripts]# cat >> liste
voir_rep_root
< Ctrl+D >
[root@ZAZOU .authoscripts]#
[root@ZAZOU .authoscripts]# mv /usr/bin/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 celà 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@ZAZOU /root]# gcc -o suscript suscript.c
[root@ZAZOU /root]# chmod 4111 suscript
[root@ZAZOU /root]# mv suscript /usr/bin
Dont on vérifie le bon foncionnement grâce à la série de commandes qui suit.
[root@ZAZOU /root]# cd .authoscripts/
[root@ZAZOU .authoscripts]# cat > script_test
#!/bin/sh
echo id: `whoami`
echo "Params :" $*
< Ctrl+D >
[root@ZAZOU .authoscripts]# chmod a+x script_test
[root@ZAZOU .authoscripts]# su xavier
[xavier@ZAZOU .authoscripts]$ cd
[xavier@ZAZOU xavier]$ 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

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.
Vous avez compris le pourquoi de cet article ?

man getuid

man setuid

man setreuid

man getlogin

man cuserid

man chmod

Tshaw

C'est tout pour cette fois !
N'hésitez pas à m'envoyer vos commentaires par mail, il reste sûrement des améliorations à faire.

a+

Auteur : Xavier GARREAU
Modifié le 22.09.2004

Rechercher :

Google
 
Web www.xgarreau.org