Skip to content
KitploitKITPLOIT
ToolsBlog
Einreichen
ToolsBlog
Einreichen

Hacking-, PenTest- und Cybersicherheits-Tools für Ihr Sicherheitsarsenal!

Kitploit ist ein Verzeichnis von Hacking-, Cybersicherheits- und Pentesting-Tools. Entdecken Sie die neuesten Projekt-Updates, um Schwachstellen zu finden, Systeme zu analysieren, Tests zu automatisieren und Ihre Sicherheit zu stärken.

··Feeds·Kontakt·Datenschutz·© 2026 Kitploit

Tool-Verzeichnis

Kategorien

Alle Kategorien anzeigen
Loading categories
CVE-2021-3156-Heap-Based-Buffer-Overflow-in-Sudo-Baron-Samedit- — Technische Analyse und Exploit-Entwicklung für CVE-2021-3156, einen Heap-basierten Pufferüberlauf in Sudo, einschließlich drei funktionierender Exploits für lokale Privilegieneskalation auf wichtigen Linux-Distributionen. | Kitploit
Tools/GitHubGitHub/sornphut/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit-
Privilege EscalationSchwachstellenanalyseExploitationPenetrationstestsPapers & ForschungLernen & BildungBinary-Exploitation
GitHubsornphut/cve-2021-3156-heap-based-buffer-overflow-in-sudo-baron-samedit-

Beliebteste

Alle anzeigen →

Entdecken Sie die meistgenutzten Tools unserer Community.

Alle Tools erkunden

Durchsuchen Sie unsere Tool-Sammlung

Alle Tools anzeigen →
Teilen

CVE-2021-3156-Heap-Based-Buffer-Overflow-in-Sudo-Baron-Samedit-

Technische Analyse und Exploit-Entwicklung für CVE-2021-3156, einen Heap-basierten Pufferüberlauf in Sudo, einschließlich drei funktionierender Exploits für lokale Privilegieneskalation auf wichtigen Linux-Distributionen.

Repository anzeigen
4vor 1 JahrNoch nicht geprüft

Qualys Security Advisory

Baron Samedit: Heap-basierter Pufferüberlauf in Sudo (CVE-2021-3156)

======================================================================== Inhalt

Zusammenfassung Analyse Ausnutzung Danksagungen Zeitplan

======================================================================== Zusammenfassung

Wir haben einen heap-basierten Pufferüberlauf in Sudo (https://www.sudo.ws/) entdeckt. Diese Schwachstelle:

  • ist von jedem lokalen Benutzer ausnutzbar (normale Benutzer und Systembenutzer, sudoers und Nicht-sudoers), ohne Authentifizierung (d. h., der Angreifer muss das Passwort des Benutzers nicht kennen);

  • wurde im Juli 2011 eingeführt (Commit 8255ed69) und betrifft alle Legacy- Versionen von 1.8.2 bis 1.8.31p2 sowie alle stabilen Versionen von 1.9.0 bis 1.9.5p1 in ihrer Standardkonfiguration.

Wir haben drei verschiedene Exploits für diese Schwachstelle entwickelt und volle Root-Rechte auf Ubuntu 20.04 (Sudo 1.8.31), Debian 10 (Sudo 1.8.27) und Fedora 33 (Sudo 1.9.2) erlangt. Andere Betriebssysteme und Distributionen sind wahrscheinlich ebenfalls ausnutzbar.

======================================================================== Analyse

Wenn Sudo ausgeführt wird, um einen Befehl im "Shell"-Modus auszuführen (shell -c command):

  • entweder über die Option -s, die Sudos MODE_SHELL-Flag setzt;

  • oder über die Option -i, die Sudos MODE_SHELL- und MODE_LOGIN_SHELL-Flags setzt;

dann schreibt parse_args() zu Beginn von Sudos main() argv um (Zeilen 609-617), indem es alle Kommandozeilenargumente verkettet (Zeilen 587-595) und alle Meta-Zeichen mit Backslashes escaped (Zeilen 590-591):


571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) { 572 char **av, *cmnd = NULL; 573 int ac = 1; ... 581 cmnd = dst = reallocarray(NULL, cmnd_size, 2); ... 587 for (av = argv; *av != NULL; av++) { 588 for (src = *av; src != '\0'; src++) { 589 / quote potential meta characters */ 590 if (!isalnum((unsigned char)*src) && *src != '_' && *src != '-' && *src != '$') 591 *dst++ = '\'; 592 *dst++ = *src; 593 } 594 -c cmnd */ ... 603 av = reallocarray(NULL, ac + 1, sizeof(char *)); ... 609 av[0] = (char plugin may override shell */ 610 if (cmnd != NULL) { 611 av[1] = "-c"; 612 av[2] = cmnd; 613 } 614 av[ac] = NULL; 615 616 argv = av; 617 argc = ac; 618 }

Später, in sudoers_policy_main(), verkettet set_cmnd() die Kommandozeilenargumente in einen heap-basierten Puffer "user_args" (Zeilen 864-871) und entfernt das Escaping der Meta-Zeichen (Zeilen 866-867), "für sudoers-Abgleich- und Protokollierungszwecke":


819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { ... 852 for (size = 0, av = NewArgv + 1; *av; av++) 853 size += strlen(*av) + 1; 854 if (size == 0 || (user_args = malloc(size)) == NULL) { ... 857 } 858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) { ... 864 for (to = user_args, av = NewArgv + 1; (from = *av); av++) { 865 while (*from) { 866 if (from[0] == '\' && !isspace((unsigned char)from[1])) 867 from++; 868 *to++ = *from++; 869 } 870 *to++ = ' '; 871 } ... 884 } ... 886 }

Leider gilt: Wenn ein Kommandozeilenargument mit einem einzelnen Backslash- Zeichen endet, dann:

  • ist bei Zeile 866 "from[0]" das Backslash-Zeichen und "from[1]" der Nullterminator des Arguments (d. h., kein Leerzeichen);

  • wird bei Zeile 867 "from" inkrementiert und zeigt auf den Nullterminator;

  • wird bei Zeile 868 der Nullterminator in den Puffer "user_args" kopiert, und "from" wird erneut inkrementiert und zeigt auf das erste Zeichen nach dem Nullterminator (d. h., außerhalb der Grenzen des Arguments);

  • liest und kopiert die "while"-Schleife bei den Zeilen 865-869 Zeichen außerhalb der Grenzen in den Puffer "user_args".

Mit anderen Worten: set_cmnd() ist anfällig für einen heap-basierten Puffer- überlauf, weil die Zeichen außerhalb der Grenzen, die in den Puffer "user_args" kopiert werden, in dessen Größe (berechnet in den Zeilen 852-853) nicht enthalten waren.

Theoretisch kann jedoch kein Kommandozeilenargument mit einem einzelnen Backslash-Zeichen enden: Wenn MODE_SHELL oder MODE_LOGIN_SHELL gesetzt ist (Zeile 858, eine notwendige Bedingung, um den anfälligen Code zu erreichen), dann ist MODE_SHELL gesetzt (Zeile 571) und parse_args() hat bereits alle Meta-Zeichen escaped, einschließlich Backslashes (d. h., es hat jeden einzelnen Backslash mit einem zweiten Backslash escaped).

In der Praxis jedoch sind der anfällige Code in set_cmnd() und der Escape- Code in parse_args() von geringfügig unterschiedlichen Bedingungen umgeben:


819 if (sudo_mode & (MODE_RUN | MODE_EDIT | MODE_CHECK)) { ... 858 if (ISSET(sudo_mode, MODE_SHELL|MODE_LOGIN_SHELL)) {

versus:


571 if (ISSET(mode, MODE_RUN) && ISSET(flags, MODE_SHELL)) {

Unsere Frage lautet also: Können wir MODE_SHELL und entweder MODE_EDIT oder MODE_CHECK setzen (um den anfälligen Code zu erreichen), aber nicht das standardmäßige MODE_RUN (um den Escape-Code zu vermeiden)?

Die Antwort scheint nein zu sein: Wenn wir MODE_EDIT (Option -e, Zeile 361) oder MODE_CHECK (Option -l, Zeilen 423 und 519) setzen, dann entfernt parse_args() MODE_SHELL aus den "valid_flags" (Zeilen 363 und 424) und beendet mit einem Fehler, wenn wir ein ungültiges Flag wie MODE_SHELL angeben (Zeilen 532-533):


358 case 'e': ... 361 mode = MODE_EDIT; 362 sudo_settings[ARG_SUDOEDIT].value = "true"; 363 valid_flags = MODE_NONINTERACTIVE; 364 break; ... 416 case 'l': ... 423 mode = MODE_LIST; 424 valid_flags = MODE_NONINTERACTIVE|MODE_LONG_LIST; 425 break; ... 518 if (argc > 0 && mode == MODE_LIST) 519 mode = MODE_CHECK; ... 532 if ((flags & valid_flags) != flags) 533 usage(1);

Aber wir haben eine Hintertür gefunden: Wenn wir Sudo als "sudoedit" statt als "sudo" ausführen, dann setzt parse_args() automatisch MODE_EDIT (Zeile 270), setzt aber "valid_flags" nicht zurück, und die "valid_flags" enthalten MODE_SHELL standardmäßig (Zeilen 127 und 249):


127 #define DEFAULT_VALID_FLAGS (MODE_BACKGROUND|MODE_PRESERVE_ENV|MODE_RESET_HOME|MODE_LOGIN_SHELL|MODE_NONINTERACTIVE|MODE_SHELL) ... 249 int valid_flags = DEFAULT_VALID_FLAGS; ... 267 proglen = strlen(progname); 268 if (proglen > 4 && strcmp(progname + proglen - 4, "edit") == 0) { 269 progname = "sudoedit"; 270 mode = MODE_EDIT; 271 sudo_settings[ARG_SUDOEDIT].value = "true"; 272 }

Folglich setzen wir, wenn wir "sudoedit -s" ausführen, sowohl MODE_EDIT als auch MODE_SHELL (aber nicht MODE_RUN), vermeiden den Escape-Code, erreichen den anfälligen Code und überlaufen den heap-basierten Puffer "user_args" durch ein Kommandozeilenargument, das mit einem einzelnen Backslash-Zeichen endet:


sudoedit -s '' perl -e 'print "A" x 65536' malloc(): corrupted top size Aborted (core dumped)

Aus Sicht eines Angreifers ist dieser Pufferüberlauf ideal:

  • wir kontrollieren die Größe des "user_args"-Puffers, den wir überlaufen (die Größe unserer verketteten Kommandozeilenargumente, in den Zeilen 852-854);

  • wir kontrollieren unabhängig die Größe und den Inhalt des Überlaufs selbst (auf unser letztes Kommandozeilenargument folgen praktischerweise unsere ersten Umgebungsvariablen, die nicht in die Größenberechnung einbezogen werden bei den Zeilen 852-853);

  • wir können sogar Null-Bytes in den Puffer schreiben, den wir überlaufen (jedes Kommandozeilenargument oder jede Umgebungsvariable, die mit einem einzelnen Backslash endet, schreibt ein Null-Byte in "user_args", bei den Zeilen 866-868).

Zum Beispiel weist der folgende Befehl auf einem amd64-Linux einen 24-Byte-"user_args"-Puffer zu (einen 32-Byte-Heap-Chunk) und überschreibt das Größenfeld des nächsten Chunks mit "A=a\0B=b\0" (0x00623d4200613d41), dessen fd-Feld mit "C=c\0D=d\0" (0x00643d4400633d43) und dessen bk-Feld mit "E=e\0F=f\0" (0x00663d4600653d45):


env -i 'AA=a' 'B=b' 'C=c' 'D=d' 'E=e' 'F=f' sudoedit -s '1234567890123456789012'

--|--------+--------+--------+--------|--------+--------+--------+--------+-- | | |12345678|90123456|789012.A|A=a.B=b.|C=c.D=d.|E=e.F=f.| --|--------+--------+--------+--------|--------+--------+--------+--------+-- size <---- user_args buffer ----> size fd bk

======================================================================== Ausnutzung

Weil Sudo Lokalisierungsfunktionen ganz am Anfang seiner main()-Funktion aufruft:


154 setlocale(LC_ALL, ""); 155 bindtextdomain(PACKAGE_NAME, LOCALEDIR); 156 textdomain(PACKAGE_NAME);

und Übersetzungsstrings (durch die gettext()-Funktion und das _()-Makro) an Format-String-Funktionen übergibt, wie zum Beispiel:


301 sudo_printf(SUDO_CONV_ERROR_MSG, _("%s is not in the sudoers " 302 "file. This incident will be reported.\n"), user_name);

wollten wir zunächst halfdogs faszinierende Technik von https://www.halfdog.net/Security/2017/LibcRealpathBufferUnderflow/ wiederverwenden und Sudos heap-basierten Pufferüberlauf in einen Format-String-Exploit verwandeln. Genauer gesagt:

  • bei Zeile 154, in setlocale(), malloc()en und free()en wir mehrere LC- Umgebungsvariablen (LC_CTYPE, LC_MESSAGES, LC_TIME usw.) und erzeugen dadurch kleine Lücken am Anfang von Sudos Heap (freie Fast- oder Tcache-Chunks);

  • bei Zeile 155 malloc()t bindtextdomain() eine struct binding, die einen dirname-Zeiger auf den Namen eines Verzeichnisses enthält, das ".mo"-Katalogdateien und damit Übersetzungsstrings enthält;

  • in set_cmnd() malloc()en wir den "user_args"-Puffer in eine der Lücken am Anfang von Sudos Heap und überlaufen diesen Puffer, wodurch wir den dirname-Zeiger der struct binding überschreiben;

  • bei Zeile 301 (zum Beispiel) lädt gettext() (durch das _()-Makro) unseren eigenen Übersetzungsstring aus dem überschriebenen dirname -- mit anderen Worten, wir kontrollieren den Format-String, der an sudo_printf() übergeben wird.

Um diese anfängliche Technik zu implementieren, schrieben wir einen rudimentären Brute-Forcer, der Sudo innerhalb von gdb ausführt, den "user_args"-Puffer überläuft und die folgenden Parameter zufällig auswählt:

  • die LC-Umgebungsvariablen, die wir an Sudo übergeben, und ihre Länge (wir verwenden das Gebietsschema "C.UTF-8" und hängen einen zufälligen "@modifier" an);

  • die Größe des "user_args"-Puffers, den wir überlaufen;

  • die Größe des Überlaufs selbst;

  • ob wir durch Sudos Authentifizierungscode gehen (-A- oder -n-Option) oder nicht (-u #realuid-Option).

Leider schlug diese anfängliche Technik fehl; unser Brute-Forcer konnte zwar den dirname-Zeiger der struct binding überschreiben:


Program received signal SIGSEGV, Segmentation fault.

0x00007f6e0dde1ea9 in __dcigettext (domainname=domainname@entry=0x7f6e0d9cc020 "sudoers", msgid1=msgid1@entry=0x7f6e0d9cc014 "user NOT in sudoers", msgid2=msgid2@entry=0x0, plural=plural@entry=0, n=n@entry=0, category=5) at dcigettext.c:619

=> 0x7f6e0dde1ea9 <__dcigettext+1257>: cmpb $0x2f,(%rax)

rax 0x4141414141414141 4702111234474983745

aber LC_MESSAGES war immer das standardmäßige "C"-Gebietsschema (nicht "C.UTF-8"), was die String-Übersetzung in gettext() deaktiviert (d. h., gettext() gibt den ursprünglichen Format-String zurück, nicht unseren eigenen).

Glücklicherweise erzeugte unser Brute-Forcer jedoch Dutzende einzigartiger Sudo-Abstürze und gdb-Backtraces; darunter erregten drei unsere Aufmerksamkeit, und wir haben letztendlich alle drei ausgenutzt.

======================================================================== 1/ struct sudo_hook_entry-Überschreibung

Der erste Absturz, der unsere Aufmerksamkeit erregte, ist:


Program received signal SIGSEGV, Segmentation fault.

0x000056291a25d502 in process_hooks_getenv (name=name@entry=0x7f4a6d7dc046 "SYSTEMD_BYPASS_USERDB", value=value@entry=0x7ffc595cc240) at ../../src/hooks.c:108

=> 0x56291a25d502 <process_hooks_getenv+82>: callq *0x8(%rbx)

rbx 0x56291c1df2b0 94734565372592

0x56291c1df2b0: 0x4141414141414141 0x4141414141414141

Unglaublicherweise stürzte Sudos Funktion process_hooks_getenv() ab (bei Zeile 108), weil wir direkt einen Funktionszeiger überschrieben, getenv_fn (ein Mitglied einer heap-basierten struct sudo_hook_entry):


99 int 100 process_hooks_getenv(const char *name, char **value) 101 { 102 struct sudo_hook_entry *hook; 103 char *val = NULL; ... 107 SLIST_FOREACH(hook, &sudo_hook_getenv_list, entries) { 108 rc = hook->u.getenv_fn(name, &val, hook->closure);

Um diese struct sudo_hook_entry-Überschreibung auszunutzen, stellen wir fest:

  • der Aufruf von getenv_fn (bei Zeile 108) ist kompatibel mit einem Aufruf von execve():

    . name ("SYSTEMD_BYPASS_USERDB") ist kompatibel mit dem Pfadnamen-Argument von execve();

    . &val (ein Zeiger auf einen NULL-Zeiger) ist kompatibel mit dem argv von execve();

    . hook->closure (ein NULL-Zeiger) ist kompatibel mit dem envp von execve();

  • wir können ASLR besiegen, indem wir den Funktionszeiger getenv_fn teilweise überschreiben (der auf die Funktion sudoers_hook_getenv() in der gemeinsamen Bibliothek sudoers.so zeigt); und glücklicherweise enthält der Anfang von sudoers.so einen Aufruf von execve() (oder execv()):


0000000000008a00 execv@plt: 8a00: f3 0f 1e fa endbr64 8a04: f2 ff 25 65 55 05 00 bnd jmpq *0x55565(%rip) # 5df70 <execv@GLIBC_2.2.5> 8a0b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

  • wir können auf Ubuntu /dev/kmsg (dmesg) als unprivilegierter Benutzer lesen und erhalten daher detaillierte Informationen über unsere Sudo-Abstürze.

Folglich verfolgen wir die folgende Strategie:

  • Zuerst brute-forcen wir die Exploit-Parameter, bis wir getenv_fn mit einer ungültigen Userland-Adresse (oberhalb von 0x800000000000) überschreiben -- bis wir einen General Protection Fault an der Aufrufstelle von getenv_fn beobachten:

sudoedit[15904] general protection fault ip:55e9b645b502 sp:7ffe53d6fa40 error:0 in sudo[55e9b644e000+1a000] ^^^

  • Als Nächstes verwenden wir diese Exploit-Parameter erneut, überschreiben getenv_fn jedoch mit einem regulären Muster aus gültigen (unterhalb von 0x800000000000), aber nicht abgebildeten Userland-Adressen -- in diesem Beispiel ist getenv_fn der 22. Zeiger, den wir überschreiben (0x32 ist '2', ein Teil unseres Musters):

sudoedit[15906]: segfault at 323230303030 ip 0000323230303030 sp 00007ffeeabf2868 error 14 in sudo[55b036c16000+5000] ^^^^

  • Zuletzt überschreiben wir getenv_fn teilweise (wir überschreiben seine zwei niederwertigsten Bytes mit 0x8a00, dem Offset von execv() in sudoers.so, und sein drittes Byte mit 0x00, dem Nullterminator von user_args in set_cmnd()), bis wir ASLR besiegen -- wir haben nach 2^(3*8-12) = 2^12 = 4096 Versuchen eine gute Chance, getenv_fn mit der Adresse von execv() zu überschreiben und so unsere eigene Binärdatei namens "SYSTEMD_BYPASS_USERDB" als Root auszuführen.

Wir haben diesen ersten Exploit erfolgreich auf Ubuntu 20.04 getestet.

======================================================================== 2/ struct service_user-Überschreibung

Der zweite Absturz, der unsere Aufmerksamkeit erregte, ist:


Program received signal SIGSEGV, Segmentation fault.

0x00007f6bf9c294ee in nss_load_library (ni=ni@entry=0x55cf1a1dd040) at nsswitch.c:344

=> 0x7f6bf9c294ee <nss_load_library+46>: cmpq $0x0,0x8(%rbx)

rbx 0x41414141414141 18367622009667905

Die glibc-Funktion nss_load_library() stürzte ab (bei Zeile 344), weil wir den Zeiger "library", ein Mitglied einer heap-basierten struct service_user, überschrieben:


327 static int 328 nss_load_library (service_user ni) 329 { 330 if (ni->library == NULL) 331 { ... 338 ni->library = nss_new_service (service_table ?: &default_table, 339 ni->name); ... 342 } 343 344 if (ni->library->lib_handle == NULL) 345 { 346 / Load the shared library. / 347 size_t shlen = (7 + strlen (ni->name) + 3 348 + strlen (__nss_shlib_revision) + 1); 349 int saved_errno = errno; 350 char shlib_name[shlen]; 351 352 / Construct shared object name. */ 353 __stpcpy (__stpcpy (__stpcpy (_"), 355 ni->name), 356 ".so"), 357 __nss_shlib_revision); 358 359 ni->library->lib_handle = __libc_dlopen (shlib_name);

Wir können diese struct service_user-Überschreibung leicht in eine beliebige Codeausführung umwandeln:

  • wir überschreiben ni->library mit einem NULL-Zeiger, um in den Block bei den Zeilen 330-342 einzutreten, den Absturz bei Zeile 344 zu vermeiden und in den Block bei den Zeilen 344-359 einzutreten;

  • wir überschreiben ni->name (ein Zeichenarray, anfänglich "systemd") mit "X/X";

  • die Zeilen 353-357 konstruieren den Namen einer gemeinsamen Bibliothek "libnss_X/X.so.2" (anstelle von "libnss_systemd.so.2");- In Zeile 359 laden wir unsere eigene Shared Library "libnss_X/X.so.2" aus dem aktuellen Arbeitsverzeichnis und führen unseren _init()-Konstruktor als root aus.

Wir haben diesen zweiten Exploit erfolgreich auf Ubuntu 20.04, Debian 10, und Fedora 33 getestet.

======================================================================== 3/ def_timestampdir überschreiben

Unser dritter Exploit stammt nicht aus einem der Sudo-Abstürze, sondern aus einer beiläufigen Beobachtung: Während unserer Brute-Force-Phase erzeugte Sudo dutzende neue Verzeichnisse in unserem aktuellen Arbeitsverzeichnis (AAAAAA, AAAAAAAAA, usw.). Jedes dieser Verzeichnisse gehört root und enthält nur eine kleine Datei, die nach unserem eigenen Benutzer benannt ist: Sudos Zeitstempeldatei -- wir haben offensichtlich def_timestampdir überschrieben, den Namen von Sudos Zeitstempelverzeichnis.

Wenn wir def_timestampdir mit dem Namen eines Verzeichnisses überschreiben, das noch nicht existiert, können wir gegen Sudos ts_mkdirs() antreten, einen Symlink auf eine beliebige Datei erstellen und:

3a/ entweder chown() wir diese beliebige Datei auf Benutzer root und Gruppe root;

3b/ oder öffnen (oder erstellen) wir diese beliebige Datei als root und schreiben eine struct timestamp_entry hinein.

Wir konnten 3a/ nicht in vollständige Root-Rechte umwandeln (wenn wir zum Beispiel unsere eigene SUID-Binärdatei mit chown() auf root setzen, dann entfernt der Kernel automatisch das SUID-Bit unseres Binärprogramms). Wenn Sie, liebe Leserin oder lieber Leser, eine Lösung für dieses Problem finden, posten Sie sie bitte auf der öffentlichen oss-security-Mailingliste!

Schließlich konnten wir 3b/ in vollständige Root-Rechte umwandeln, aber wir sahen uns zunächst zwei Problemen gegenüber:

  • Sudos timestamp_open() löscht unseren beliebigen Symlink, wenn die Datei, auf die er zeigt, älter als die Bootzeit ist. Wir konnten dieses erste Problem lösen, indem wir eine sehr alte Zeitstempeldatei (aus der Unix-Epoche) erstellten, indem wir warteten, bis timestamp_open() sie löscht, und indem wir gegen timestamp_open() antraten, um unseren endgültigen, beliebigen Symlink zu erstellen.

  • Wir kontrollieren nicht den Inhalt der struct timestamp_entry, die in die beliebige Datei geschrieben wird. Nach unserem besten Wissen kontrollieren wir nur drei Bytes (eine Prozess-ID oder eine struct timespec), und wir waren nicht in der Lage, dieses Drei-Byte-Schreiben in vollständige Root-Rechte umzuwandeln. Wenn Sie, liebe Leserin oder lieber Leser, eine Lösung für dieses Problem finden, posten Sie sie bitte auf der öffentlichen oss-security-Mailingliste!

Allerdings konnten wir dieses zweite Problem umgehen, indem wir einen kleinen Fehler in Sudos timestamp_lock() ausnutzten. Wenn wir die beiden Wettläufe gegen ts_mkdirs() und timestamp_open() gewinnen und wenn unser beliebiger Symlink auf /etc/passwd zeigt, dann wird diese Datei als root geöffnet, und:


65 struct timestamp_entry { 66 unsigned short version; /* version number / 67 unsigned short size; / entry size / 68 unsigned short type; / TS_GLOBAL, TS_TTY, TS_PPID */ .. 78 };

305 static ssize_t 306 ts_write(int fd, const char *fname, struct timestamp_entry *entry, off_t offset) 307 { ... 318 nwritten = pwrite(fd, entry, entry->size, offset); ... 350 }

619 bool 620 timestamp_lock(void *vcookie, struct passwd *pw) 621 { 622 struct ts_cookie *cookie = vcookie; 623 struct timestamp_entry entry; ... 644 nread = read(cookie->fd, &entry, sizeof(entry)); 645 if (nread == 0) { ... 652 } else if (entry.type != TS_LOCKEXCL) { ... 657 if (ts_write(cookie->fd, cookie->fname, &entry, 0) == -1)

  • In Zeile 644 werden die ersten 0x38 Bytes von /etc/passwd ("root❌0:0:...") in eine stack-basierte struct timestamp_entry, entry, gelesen;

  • In Zeile 652 ist entry.type 0x783a (":x"), nicht TS_LOCKEXCL;

  • In den Zeilen 657 und 318 werden entry->size Bytes aus dem stack-basierten entry nach /etc/passwd geschrieben, aber entry->size ist tatsächlich 0x746f ("ot"), nicht sizeof(struct timestamp_entry).

Als Ergebnis schreiben wir den gesamten Inhalt von Sudos Stack nach /etc/passwd (einschließlich unserer Befehlszeilenargumente und unserer Umgebungsvariablen): Wir fügen einen beliebigen Benutzer in /etc/passwd ein und erhalten dadurch volle Root- Rechte. Wir haben diesen dritten Exploit erfolgreich auf Ubuntu 20.04 getestet.

Hinweis: Dieser kleine Fehler in timestamp_lock() wurde im Januar 2020 durch Commit 586b418a behoben, aber dieser Fix wurde nicht auf ältere Versionen zurückportiert.

======================================================================== Danksagungen

Wir danken Todd C. Miller für seine Professionalität, seine schnelle Reaktion und die akribische Aufmerksamkeit für jedes Detail in unserem Bericht. Wir danken auch den Mitgliedern von distros@openwall.

======================================================================== Zeitplan

2021-01-13: Advisory an Todd.Miller@sudo gesendet.

2021-01-19: Advisory und Patches an distros@openwall gesendet.

2021-01-26: Koordinierter Veröffentlichungstermin (18:00 Uhr UTC).

Tool herunterladen
dst++ = ' '; 595 } ... 600 ac += 2; /
)user_details.shell; /
stpcpy (shlib_name, 354 "libnss