Sudo et LOLBins
La croyance confortable sur sudo, c’est qu’une règle étroite est une règle sûre, que NOPASSWD: /usr/bin/find accorde le droit de lancer find et rien d’autre.
GTFOBins a démoli cette croyance et la version honnête d’une note de durcissement sudo doit partir de là: la granularité de sudoers ne limite rien dès l’instant où le binaire autorisé peut être convaincu d’ouvrir un shell et une fraction surprenante de la boîte à outils standard le peut.
La question n’est jamais “quelle commande ai-je accordée”, c’est “qu’est-ce qu’on peut faire faire à cette commande une fois qu’elle tourne en root”.
Pourquoi la règle étroite ne l’est pas
Chacune de ces lignes est un shell root complet depuis une règle qui semble étroitement cadrée:
sudo vi /etc/motd # puis :!/bin/sh
sudo less /var/log/x # puis !/bin/sh
sudo find . -exec /bin/sh \;
sudo awk 'BEGIN{system("/bin/sh")}'
sudo systemctl edit --full x # l'éditeur qu'il ouvre est root
sudo docker run -v /:/host -it alpine chroot /host
Aucune n’est un bug.
Un éditeur est censé pouvoir sortir vers un shell, find est censé exécuter, awk est censé lancer des programmes et docker avec un bind-mount de l’hôte est censé atteindre l’hôte.
GTFOBins n’est que le catalogue des binaires du quotidien qui portent cette propriété et il est long: éditeurs, pagers, interpréteurs, archiveurs et la plupart des outils d’administration système.
Une règle NOPASSWD pour l’un d’eux est un shell root sans mot de passe portant le nom d’une seule commande.
Le corollaire est inconfortable et mérite d’être dit franchement: un fichier sudoers plein de règles soigneusement cadrées peut être, en pratique, du root sans restriction et le fait qu’il le soit dépend entièrement de la présence des binaires autorisés dans GTFOBins. Le cadrage n’a jamais été le contrôle.
Ce qui limite vraiment
Les vraies mesures ne cherchent pas à rendre sûr un binaire GTFOBins; elles évitent de l’accorder ou elles retirent la capacité qu’il détourne.
N’accordez pas d’interpréteurs, d’éditeurs ni de pagers via sudo, point.
Quand un utilisateur doit éditer un fichier appartenant à root, la réponse est sudoedit (c’est-à-dire sudo -e), qui copie le fichier dehors, lance l’éditeur sans privilège sous l’utilisateur et le recopie, de sorte que l’éditeur ne tourne jamais en root et que son échappement vers un shell ne mène nulle part:
alice ALL=(root) sudoedit /etc/nginx/nginx.conf
Quand une commande doit vraiment être accordée, enveloppez-la plutôt que d’exposer l’outil générique et faites valider ses arguments par le wrapper contre une liste blanche pour qu’il n’y ait pas de place où glisser une échappée:
#!/bin/bash
# /usr/local/bin/svcctl, la seule chose que sudo accorde
set -euo pipefail
case "$1:$2" in
apache2:restart|apache2:reload|nginx:restart|nginx:reload) ;;
*) logger -t svcctl "DENY $SUDO_USER $*"; echo "non autorisé" >&2; exit 1 ;;
esac
logger -t svcctl "$SUDO_USER: systemctl $2 $1"
exec /usr/bin/systemctl "$2" "$1"
webadmin ALL=(root) NOPASSWD: /usr/local/bin/svcctl
Le wrapper appartient à root et n’est pas modifiable par l’appelant, ce qui compte, parce qu’un wrapper que l’utilisateur peut éditer n’est qu’un chemin créatif de retour vers un shell root.
Les défauts de sudoers font le reste du travail structurel.
secure_path empêche un binaire déposé plus tôt dans le PATH d’être ce qui s’exécute, env_reset empêche l’environnement de l’appelant de fuir dans la commande root, NOEXEC empêche une commande accordée de fork-exec un enfant là où la plateforme le permet et des chemins absolus complets sans wildcard gardent à la règle le sens qu’elle affiche:
Defaults env_reset
Defaults secure_path="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin"
Defaults !visiblepw
Defaults passwd_tries=3, timestamp_timeout=5
Un wildcard dans une règle de commande (/usr/bin/*, /opt/scripts/*.sh) jette tout cela, parce qu’il réadmet chaque binaire GTFOBins et chaque script que l’utilisateur peut créer sous un nom correspondant.
Les règles portent des chemins complets, des arguments fixes quand c’est possible et jamais de wildcard.
La surveillance, honnêtement
La surveillance utile est le logging d’entrée-sortie, qui enregistre ce qui s’est vraiment passé dans une session sudo plutôt que de le deviner depuis la ligne de commande:
Defaults log_input, log_output
Defaults iolog_dir=/var/log/sudo-io/%{user}
Ce qui ne marche pas et ce contre quoi je m’inscrirais en faux dans une conception, c’est de grepper sudo.log pour des sous-chaînes effrayantes et de verrouiller le compte automatiquement.
Ça génère des faux positifs sur du travail d’admin légitime, ça se contourne trivialement pour quiconque a lu la regex et un attaquant qui a déjà un shell root ne sera pas arrêté par un surveillant de logs qui réagit une seconde plus tard.
La détection sert à la piste d’audit et à l’après-coup, pas à la frontière.
La frontière, c’est de ne pas accorder le binaire dangereux au départ.
Le contrôle applicatif (AppArmor ou SELinux) est le seul endroit où contraindre un binaire plutôt que le refuser paie, parce qu’un profil peut interdire à un outil sensible les fichiers et le réseau qu’il n’a aucune raison de toucher même quand il tourne:
# /etc/apparmor.d/local, illustration: un curl confiné pour une sauvegarde
/usr/bin/curl {
#include <abstractions/base>
network inet stream,
deny /etc/shadow r,
deny /root/** r,
}
C’est un complément, pas un substitut. Le profil rétrécit le rayon d’impact; il ne rend pas sûre une concession sudo sur un binaire GTFOBins.
Où atterrir
Accordez la chose la plus étroite qui fait le travail et assurez-vous que cette chose n’est pas un binaire GTFOBins: un wrapper qui valide ses arguments, sudoedit plutôt qu’un éditeur root, jamais un interpréteur, un pager ou un shell, jamais un wildcard.
Gardez env_reset et secure_path actifs, les wrappers possédés par root et non modifiables par l’appelant et le logging d’entrée-sortie qui coule quelque part où on le lit.
Et gardez la vérité inconfortable en tête pendant que vous écrivez le fichier: le cadrage d’une règle sudoers est une suggestion pour quiconque connaît GTFOBins et les seules règles qui tiennent sont celles dont la commande autorisée ne peut vraiment pas être transformée en shell.
[edit: 2026] L’implémentation Ansible de tout ceci: le rôle users .