Snyk Fetch the Flag CTF 2023: Write-up zu „Off the SETUID“
Carlos Polop
Yago Gutiérrez
30. November 2023
0 Min. LesezeitDanke, dass Sie mit uns Fetch gespielt haben! Glückwunsch an die Tausenden von Spielerinnen und Spielern, die bei Fetch the Flag CTF dabei waren. Wenn Sie bei Snyks Fetch the Flag 2023 dabei waren und nach der Lösung für die Challenge Off the SETUID suchen, sind Sie hier genau richtig. Gehen wir die Lösung gemeinsam durch!
Sie finden sich in einer unbekannten Umgebung wieder. Können Sie mit den vorhandenen Mitteln auskommen? Oder können Sie einfach …
Holen Sie die Flag aus dem Home-Verzeichnis des Root-Benutzers.
Das ist eine seltsame Challenge. Ist es eine Web-Challenge oder eine Kernel-Pwn-Challenge?
Wir haben ein QEMU-Skript, einen kompilierten Kernel (mit dem dazugehörigen .diff) und ein initramfs-Image (samt Code für das Init-Programm). Als Erstes können wir einfach das initramfs entpacken und einen Blick hineinwerfen.
Distribution oder keine Distribution?
Alles klar, keine Distribution. Keine Shell, keine gängigen Tools, rein gar nichts. Das Init-Programm ist natürlich kompiliert (es gibt keine Shell, also kann es kein Skript sein), aber wir haben auch den Quellcode:
Es erledigt die üblichen Aufgaben eines Init-Skripts: Es initialisiert einige Geräte, bindet procfs und sysfs ein (mit noexec), bindet rootfs erneut schreibgeschützt ein und initialisiert das Netzwerk. Anschließend startet es einen HTTP-Server, der auf Port 8080 lauscht und dessen Stammverzeichnis /var/run ist. Der Server läuft als Benutzer mit uid=100.
In /var/run befindet sich nur eine index.php:
Kurz gesagt: Wir haben einen HTTP-Server, der eindeutig anfällig für PHP-Code-Injection ist. Mit etwas wie dem Folgenden können wir also eine interaktive PHP-Reverse-Shell öffnen:
Die Flag können wir aber nicht lesen, da sie nur für Root lesbar ist. Wir müssen also einen Weg finden, unsere Berechtigungen zu erhöhen. Hier kommt fun_setuid.diff ins Spiel.
fun_setuid()
In der .diff-Datei sehen wir, dass dem Kernel ein neuer syscall hinzugefügt wurde:
Kurz gesagt: Dieser syscall ermöglicht einem Prozess, seine uid zu einer anderen zu ändern, sofern diese größer als die aktuelle ist (oder der Prozess über die Capability CAP_SETUID verfügt).
Der syscall verwendet eine Hilfsfunktion namens prepare_user_creds(), die wiederum prepare_cred() nutzt, um eine neue struct cred zuzuweisen. Anschließend ändert sie diese so, dass sie dem neuen Benutzer entspricht, und gibt sie zurück. Tritt ein Fehler auf, gibt sie den negativen Wert eines Fehlercodes zurück. Deshalb prüft die syscall-Funktion vor dem Übernehmen der Anmeldedaten, ob der zurückgegebene Wert nicht negativ ist (siehe Designhinweis unten). Außerdem prüft sie, ob die angeforderte uid ungültig ist (also gleich -1). In diesem Fall gibt sie NULL zurück.
Designhinweis
Dieses Design hat außerdem zwei schwerwiegende Probleme:
Einen Adresswert in C mit null zu vergleichen, funktioniert nicht. Einfach nicht. Adressen werden wie vorzeichenlose Werte behandelt. Daher ergibt es keinen Sinn, zu prüfen, ob eine Adresse negativ ist, und der Compiler entfernt die if-Abfrage vollständig.
Selbst wenn der Compiler die Abfrage nicht entfernen würde, wäre der Code dennoch fehlerhaft: Unter Linux auf x86 sind Kernel-Adressen immer negativ. Selbst wenn
prep_user_creds()keine Probleme findet, würde der zurückgegebene Zeigerstruct cred*als negativ interpretiert und nicht übernommen.
Credentialism
Das Problem: Die syscall-Funktion prüft diese letzte Möglichkeit nicht. Deshalb kann sie einen NULL-Zeiger als Zeiger auf unsere Anmeldedatenstruktur übernehmen. Nachdem wir das initramfs angepasst und ein busybox, hinzugefügt haben, können wir sysctl vm.mmap_min_addr ausführen, um die niedrigste Adresse zu ermitteln, die ein Prozess auf diesem System mappen kann. Für weiteres Debugging steht uns außerdem /proc/config.gz zur Verfügung.
Wir sehen, dass mmap_min_addr=0 gilt und das VM in der Datei run.sh kein SMAP aktiviert hat … hervorragend! Wir können die NULL-Adresse mappen, dort eine Anmeldedatenstruktur für Root ablegen und fun_setuid(-1) aufrufen, sodass unser Anmeldedatenzeiger auf NULL zeigt, also auf unsere gefälschten Anmeldedaten.
Die Capabilities müssen wir nicht unbedingt ändern, aber wenn wir schon dabei sind, können wir das für zusätzliche Punkte tun. Wichtig ist jedoch, cred->usage=1 zu setzen (dieser Wert funktioniert wie ein Referenzzähler). Andernfalls hält der Kernel den Vorgang für einen UAF-Bug: (BUG_ON(atomic_read(&new->usage) < 1)).
Außerdem darf dieser Prozess nicht enden, da der Kernel sonst versucht, diese Struktur freizugeben, was problematisch wäre. Deshalb können wir keine Shell starten (ein Aufruf von execve() führt letztlich zum Ende des aktuellen Prozesses). Darum steht am Ende pause().
Es gibt aber noch ein Problem: Wir können den Exploit nirgendwo hochladen, um ihn auszuführen. Wir können ihn in Shellcode umwandeln und mit PHP-Code in die procfs-Speicherdatei des PHP-Prozesses schreiben, um beliebige native Codeausführung zu erreichen. Von dort aus können wir mmap() auf NULL anwenden, gefälschte Anmeldedaten ablegen und fun_setuid(-1) aufrufen, da sich all das nicht mit einem PHP-Skript erledigen lässt …
Aber das wäre nicht cool genug.
Memexec
Vor einigen Monaten haben wir auf der DEFCON 31 einen Vortrag gehalten, in dem es darum ging, unter bestimmten Umständen Umgebungen ohne Distribution zu umgehen. Dafür habe ich (Yago Gutiérrez) ein Tool namens memexec entwickelt. Damit können Sie beliebige Programme direkt aus PHP heraus ausführen, ohne sie auf einer Datei abzulegen.
Fügen Sie einfach diese interaktive PHP-Sitzung ein. Von nun an steht Ihnen eine Funktion memexec() zur Verfügung, die zwei Argumente entgegennimmt: eine URI zu einer Binärdatei und ein Array mit Argumenten für das Programm. Wir können zum Beispiel einen HTTP-Server mit einem busybox einrichten und Befehle auf dem System ausführen:
Wichtig: In der VM sind keine SSL-Bibliotheken vorhanden. Verwenden Sie daher kein HTTPS.
Alles klar, knacken wir diesen Kernel ein für alle Mal.
Danke, dass Sie Fetch möglich gemacht haben!
Ein großes Dankeschön an alle Teams bei Fetch the Flag 2023! Es war großartig, Sie alle dort zu sehen. Sie finden uns jederzeit unter @Arget (@arget13 auf GH) und @carlospolop (@carlospop ebenfalls auf GH).
Hier finden Sie die Write-ups zu den anderen Challenges von 2023. Viel Spaß beim Lesen!
