Chrooter son site PHP "facilement"

Administration système 8 avr. 2021

Pour protéger son serveur et tout ce qu'il contient, il existe énormément de méthodes, de moyens. Les plus basiques restent de mettre à jour son système / ses applications (ce qui est terriblement efficace) et d'utiliser un logiciel de détection de brute force (les systèmes complet de détection d'intrusion, ou HIDS, tel que Wazuh sont réservés à de plus grosses infrastructures qu'un simple serveur). Rien que ça vous permet déjà de bien sécuriser votre serveur pour parer la majorité des attaques.
Mais dans certains cas, notamment celui des CMS, la recherche de failles est une réel compétition tant les enjeux sont importants. Imaginez, vous trouvez une faille 0 day, vous pouvez automatiser son exploitation et ainsi créer une porte dérobé sur beaucoup de sites. Avec cette faille exploité, vous pouvez louer ou revendre vos victimes devenu zombie à des services de DDoS (une attaque DDoS d'ailleurs est facturé 70€, ça vous laisse voir la rentabilitée assez rapidement).
Ce qui nous intéresse ici cependant, ce n'est pas de sécuriser votre CMS, mais de savoir que si un site web est infecté, le pirate dessus possède de ce fait un utilisateur sur votre machine.

Choquant n'est-ce pas ?
Alors cet accès utilisateur n'est possible que via des scripts PHP, les fameuses "backdoors". À partir de là, notre nouveau pensionnaire pourra agir sur votre site mais aussi sur la machine l'hébergeant (d'ailleurs dites vous que si les sauvegardes de votre CMS sont effectués par le CMS lui même, elles sont bonne a jeter). Les 5 actions les plus courantes sont :

  1. Faire un site zombie pour le mettre à disposition pour une futur attaque DDoS
  2. Servir de pied à terre pour lancer des campagnes de spam, de scan et d'infection d'autres sites
  3. Réaliser un proxy (souvent utilisé par des IPs Russes d'ailleurs)
  4. Faire un site d'hameçonnage
  5. Miner de la cryptomonnaie type monéro tout discrètement.

Vous vous doutez cependant qu'avoir un site troué c'est bien, infecter la machine derrière c'est mieux car plus difficilement détectable. Donc lorsqu'un CMS se fait percer à jour, il n'est pas rare que le pirate (enfin surtout son bot) scan le serveur à la recherche d'une vulnérabilité connu ou tente d'utiliser une faille 0 day. Dans le premier cas si vous mettez votre système à jour il n'y a pas de soucis à se faire. Dans le second cas vous pouvez vous inquiéter... Ou chercher à vous en prémunir !
Je vous explique comment compliquer la tâche aux vilains pirates ?

Alors non, bien que couler un bateau empêche un pirate d'attaquer, il est inutile de saborder votre propre serveur, vous perdrez beaucoup plus que l'attaquant.
Nous allons plutôt regarder du coté des solutions possibles, en excluant les machines virtuelles, c'est consommateur en ressource et pas franchement utile dans nos exemples.
Pour complexifier la vie de Jack Sparrow, vous avez surement songé à la containerisation. En soit c'est une excellente idée, en cas de compromition du site, il est isolé dans un container. Cependant, pour que cette idée reste excellente, il faut que votre container (LXC, Systemd Nspawn, Docker) soit épuré au strict minimum fonctionnel pour éviter une faille 0 day (par exemple vous supprimez les binaire bash ou sh). À partir de là, les images grands publiques sont rarement taillée pour. Donc il vous faut votre propre image, avec les builds, le maintien des mises à jour... C'est long, lent, chiant.
L'autre idée plus accessible c'est... Le chroot (change root). Et pas n'importe laquelle, vous allez utiliser la fonction chroot de PHP. L'avantage de cette méthode est que vous utilisez le paquet PHP de votre système, donc moins de build à suivre, elle ne consomme rien en ressource et elle s'arrête au strict minimum. Bref, idéal.

Allez, c'est parti !

Créer son modèle de chroot

Lors de la première réalisation de ce tuto, utilisez une machine virtuelle. Cet article est inspiré de á.se qui m'a joliment explosé libnss et en tentant de réparer j'ai fusillé libc6. À savoir que sans libnss, votre machine ne pourra pas faire de résolution DNS et sans libc6 bah... Libc6 est utilisé par tout. Donc rien ne fonctionnera correctement. Vous pouvez monter votre propre machine ou ne pas vous embéter et utiliser un VPS (ceux de Hetzner coûtent 0,005€/h).

Pour ce superbe tutoriel il vous faut une machine avec php-fpm dessus, si possible avec au moins un pool fonctionnel autre que celui par défaut. Que vous utilisez Apache ou Nginx n'a pas d'importance, mais je ne sais pas comment configurer Apache pour qu'il utilise un socket spécifique pour php-fpm.
C'est bon ? Votre machine est installé et fonctionnel ? Et bien go !

Tout d'abord nous allons créer un dossier nommé chroot dans /var/chroot. Dans ce dossier, vous allez y placer le fichier chroot.sh que vous trouverez dans mon repository git.

Je vous vois bougonner devant votre écran ! Vous pensiez qu'on allait tout faire à la main ? Non, j'ai créé le script en ajoutant deux la possibilité de mettre à jour vos dépendances et de mettre en place le chroot facilement.

Donc ce script, vous allez le lire attentivement, vous allez devoir modifier deux variables :

  • CHROOTPATH
  • ZONEINFO
  • LISTCHROOTFILE

Pour la première variable, un /var/chroot suffit.
Pour la seconde, vous trouverez votre bonheur avec la commande find /usr -type d -name "zoneinfo" . Récupérez le résultat et ajoutez le à la variable.
La troisième variable permet de définir la localisation du fichier qui liste tout les chroots.

Une fois cela fait, lancez le script avec le paramètre --install : /bin/bash chroot.sh --install.

Le script va

  • Copier les timezones ETC et Europe, sans ça PHP-FPM va automatiquement renvoyer une erreur
*2193567 FastCGI sent in stderr: "PHP message: PHP Fatal error:  date_default_timezone_get(): Timezone database is corrupt - this should *never* happen! in /www/init.php on line 10"
  • Créer et copier une partie du contenu de /etc notamment le resolv.conf pour indiquer à PHP quel serveur DNS utiliser, localtime pour savoir quel timezone utiliser...
  • Créer un /dev avec les fichiers spéciaux (dev, zero, random et urandom) utile pour l'aléatoire notamment
  • Copier les librairies pour que la résolution DNS ainsi que l'accès aux sites sécurisé puissent se faire
  • Copier des binaires tel que bash, dig et ls ainsi que leurs dépendances

Pour la partie librairie, je remercie Alexis Madrzejewski qui, dans son article, a expliqué lesquelles étaient nécessaire (strace est un poil verbeux et j'ai pu voir l'appel aux certificats mais pas les lib dns...).

Une fois le script fini, les librairies et dossiers pour le chroot seront dans le dossier que vous avez défini (pour moi c'est /var/chroot).

Avant de poursuivre, je vous invite à modifier les fichiers hosts, passwd, group et resolv.conf et a retirer ce qui n'a pas a y être (compte d'application par exemple).

Voici à quoi ressemble pour fichier /etc/passwd :

root@Mul /var/chroot # cat etc/passwd 
root:x:0:0:root:/root:/bin/bash
daemon:x:1:1:daemon:/usr/sbin:/usr/sbin/nologin
bin:x:2:2:bin:/bin:/usr/sbin/nologin
sys:x:3:3:sys:/dev:/usr/sbin/nologin
www-data:x:33:33:www-data:/var/www:/usr/sbin/nologin

Et voici à quoi ressemble mon fichier /etc/group

root@Mul /var/chroot # cat etc/group 
root:x:0:
daemon:x:1:
bin:x:2:
sys:x:3:
adm:x:4:
tty:x:5:

Chrooter son site

Maintenant nous allons mettre en place le chroot pour votre site. Tout d'abord votre site doit se trouver dans un sous-sous dossier d'un dossier partagé. Par exemple Shaarli est stocké dans /var/www/dryusdan_link/www, de ce fait, /var/www/dryusdan_link va posséder les librairies, les configurations... Votre site sera donc virtuellement dans /www. Voilà à quoi ressemblera votre chroot.


/
├── bin
│   ├── bash
│   └── ls
├── dev
│   ├── null
│   ├── random
│   ├── urandom
│   └── zero
├── etc
│   ├── group
│   ├── hosts
│   ├── ld.so.cache
│   ├── localtime
│   ├── networks
│   ├── nsswitch.conf
│   ├── passwd
│   ├── protocols
│   ├── resolv.conf
│   ├── services
│   └── ssl
│       └── certs
│           ├── 02265526.0 -> Entrust_Root_Certification_Authority_-_G2.pem
├── lib
│   └── x86_64-linux-gnu
│       ├── libcom_err.so.2
│       ├── libc.so.6
│       ├── libdl.so.2
│       ├── libdns-export.so.1110
│       ├── libdns-export.so.1110.0.2
│       ├── libdns-export.so.162
│       ├── libdns-export.so.162.1.3
│       ├── libgcc_s.so.1
│       ├── libkeyutils.so.1
│       ├── liblzma.so.5
│       ├── libm.so.6
│       ├── libnss_dns-2.31.so
│       ├── libnss_dns.so.2
│       ├── libnss_mdns.so.2
│       ├── libpthread.so.0
│       ├── libresolv.so.2
│       ├── libselinux.so.1
│       ├── libtinfo.so.6
│       └── libz.so.1
├── lib64
│   └── ld-linux-x86-64.so.2
├── tmp
├── usr
│   ├── bin
│   │   └── dig
│   ├── lib
│   │   └── x86_64-linux-gnu
│   │       ├── libbind9-9.16.12-Debian.so
│   │       ├── libcrypto.so.1.1
│   │       ├── libcurl.a
│   │       ├── libcurl.so
│   │       ├── libdns-9.16.12-Debian.so
│   │       ├── libfstrm.so.0
│   │       ├── libgssapi_krb5.so.2
│   │       ├── libicudata.so.67
│   │       ├── libicuuc.so.67
│   │       ├── libidn2.so.0
│   │       ├── libirs-9.16.12-Debian.so
│   │       ├── libisc-9.16.12-Debian.so
│   │       ├── libisccfg-9.16.12-Debian.so
│   │       ├── libjson-c.so.5
│   │       ├── libk5crypto.so.3
│   │       ├── libkrb5.so.3
│   │       ├── libkrb5support.so.0
│   │       ├── liblmdb.so.0
│   │       ├── libmaxminddb.so.0
│   │       ├── libns-9.16.12-Debian.so
│   │       ├── libnss3.so
│   │       ├── libnss_dns.so
│   │       ├── libpcre2-8.so.0
│   │       ├── libprotobuf-c.so.1
│   │       ├── libsqlite3.a
│   │       ├── libsqlite3.so
│   │       ├── libssl.so.1.1
│   │       ├── libstdc++.so.6
│   │       ├── libunistring.so.2
│   │       ├── libuv.so.1
│   │       ├── libxml2.a
│   │       ├── libxml2.so
│   │       └── libxml2.so.2
│   └── share
│       └── zoneinfo
├── var
│   └── lib
│       └── php
│           └── sessions
└── www
│   └── index.php

Pour chrooter votre site PHP, tapez la commande ./chroot.sh --configure /var/www/dryusdan_link. Cette commande va créer les dossiers nécessaires, faire un hardlink (ou lien physique) sur les librairies et quelques fichiers de configuration pour ne pas dupliquer les fichiers et prendre de la place inutilement. Pour d'autre fichier tel que passwd, hosts, group, ils seront copiés. Suite à ça chroot.sh va créer un dossier etc/ssl/certs et monter en lecture seul le /etc/ssl/certs de votre système. Pour finir créer un dossier /var/lib/php/sessions avec un sticky bit de façon a ressembler au même dossier du système.

Bon, vous avez fait 90% du travail. Maintenant il faut configurer PHP (ce qui est relativement facile).

Je pars du principe que vous avez déjà configuré un pool FPM avec PHP et que votre serveur HTTP l'utilise.

Ici nous allons avoir besoin de deux configuration : chroot et chdir

[dryusdan.link]

chdir = /www
chroot = /var/www/dryusdan_link

php_admin_value[disable_functions] = pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_get_handler,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,pcntl_async_signals,exec,passthru,popen,proc_close,proc_get_status,proc_nice,proc_open,proc_terminate,shell_exec,show_source,system

Le chroot ici indique au pool FPM que la racine du système se trouve dans /var/www/dryusdan_link (donc / est égal au dossier mentionner juste avant) et votre site présent dans le dossier www se retrouve donc pour PHP dans... /www ! Donc vous indiqueé que votre site est dans /www avec la directive chdir .

Pour les plus agguerie, j'ai rajouté la directive php_admin_value[disable_functions]. Cette directive a pour but d'interdire au code d'utiliser certaines librairies ou fonction (par exemple vous pouvez interdire la commande shell_exec empéchant ainsi l'accès à /bin/bash par PHP (même s'il est chrooté). Cela réduit encore plus la surface d'attaque possible. Je ne vous cite pas tout, vous trouverez toutes les informations dans la superbe doc php.net.

À savoir qu'après avoir chrooté votre site avec PHP, il faut changer une directive $document_root dans Nginx lorsque vous appelez vos scripts PHP. En effet elle va avoir la valeur /var/www/dryusdan_link/www alors que PHP connait que /www.

Bien entendu le chroot peut apporter d'autre soucis (notamment lié à des extensions PHP qui ne peuvent pas accéder à une librairie). Il faut bien connaitre son application ou ne pas avoir peur de la commande strace.

Et nous avons fini ! Vous gérer maintenant l'art de chrooter son site :)

Bien que cet article ressemble à une bidouille qu'on testera une fois, la réalité est tout autre. J'utilise la fonction chroot de PHP sur tous mes sites en PHP. Une fois l'application connue et maitrisé il est facile de la maintenir à jour, de la faire évoluer sans se faire embêter par le chroot.

Après 1 mois sur ma prod, je constate que ça ne consomme juste rien en temps et en ressource, bref une solution quasi parfaite !

Sur ce, portez-vous bien.

Photo by Bill Oxford

Dryusdan

Chasseur de bug et régleur de problème (alias DevOps).

Super ! Vous vous êtes inscrit avec succès.
Super ! Effectuez le paiement pour obtenir l'accès complet.
Bon retour parmi nous ! Vous vous êtes connecté avec succès.
Parfait ! Votre compte est entièrement activé, vous avez désormais accès à tout le contenu.