Compte rendu du CTF Snyk Fetch the Flag 2023 : Off the SETUID
Carlos Polop
Yago Gutiérrez
30 novembre 2023
0 minutes de lectureMerci d’avoir participé à Fetch avec nous ! Félicitations aux milliers de joueurs qui nous ont rejoints pour le CTF Fetch the Flag. Si vous avez participé à Fetch the Flag 2023 de Snyk et cherchez la solution du défi Off the SETUID, vous êtes au bon endroit. Découvrons-la ensemble !
Vous voilà dans un environnement inconnu. Saurez-vous vous débrouiller avec les ressources disponibles ? Ou pourrez-vous simplement vous en sortir…
Récupérez le flag dans le répertoire personnel de l’utilisateur root.
Voilà un drôle de défi. S’agit-il d’un défi web ou d’une exploitation du noyau ?
Nous avons un script qemu, un noyau compilé (avec son fichier .diff) et une image initramfs (ainsi que le code du programme init). Pour commencer, nous pouvons simplement décompresser initramfs et regarder ce qu’elle contient.
Distribution ou pas distribution ?
Bon, il n’y a pas de distribution. Pas de shell, pas d’utilitaires courants, rien du tout. Le programme init est évidemment compilé (il n’y a pas de shell, donc il ne peut pas s’agir d’un script), mais nous avons aussi son code source :
Il effectue les opérations habituelles d’un script init : initialiser certains périphériques, monter procfs et sysfs (avec noexec), remonter rootfs en lecture seule et initialiser le réseau. Il démarre ensuite un serveur HTTP qui écoute sur le port 8080, dont le répertoire racine est /var/run, et qui s’exécute avec l’utilisateur ayant uid=100.
Dans /var/run, il n’y a qu’un fichier index.php :
En résumé, nous avons un serveur HTTP manifestement vulnérable à l’injection de code PHP. Nous pouvons donc utiliser quelque chose comme ce qui suit pour obtenir un shell PHP interactif inversé :
Mais nous ne pouvons pas lire le flag, car seul root y a accès. Nous devons donc trouver un moyen d’élever nos privilèges. Voici fun_setuid.diff.
fun_setuid()
Dans le fichier .diff, nous voyons qu’un nouveau syscall a été ajouté au noyau :
En bref, ce syscall permet à un processus de modifier son uid pour en adopter un autre, à condition que celui-ci soit supérieur à l’actuel (ou que le processus dispose de la capacité CAP_SETUID).
Le syscall utilise une fonction auxiliaire appelée prepare_user_creds(), qui fait appel à son tour à prepare_cred() pour allouer une nouvelle struct cred, puis la modifie pour qu’elle corresponde au nouvel utilisateur et la renvoie. En cas d’erreur, la fonction renvoie la valeur opposée d’un code d’erreur. C’est pourquoi la fonction syscall vérifie que la valeur renvoyée n’est pas négative avant de valider les identifiants (voir la note de conception ci-dessous). Elle vérifie également que le uid demandé est valide (c’est-à-dire différent de -1) ; dans le cas contraire, elle renvoie NULL.
Note de conception
Cette conception présente également deux problèmes majeurs :
En C, comparer une adresse à zéro ne fonctionne pas. Tout simplement. Les adresses sont traitées comme des types non signés : vérifier si l’une d’elles est négative n’a donc aucun sens, et le compilateur supprime entièrement le if.
Même si le compilateur ne le supprimait pas, le code ne fonctionnerait toujours pas, car sous Linux sur x86, les adresses du noyau sont toujours négatives. Ainsi, même si
prep_user_creds()ne détecte aucun problème, le pointeurstruct cred*qu’elle renvoie serait interprété comme négatif et ne serait pas validé.
Les identifiants
Le problème, c’est que la fonction syscall ne vérifie pas cette dernière possibilité : elle peut donc valider un pointeur NULL comme pointeur vers notre structure d’identifiants. Après avoir modifié initramfs pour y ajouter busybox, nous pouvons exécuter sysctl vm.mmap_min_addr afin de connaître l’adresse minimale qu’un processus peut mapper sur ce système. Si nous avons besoin d’effectuer des vérifications supplémentaires, nous avons aussi accès à /proc/config.gz.
Nous constatons que mmap_min_addr=0 et que le script run.sh n’active pas SMAP sur la machine virtuelle… Parfait ! Nous pouvons mapper l’adresse NULL, y placer une structure d’identifiants pour root et appeler fun_setuid(-1) afin que notre pointeur d’identifiants pointe vers NULL, c’est-à-dire vers nos faux identifiants.
Nous n’avons pas vraiment besoin de modifier les capacités, mais puisque nous y sommes, autant le faire pour gagner des points supplémentaires. Notez toutefois qu’il est essentiel de définir cred->usage=1 (qui joue le rôle d’un compteur de références), sinon le noyau considérera qu’il s’agit d’un bug UAF (BUG_ON(atomic_read(&new->usage) < 1)).
Nous ne devons pas non plus laisser ce processus se terminer, car le noyau essaierait alors de libérer cette structure, ce qui serait problématique. C’est pourquoi nous ne pouvons pas lancer un shell (un appel à execve() entraîne finalement la fin du processus en cours), d’où le pause() à la fin.
Mais il reste un problème : nous ne pouvons téléverser l’exploit nulle part pour l’exécuter. Nous pouvons le convertir en shellcode et utiliser du code PHP pour écrire dans le fichier mem de procfs du processus PHP, afin d’exécuter du code natif arbitraire. Nous pourrons alors mapper NULL avec mmap(), y placer de faux identifiants et appeler fun_setuid(-1), puisque nous ne pouvons rien de tout cela avec un script PHP…
Mais ça manque un peu de panache.
Memexec
Il y a quelques mois, nous avons donné une conférence à DEFCON 31 sur le contournement, dans certaines circonstances, des environnements sans distribution. Pour cela, j’ai (Yago Gutiérrez) développé un outil appelé memexec, qui permet d’exécuter sans fichier, depuis PHP, n’importe quel programme.
Collez simplement cette session PHP interactive. Vous disposerez alors d’une fonction memexec() qui accepte deux arguments : l’URI d’un binaire et un tableau d’arguments pour le programme. Nous pouvons, par exemple, configurer un serveur HTTP avec busybox et exécuter des commandes sur le système :
Important : la machine virtuelle ne contient aucune bibliothèque SSL. Veillez donc à ne pas utiliser HTTPS.
Bon, allez, exploitons ce noyau une bonne fois pour toutes.
Merci d’avoir contribué à la réussite de Fetch !
Un immense merci à toutes les équipes qui ont participé à Fetch the Flag 2023 ! C’était formidable de vous y retrouver. Vous pouvez toujours nous retrouver sur @Arget (@arget13 sur GH) et @carlospolop (@carlospop également sur GH).
Voici les comptes rendus des autres défis de 2023. Bonne lecture !
