
J’ai ces derniers temps dû faire face à ce que je redoute le plus depuis que les ports 80 de par le globe ont commencé à s’ouvrir.
Tel Freddy contre Jason, tout comme Batman face au Joker, tout comme Demis Roussos devant son rasoir, il eut à se battre dans une joute mortelle et sans fin contre sa plus grande chimère, à la fois sa pire peur et son pire cauchemar, contre L’Engeance Malsaine Du Web 1.5 : PHP
Travaillant au développement d’une petite appliance VPN maison, il me fallait “coder” une interface web pour celle-ci, avec pour seul langage disponible PHP.
Je me permet de mettre coder entre guillemets pour la simple joie du Troll, et de rappeler que pour recruter un développeur PHP, il suffit de mettre un coup de pied dans une poubelle, une dizaines en ressortent.
La question qui s’est très vite posée : Comment faire exécuter des tâches nécessitant les droits root de manière :
- Pas trop sale
- Pas trop insecure
Ci-après un florilège des solutions les plus imbéciles qui font autorité sur la toile, et démonstration d’une solution viable.
Pot-pourri de solutions pourries
La méthode sudo no password
Celle-ci, qui n’est pas la pire de toute dans le sens où elle a le mérite de ne stocker aucun mot de passe en clair, reste rigolote à regarder.
Elle consiste à utiliser sudo, en ayant au préalable déclaré comme sudoer (/etc/sudoers) l’utilisateur du serveur web (fut-ce www-data ou autre chose), et en lui octroyant le droit de sudo sans mot de passe. Je cite donc ce judicieux (ou pas) conseil donné par un certain “Velox Letum” sur codingforums :
I just thought of a different solution. Do you have sudo? If so this could be pretty easy. Add a line to /etc/sudoers. You have to edit this with the command visudo.
apacheusername ALL=(ALL) NOPASSWD
Then every time the script is executed (either by command line or by the webserver) it automatically gains root privileges
En voilà une idée sympathique à ne pas suivre. Et un accès root gratuit, un !
La méthode sudo -S + pipe
Une autre méthode à grands coups de sudo, qui elle nécessite un mot de passe, mais stocké en clair.
Le principe est le même : apacheusername ALL=(ALL) dans le /etc/sudoers (pas de NOPASSWD).
puis, du côté de php, il suffit d’utiliser sudo -S, qui permet de lire le mot de passe via l’entrée stdin, et d’y pipe joyeusement notre mot de passe en clair :
< ?php system('echo "my-super-password" | sudo -S rm -rf /'); ?>
Sympa aussi, non ?
La méthode ssh+rsa_key
Une sympathique que j’ai peu croisé et plutôt rigolote.
Le principe est fort simple : générer une paire de clés privée/publique à l’utilisateur du serveur web, puis déposer la clé publique de celui-ci dans /root/.ssh/authorized_keys.
< ?php system('ssh root@localhost -t rm -rf /'); ?>
La grande classe.
La méthode ultime : exécuter le serveur web en tant que root
Non, je déconne.
Une vraie solution :
La méthode Wrappers + suid
Après cette fort intéressante compilation d’extraits des conseils des plus grandes éminences du Web2.0 (ou pas, toujours), je vais vous présenter une méthode que je juge plus propre, plus sécurisée, dans le sens ou :
- Aucun mot de passe en clair n’est ni utilisé ni pipé
- Ni sudo ni su ne sont utilisés
- Le processus qui s’exécute en root ne fait et ne peut faire QUE ce qu’on lui a demandé de faire et pas autre chose
Le principe dans est simple. Nous allons coder un “wrapper”, ici en C (mais celà peut tout aussi bien être fait en python Perl ou Ruby), qui va faire l’action système que nous voulons que php fasse en root. Puis nous lui affecterons le droit de suid(0).
Démonstration ici dans un cas pratique :
Ex : Insertion de routes statiques (linux/iproute2)
L’objectif est ici tout bête, pouvoir via un formulaire, insérer une route statique dans la Kernel Table.
La forme est simple :
ip route add $IP/$CIDR_MASK via $GATEWAY dev $DEVICE
On crée d’abord un endroit ou stocker les wrappers :
root@web$ mkdir -p /home/wrappers/
On code un petit wrapper en C, qui va devoir suid(0); :
root@web:/home/wrappers/$ cat add-static-route.c
#include "stdio.h"
#include "sys/types.h"
#include "unistd.h"
int main (int argc, char *argv[])
{
if (setuid(0))
{
perror("setuid");
return 1;
}
execl("/sbin/ip", "/sbin/ip", "r", "add", argv[1], "via", argv[2], "dev", argv[3], (char *) 0);
return 0;
}
On le compile, puis on lui donne le droit de setuid :
root@web:/home/wrappers/$ gcc -o add-static-routes add-static-route.c root@web:/home/wrappers/$ chmod +s add-static-route root@web:/home/wrappers/$ ls -ahl add-static-route -rwsr-sr-x 1 root root 6,5K 2010-08-23 23:53 add-static-route
Exemple de form pour l’ajout d’une route :
Destination Address: CIDR Mask (Without "/"): Gateway: Device:
| Destination Address: | |
| CIDR Mask (Without “/”): | |
| Gateway: | |
| Device: | |
Et voilà l’appel au wrapper :
Évidemment vous serez bien aimable d’assainir vos $_POST en les validant avant d’éxécuter le wrapper, s’il vous plait, pas comme ici.
Cas de plusieurs commandes :
execl() ne permet en effet pas de multiplier les commandes.
Pour ce faire la solution la plus simple consiste à passer dans le wrapper C non pas la commande, mais un script de votre cru. (On ne peut pas suid un script directement, celui-ci sera ignoré).
L’autre solution, ce sont les fpipes :
/* C Wrapper for settings VPN X509 Certificate */
#include "stdio.h"
#include "sys/types.h"
#include "unistd.h"
int main(void)
{
if (setuid(0))
{
perror("setuid");
return 1;
}
FILE *fpipe;
char *command="/bin/mv /home/vpn/ca.crt /home/vpn/ca.crt.old";
char line[256];
if ( !(fpipe = (FILE*)popen(command,"r")) )
{
perror("Problems with pipe");
exit(1);
}
while ( fgets( line, sizeof line, fpipe))
{
printf("%s", line);
}
pclose(fpipe);
command="/bin/mv /var/www/upload/ca.crt /home/vpn/ca.crt";
if ( !(fpipe = (FILE*)popen(command,"r")) )
{
perror("Problems with pipe");
exit(1);
}
while ( fgets( line, sizeof line, fpipe))
{
printf("%s", line);
}
pclose(fpipe);
execl("/bin/chown", "/bin/chown", "vpn:vpn", "/home/vpn/ca.crt", (char *) 0);
return 0;
}
Voilà. En espérant croiser un peu moins des méthodes citées dans le pot-pourri.
Merci pour l’astuce !
Et le savon ?
Comment faire quelquechose de simple de horriblement
La méthode la plus simple à mon sens reste d’enchainer les commandes a réaliser dans un script bash et de configurer sudo pour autoriser l’user www-data a éxécuter CE SCRIPT (et lui uniquement) en tant que root via une ligne du style :
www-data ALL = NOPASSWD: /home/moncul/monscript.sh
ça c’est une vraie solution …
Il faut bien sur que ce script ait les permissions qui vont bien pour éviter qu’il soit modifié par n’importe qui. Pour les plus paranoïaques un petit chattr +i devrait faire l’affaire.
De “simple horriblement” ?
Sudo est quelque chose de très faillible et completement unsecure. Rien que ces 6 derniers mois, deux failles de sécurité critiques permettant d’obtenir un root sans aucun mot de passe avec trois bouts de ficelles dans un script bash.
Donc pour la “Vraie solution”, on repassera.
Quand aux wrappers C et autres perl, pourquoi, s’ils sont si horrible et ne sont pas La solution, sont t’il alors utilisé dans les distributions spécialisée de Routage et autre appliance ?
Je voulais écrire “de simple de manière horriblement compliquée”.
Certes les wrappers sont utilisés dans les distributions spécialisées routage – et alors ? On parle ici de créer deux routes avec un script php maison, si on peut éviter de sortir gcc pour ça c’est quand même plus sympa …
Mon ojectif principal était de signaler le manque d’honnêteté de cet article qui ne présente dans son pot-pourri que des solutions – effectivement débiles – sans prendre aucun recul, alors qu’il est possible en les modifiant très légéremment d’obtenir des résultats acceptables.
Les routes sont un exemple. Dans l’article, je parle de ce qui a été fait sur une distrib de routage/VPN(justement) maison, et qui va taper du BGP. Les bonnes habitudes n’ont selon moi pas spécialement a avoir d’exception, et je n’ai aucune confiance en sudo au vu de ses derniers déboires.
Ceci étant, je ne manque pas d’honnêteté, mais je suis plus probablement sarcastique et cynique, à bon escient au vu du caractère dangereux que représente sudo.
Tu aurait dût utiliser du SOAP …
C’est tellement vulgaire comme attaque…
Un “cp -b” plutot que 2 mv aurait été plus joli dans le deuxième exemple.
Des chemins relatifs plutôt que des chemin absolu aussi (pour la compatibilité avec des chroot et systèmes multiples (plusieurs arborescences systèmes/distrib tournant sur un même noyau ou des emulations)).
Enfin si on peut utiliser des scripts CGI je préfère en utiliser de mon cru qui ne prennes que certaines commandes pré-definies en entrée.
Merci tout de même beaucoup pour ce billet (qui m’a permis de poster).
Pingback: Le panier du marché libre #2
Faire un appel system avec des paramètres en $_POST non protégé ça fait juste froid dans le dos. Heureusement, ce genre de code ne traîne pas sur mes serveurs.
Je remet ici la très juste remarque de Truffo, au cas ou certains n’aient pas compris que l’exemple d’appels de POST n’est qu’un exemple à la va-vite.
“Avec un paramètre dans le formulaire du genre ” ; rm -rf /var/www/monsite” dans me champ destination. Ce qui donne appel :
system(‘/home/wrappers/add-static-route; rm -rf /var/www/monsite’);
En supposant que l’utilisateur qui exécute le script a les droits d’écriture sur le répertoire, je vous laisse imaginer le carnage.
Bien sûr, toutes les commandes possibles et imaginables peuvent être passé en paramètre et donc exécuté.”
Un probleme qui ne m’est pas venu a l’idée dans ce cas-ci, puisque l’ensemble du systeme de l’appliance fonctionne en lecture seule, que l’utilisateur du serveur web n’as pas la possibilité de lever cette restions sans un appel au Wrapper adequate.
Tu peux citer des sources concernants sudo ?
Je trouve plus pratique de n’autorisé qu’une commande en tant que root, plutôt que toute (de même, faire un cgi-c à chaque fois..)