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.
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.
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)
[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éeVous 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.
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). )
[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
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éeOui, 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.
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 .zshrcVous 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.
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.
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/binDont 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
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
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+