
Linux LPE - Zuverlässiger Jail/Container-Escape
Ein zuverlässiger Proof-of-Concept für einen nicht-privilegierten Container-/Jail-Escape für CentOS / RHEL 10.
Es reitet einen mittlerweile behobenen IPv6-Fragmentierungsfehler in __ip6_append_data() (upstream geschlossen durch 38becddc, keine CVE), einen linearen In-Slab-Überlauf in den skb_shared_info am Ende des eigenen Head-Objekts eines Pakets. Dieser README dokumentiert nur die Exploitation-Kette. Er behandelt nicht den Auslöser.
6.12.0-242.el10).Dies ist ein Proof-of-Concept, bewusst kein einsatzbereites Werkzeug. Das Reliability-Gerüst wurde absichtlich weggelassen: kein per-CPU PCP-Grooming, keine Cross-Cache SLUB-Armierung, und der skb-Head ist in einem gemeinsam genutzten, nicht so ruhigen Cache platziert. Was ausgeliefert wird, zündet oft genug, um die Kette zu beweisen, und nicht viel mehr. Es ist nur auf CentOS / RHEL 10 ausgelegt.
CONFIG_INIT_ON_ALLOC_DEFAULT_ON aus, der Standard von RHEL / CentOS. Dieser PoC pflanzt einen veralteten Zeiger in nicht initialisierte Slab-Bytes; mit init_on_alloc=1 wird dieser Slot genullt und diese spezielle Technik stürzt nur den Kernel ab (eine NULL-Deref). Das ist eine Grenze der Technik, nicht des Bugs: Ein vollständiger Exploit, der auch diese Kernel abdeckt, mit einer anderen Technik, wird folgen, sobald der Fix und die CVE öffentlich sind./sys/kernel/btf/vmlinux vorhanden und weltweit lesbar (Standard auf RHEL / CentOS).| Distro | Kernel | Ergebnis |
|---|---|---|
| CentOS Stream 10 | 6.12.0-242.el10 | Root, Container-Escape |
| RHEL 10 | 6.12.0-228.el10 | httpd_t-Kontext-Escape |
In-Slab-Überlauf zu einer Selbst-UAF. Der Bug gibt die Kontrolle über das einzelne nr_frags-Byte in skb_shared_info. Der Freigabepfad (skb_release_data()) durchläuft frags[0 .. nr_frags) und führt put_page() auf jeden Eintrag aus, und frags[] wird nie initialisiert. Wir platzieren vorab einen struct page * für eine Pipe-Buffer-Seite in diesen Slab-Slot durch kontrollierte Cache-Wiederverwendung, setzen dann nr_frags auf 1, sodass der Abbau eine Referenz auf eine Seite verliert, die wir noch besitzen. Der lineare Überlauf wird zu einem Page-Use-After-Free. Der Überlauf berührt nie frags[0], muss also nicht gegen das Spray getimed werden, was ihn größtenteils nachsichtig macht.
Page-UAF zu Dirty-Pagetable. Die freigegebene Pipe-Seite wird als letzte Level-Seitentabelle zurückgefordert, indem eine frische anonyme Mapping ausgelöst wird. Eine physische Seite ist nun gleichzeitig eine lebendige Blatt-Seitentabelle und die Seite, die die Pipe noch liest und schreibt. Das Schreiben von acht Bytes in die Pipe installiert eine gefälschte PTE; das Lesen der Pipe liest die Tabelle zurück. Dies ist ein endlicher beliebiger physischer Lese-/Schreibzugriff, in der Größenordnung von 460 PTE-Fenstern pro Tabelle.
Mit dem physischen Lesen scannen wir die feste Low-Memory-SMP-Trampoline-Seitentabelle, die KASLR nie verschiebt; ihr Kernel-Hälfte-Eintrag zeigt auf , die physische Basis des Kernels. Von dort ist (erkennbar an seinem selbstreferenzierenden Eintrag 511) ein universeller virtuell-zu-physisch-Übersetzer, der die virtuelle Kernel-Basis wiederherstellt und unseren Adressraum an den physischen Speicher bindet.
level4_kernel_pgtinit_top_pgtEndlich zu unendlich Lese-/Schreibzugriff. Wir fälschen eine PTE in der Blatttabelle, die auf die physische Adresse der Tabelle selbst zeigt. Das passende virtuelle Fenster aliast dann die Seitentabelle selbst, sodass PTEs zu normalem Speicher werden: unbegrenzter, wahlfreier Kernel-Lese-/Schreibzugriff, ohne Pipe in der Schleife. Die Ring-3-TLB-Kohärenz wird durch die Erzwingung eines vollständigen mm-Flush mit einem übergroßen mprotect() behandelt.
Offsets auflösen und Credentials übernehmen. Struct-Member-Offsets werden zur Laufzeit aus /sys/kernel/btf/vmlinux gelesen, nicht fest eincompiliert. Wir lokalisieren unsere eigene task_struct durch den vmemmap struct page-Walk und mm_struct.owner, nullen die Credential-IDs und füllen alle Capability-Sets auf cred und real_cred. Symbol-Adressen stammen von einem internen Resolver, der die eigenen kallsyms-Tabellen des Kernels durch unseren beliebigen Lesezugriff parst; nur wenn das fehlschlägt, fallen wir darauf zurück, cred.user_ns auf init_user_ns zu setzen (damit CAP_SYSLOG im initialen Namespace gilt) und die echten Adressen aus /proc/kallsyms zu lesen.
SELinux deaktivieren, ohne enforcing umzuschalten. Wir überschreiben den Prolog von avc_denied() mit einem xor eax, eax ; ret-Stub (über endbr64 unter IBT hinwegschreitend). Jede Ablehnung wird nun prozessweit als gewährt zurückgegeben, während getenforce immer noch Enforcing meldet. Dies ist auch eine Voraussetzung für den Escape: Der Cross-Domain-Re-Exec des Core-Dump-Handlers würde sonst verweigert.
Escape durch core_pattern. Wir überschreiben das globale core_pattern mit einem mit | präfixierten Handler, der über /proc/<PID>/root (das Container-Chroot des abstürzenden Tasks) auf unser eigenes Binary zeigt, und stürzen dann ein Child ab. Der Kernel führt den Handler als Usermodehelper aus, root in den initialen Namespaces, und wir leiten eine interaktive Root-Shell über einen Unix-Socket weiter, der innerhalb des Containers gebunden ist. Der Handler wird in den init-Namespaces geboren, daher ist keine Namespace-Chirurgie vor Ort und keiner der damit verbundenen PID-Namespace-Gefahren erforderlich.